fraud-sim 0.2.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,91 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.2.0] — LLM Adaptive Attacker
9
+
10
+ ### Added
11
+
12
+ - **`adaptive-attacker` scenario** — the headline feature. An attacker that
13
+ probes a login endpoint and, after each block, asks an LLM what to change for
14
+ the next attempt (IP, device fingerprint, user agent, timing). Emits a new
15
+ `attacker_adapts` event carrying the LLM's reasoning *before* each adapted
16
+ attempt. Reports `rounds`, `outcome` (`attacker_succeeded` /
17
+ `attacker_defeated`), and `strategyHistory`.
18
+ - **LLM provider interface and three providers** (`fraud-sim/llm`):
19
+ - `deterministic` — no-API fallback using seeded heuristics; reproducible.
20
+ Used automatically when a scenario is run without an `llm`.
21
+ - `openai` — OpenAI Chat Completions provider.
22
+ - `anthropic` — Anthropic Messages API provider (default model
23
+ `claude-sonnet-5`).
24
+ Both API providers fall back to a randomised strategy on any error (timeout,
25
+ non-2xx, invalid JSON) and never throw.
26
+ - `src/utils/rng.js` — `DeterministicRng`, a seeded PRNG for reproducible runs.
27
+ - `src/streaming/sse.js` — `sseStream(res)` Server-Sent Events helper for
28
+ streaming simulation events to a browser dashboard (Task 2.6). Sets SSE
29
+ headers, frames events as `data: {...}\n\n`, sends a `{"type":"done"}`
30
+ sentinel on `close()`, and queues writes under backpressure until `drain`.
31
+ - `./llm` export subpath in `package.json`.
32
+ - 37 new tests (72 total, all passing).
33
+
34
+ ### Fixed
35
+
36
+ - `package.json` declared a `"./streaming"` export subpath but `src/streaming/`
37
+ did not exist — `import 'fraud-sim/streaming'` failed with
38
+ `ERR_MODULE_NOT_FOUND`. The directory now exists and the subpath resolves.
39
+
40
+ ### Notes
41
+
42
+ - The `adaptive-attacker` scenario paces rounds by `options.delayMs`, not the
43
+ attacker's chosen delay. The attacker's timing decision is recorded in the
44
+ `attacker_adapts` event and `strategyHistory` but not enacted as a real
45
+ wall-clock wait — a velocity-evasion delay of 5–15s per round would make a
46
+ simulation take minutes for no benefit.
47
+
48
+ ## [0.1.0] — Initial release
49
+
50
+ ### Added
51
+
52
+ - Core abstractions: `Runner`, `Scenario`, `TargetAdapter`, `ScenarioEvent`
53
+ - Public API: `run()`, `runSuite()`, `listScenarios()`, `registerScenario()`, `unregisterScenario()`
54
+ - Two scenarios:
55
+ - `credential-stuffing` — botnet-style login attacks with rotating IPs and user agents
56
+ - `account-drain` — takeover, password change, and rapid withdrawal pattern
57
+ - Three target adapters:
58
+ - `httpAdapter` — works against any REST API via configurable `responseMap`
59
+ - `mockAdapter` — sequenced responses for unit testing scenarios
60
+ - `loggingAdapter` — inspects payloads without sending to a network
61
+ - Abort-aware execution via `AbortSignal`
62
+ - Structured event stream with typed events: `attempt`, `response`, `milestone`, `error`, `summary`
63
+ - Subscriber isolation: buggy event handlers do not crash simulations
64
+ - Safety defaults: maximum attempt ceilings, paced delays, no infinite loops
65
+ - 35 tests (33 unit, 2 integration) — all passing in under 1 second
66
+ - Examples directory with three runnable demos:
67
+ - `basic-credential-stuffing/` — colored CLI output of an attack
68
+ - `account-drain-demo/` — narrated drain attack
69
+ - `express-mock-target/` — local target to test against
70
+
71
+ ### Design decisions
72
+
73
+ - No npm dependencies — fraud-sim ships zero runtime dependencies. Internal `random.js` replaces faker.
74
+ - Target-agnostic by design — no FriskLayer-specific code paths
75
+ - ES modules only (Node 20+)
76
+ - Tests use the built-in Node test runner (no Jest, no Mocha)
77
+
78
+ ### Out of scope (Phase 2+)
79
+
80
+ - LLM-driven adaptive attacker (v0.2)
81
+ - Server-Sent Events helper (v0.2)
82
+ - Seven additional scenarios (v0.3)
83
+ - CLI runner (v0.3)
84
+ - Reporting and benchmarking suite (v0.4)
85
+ - Community scenario library (v1.0)
86
+
87
+ See `IMPLEMENTATION_GUIDE.md` for the detailed phase plan.
88
+
89
+ [Unreleased]: https://github.com/arthurr-beep/fraudsim/compare/v0.2.0...HEAD
90
+ [0.2.0]: https://github.com/arthurr-beep/fraudsim/compare/v0.1.0...v0.2.0
91
+ [0.1.0]: https://github.com/arthurr-beep/fraudsim/releases/tag/v0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 FriskLayer maintainers and contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,485 @@
1
+ # fraud-sim
2
+
3
+ **Realistic synthetic attack traffic for testing fintech fraud detection systems.**
4
+
5
+ [![npm version](https://img.shields.io/npm/v/fraud-sim.svg)](https://www.npmjs.com/package/fraud-sim)
6
+ [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
+ [![tests](https://github.com/arthurr-beep/fraudsim/workflows/test/badge.svg)](https://github.com/arthurr-beep/fraudsim/actions)
8
+ [![lint](https://github.com/arthurr-beep/fraudsim/workflows/lint/badge.svg)](https://github.com/arthurr-beep/fraudsim/actions)
9
+ [![secret-scan](https://github.com/arthurr-beep/fraudsim/workflows/secret-scan/badge.svg)](https://github.com/arthurr-beep/fraudsim/actions)
10
+ [![node](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](https://nodejs.org)
11
+
12
+ `fraud-sim` is a target-agnostic library for generating realistic fraud attack traffic — credential stuffing, account takeovers, card testing, and LLM-driven adaptive attackers — against any fraud detection system you want to evaluate.
13
+
14
+ You build the defence. We bring the adversary.
15
+
16
+ ```js
17
+ import { run, httpAdapter } from 'fraud-sim';
18
+
19
+ const target = httpAdapter({
20
+ baseUrl: 'https://api.your-fraud-system.com',
21
+ headers: { Authorization: `Bearer ${process.env.API_KEY}` },
22
+ });
23
+
24
+ const report = await run('credential-stuffing', {
25
+ options: { targetUserId: 'user_001', attackerCount: 50 },
26
+ target,
27
+ onEvent: (event) => console.log(event.type, event),
28
+ });
29
+
30
+ console.log(`Blocked ${report.blocked} of ${report.attempts} attempts (${(report.blockRate * 100).toFixed(1)}%)`);
31
+ ```
32
+
33
+ ---
34
+
35
+ ## Why this exists
36
+
37
+ Every team building fraud detection needs to answer one question: *does it actually work?*
38
+
39
+ The honest ways to find out are limited. You can wait for real attackers (expensive in losses, slow in feedback). You can write throwaway shell scripts (different per engineer, never reused, never reviewed). You can buy proprietary attack libraries from vendors (closed, locked to one product, often years out of date).
40
+
41
+ `fraud-sim` is the open alternative: a maintained, scenario-extensible library that any team can point at any target. Test your in-house rules engine. Benchmark Stripe Radar. Compare two vendors side by side. Run it in CI to catch regressions when you change a threshold.
42
+
43
+ It works against any HTTP API. It uses no real fraud data. It includes an LLM-powered adaptive attacker that learns from your blocks and changes strategy mid-attack — because real attackers do exactly that.
44
+
45
+ ---
46
+
47
+ ## What it does
48
+
49
+ - **9 attack scenarios out of the box** — credential stuffing, account drain, card testing, promo abuse, mule networks, slow drain, session hijack, SIM swap takeover, and the adaptive LLM attacker
50
+ - **Target-agnostic** — works against any REST API via a configurable adapter
51
+ - **LLM-driven adaptive attacker** — bring your own OpenAI, Anthropic, or local model. A deterministic fallback works without any LLM
52
+ - **Structured event stream** — every action emits a typed event. Pipe it to a dashboard, log it, or stream it via Server-Sent Events
53
+ - **Benchmarking and reporting** — run scenarios as a suite, compare two systems, generate shareable HTML reports, integrate with CI
54
+ - **Safe by default** — modest attack rates that won't accidentally DDoS your own staging environment
55
+
56
+ ---
57
+
58
+ ## What it doesn't do
59
+
60
+ - **It doesn't detect fraud.** It's the attacker, not the defender. You bring the detection system.
61
+ - **It doesn't ship with real fraud data.** Every payload is synthetic. No leaks, no licensing risks, no privacy concerns.
62
+ - **It isn't an attack tool.** Use it against systems you own or have written permission to test. The README will keep saying this.
63
+
64
+ ---
65
+
66
+ ## Install
67
+
68
+ ```bash
69
+ npm install fraud-sim
70
+ ```
71
+
72
+ Requires Node.js 20 or higher. Works with ESM and CommonJS.
73
+
74
+ ---
75
+
76
+ ## Test it safely first (no real system needed)
77
+
78
+ Everything can be exercised against targets that live entirely on your machine
79
+ before you ever point it at a production fraud system. Two shipped options:
80
+
81
+ **In-process mock — zero dependencies, no network.** Script the responses a
82
+ detection system would return and run a scenario against them:
83
+
84
+ ```js
85
+ import { run } from 'fraud-sim';
86
+ import { mockAdapter } from 'fraud-sim/adapters';
87
+
88
+ const target = mockAdapter({
89
+ scoreLogin: [{ decision: 'ALLOW' }, { decision: 'STEP_UP' }, { decision: 'BLOCK' }],
90
+ });
91
+ const report = await run('credential-stuffing', {
92
+ target,
93
+ options: { targetUserId: 'victim_001', attackerCount: 3, attemptsPerIp: 1 },
94
+ });
95
+ console.log(report); // { attempts, blocked, stepUp, allowed, blockRate, ... }
96
+ ```
97
+
98
+ **Bundled mock HTTP target — a realistic practice range.** The package ships a
99
+ small Express server with an actual rules engine. Install `express` once
100
+ (`npm install express`), then:
101
+
102
+ ```bash
103
+ npm run example:mock-target # terminal 1 → http://localhost:4000
104
+ npm run example:basic # terminal 2 → attacks it, watch decisions escalate
105
+ ```
106
+
107
+ Graduating to the real thing is a one-line change — swap the `baseUrl`. Full
108
+ walk-through in **[docs/TESTING.md](docs/TESTING.md)**.
109
+
110
+ ---
111
+
112
+ ## Quickstart — 5 minutes from zero to your first simulation
113
+
114
+ ### 1. Set up a target
115
+
116
+ Any HTTP API that takes a JSON payload and returns a decision. The simplest target is your own Express server returning `{ decision: 'ALLOW' }` for everything — useful for verifying the setup works before pointing at a real fraud system.
117
+
118
+ ```js
119
+ // example-target.js
120
+ import express from 'express';
121
+ const app = express();
122
+ app.use(express.json());
123
+ app.post('/score/login', (req, res) => {
124
+ res.json({ decision: 'ALLOW', risk_score: 0.1, reasons: [] });
125
+ });
126
+ app.listen(3000, () => console.log('Target listening on :3000'));
127
+ ```
128
+
129
+ ### 2. Run a simulation against it
130
+
131
+ ```js
132
+ // run-sim.js
133
+ import { run, httpAdapter } from 'fraud-sim';
134
+
135
+ const target = httpAdapter({
136
+ baseUrl: 'http://localhost:3000',
137
+ endpoints: { scoreLogin: '/score/login' },
138
+ });
139
+
140
+ const report = await run('credential-stuffing', {
141
+ options: { targetUserId: 'test_user_001', attackerCount: 10 },
142
+ target,
143
+ onEvent: (event) => {
144
+ if (event.type === 'response') {
145
+ console.log(`Attempt ${event.round}: ${event.response.decision} (${event.latencyMs}ms)`);
146
+ }
147
+ },
148
+ });
149
+
150
+ console.log('\nFinal report:', report);
151
+ ```
152
+
153
+ ```
154
+ Attempt 0: ALLOW (3ms)
155
+ Attempt 1: ALLOW (2ms)
156
+ Attempt 2: ALLOW (2ms)
157
+ ...
158
+ Final report: { attempts: 30, blocked: 0, stepUp: 0, allowed: 30, blockRate: 0 }
159
+ ```
160
+
161
+ The example target allows everything — block rate is 0%. Now point `httpAdapter` at your real fraud system and see what changes.
162
+
163
+ ### 3. Try the LLM-driven adaptive attacker
164
+
165
+ ```js
166
+ import { run, httpAdapter } from 'fraud-sim';
167
+ import { anthropic } from 'fraud-sim/llm';
168
+
169
+ const target = httpAdapter({
170
+ baseUrl: 'https://api.your-fraud-system.com',
171
+ headers: { Authorization: `Bearer ${process.env.API_KEY}` },
172
+ });
173
+
174
+ const llm = anthropic({
175
+ apiKey: process.env.ANTHROPIC_API_KEY,
176
+ model: 'claude-sonnet-4-20250514',
177
+ });
178
+
179
+ const report = await run('adaptive-attacker', {
180
+ options: { targetUserId: 'test_user_001', maxRounds: 10 },
181
+ target,
182
+ llm,
183
+ onEvent: (event) => {
184
+ if (event.type === 'attacker_adapts') {
185
+ console.log(`Round ${event.round}: ${event.reasoning}`);
186
+ }
187
+ if (event.type === 'response') {
188
+ console.log(` → ${event.response.decision} (${event.response.reasons?.map(r => r.code).join(', ')})`);
189
+ }
190
+ },
191
+ });
192
+
193
+ console.log(`\nOutcome: ${report.outcome}`);
194
+ console.log(`The attacker took ${report.rounds} rounds to ${report.outcome === 'attacker_defeated' ? 'give up' : 'succeed'}.`);
195
+ ```
196
+
197
+ Don't have an LLM API key? Use the deterministic fallback — it runs without any external API and produces reproducible results:
198
+
199
+ ```js
200
+ import { deterministic } from 'fraud-sim/llm';
201
+ const llm = deterministic({ seed: 42 });
202
+ ```
203
+
204
+ ---
205
+
206
+ ## The scenarios
207
+
208
+ | ID | What it simulates |
209
+ |---|---|
210
+ | `credential-stuffing` | A botnet authenticates using stolen credentials across many IPs and rotating user agents |
211
+ | `account-drain` | Attacker logs in from a new device, changes the password, and rapidly drains funds to new payees |
212
+ | `adaptive-attacker` | An LLM-driven attacker that observes blocks and reason codes, then adapts its strategy |
213
+ | `card-testing` | BIN attacks, micro-authorisation probing, and automated card validation patterns |
214
+ | `promo-abuse` | One actor creating many accounts to farm signup bonuses and referral rewards |
215
+ | `mule-network` | Coordinated transfers between accounts simulating fund laundering |
216
+ | `slow-drain` | Withdrawals individually below the velocity threshold but collectively significant |
217
+ | `session-hijack` | Mid-session device or IP changes simulating session takeover |
218
+ | `sim-swap-takeover` | Phone number ported, OTP redirected, login from new device, immediate withdrawal |
219
+
220
+ Each scenario has its own options and emits structured events you can subscribe to. See [`docs/ATTACK_PATTERNS.md`](docs/ATTACK_PATTERNS.md) for full details on each one.
221
+
222
+ List scenarios programmatically:
223
+
224
+ ```js
225
+ import { listScenarios } from 'fraud-sim';
226
+ const scenarios = await listScenarios();
227
+ // → [{ id: 'credential-stuffing', name: '...', description: '...', defaultOptions: {...} }, ...]
228
+ ```
229
+
230
+ ---
231
+
232
+ ## The CLI
233
+
234
+ For quick experiments without writing code:
235
+
236
+ ```bash
237
+ npx fraud-sim run credential-stuffing \
238
+ --target https://api.your-fraud-system.com \
239
+ --auth "Bearer $API_KEY" \
240
+ --user user_001 \
241
+ --attackers 50
242
+
243
+ # Run a full benchmark suite
244
+ npx fraud-sim suite owasp-baseline \
245
+ --target https://api.your-fraud-system.com \
246
+ --auth "Bearer $API_KEY" \
247
+ --output report.html
248
+
249
+ # Compare two targets
250
+ npx fraud-sim compare \
251
+ --target-a "https://api.system-a.com" \
252
+ --target-b "https://api.system-b.com" \
253
+ --suite owasp-baseline
254
+ ```
255
+
256
+ ---
257
+
258
+ ## The adaptive attacker — how it works
259
+
260
+ The `adaptive-attacker` scenario is `fraud-sim`'s headline feature, and the part that's worth understanding before using it.
261
+
262
+ In a normal scripted attack scenario, the attacker follows a predetermined sequence. The adaptive attacker instead observes how the target responds to each attempt and changes strategy.
263
+
264
+ ```
265
+ Round 1: Attacker tries a standard login (residential IP, common user agent)
266
+ Target responds: STEP_UP — reasons: ["NEW_DEVICE"]
267
+
268
+ Round 2: LLM reasons: "Device fingerprint was flagged — I should reuse the
269
+ same fingerprint instead of generating a new one."
270
+ Attacker tries again with consistent device.
271
+ Target responds: STEP_UP — reasons: ["IP_PROXY_VPN"]
272
+
273
+ Round 3: LLM reasons: "IP is flagged as a proxy. Switching to a residential
274
+ IP range."
275
+ Attacker tries with a residential-looking IP.
276
+ Target responds: BLOCK — reasons: ["LOGIN_VELOCITY_USER"]
277
+
278
+ Round 4: LLM reasons: "Velocity threshold hit. I'll wait and reduce my pace."
279
+ Attacker waits 60 seconds, tries again with cleaner signals.
280
+ Target responds: BLOCK — reasons: ["IP_MANY_USERS"]
281
+
282
+ ...
283
+ ```
284
+
285
+ **What the LLM sees:** the decision (`ALLOW`/`STEP_UP`/`BLOCK`), the reason codes (`NEW_DEVICE`, `IP_PROXY_VPN`, etc.), and the round number. Nothing else. It cannot see the target's rule weights, thresholds, or internal state.
286
+
287
+ **Why this matters:** real attackers operate this way. They probe, learn, and adapt. A fraud system that catches a one-shot attack but fails against an adaptive adversary is not actually well-defended. The adaptive attacker exposes the difference.
288
+
289
+ **Defensive systems that defeat the adaptive attacker do so because rules stack.** Defeating one signal exposes the next. A system with only one rule (say, IP reputation) can be evaded by changing IPs. A system with nine independent signals — velocity, device, geo, behaviour, payee, session continuity — cannot be evaded by changing any one of them.
290
+
291
+ The library ships with three LLM providers (OpenAI, Anthropic, and a deterministic fallback) and a clean interface for plugging in others. See [`docs/LLM_PROVIDERS.md`](docs/LLM_PROVIDERS.md).
292
+
293
+ ---
294
+
295
+ ## Benchmarking and reporting
296
+
297
+ Run a suite of scenarios and generate a shareable report:
298
+
299
+ ```js
300
+ import { runSuite, generateReport } from 'fraud-sim';
301
+ import fs from 'node:fs';
302
+
303
+ const result = await runSuite({
304
+ name: 'monthly-benchmark',
305
+ scenarios: [
306
+ { id: 'credential-stuffing', options: { attackerCount: 100 } },
307
+ { id: 'account-drain', options: { withdrawalCount: 10 } },
308
+ { id: 'card-testing', options: { cardCount: 50 } },
309
+ { id: 'adaptive-attacker', options: { maxRounds: 15 }, llm },
310
+ ],
311
+ target,
312
+ });
313
+
314
+ const html = await generateReport(result, { format: 'html' });
315
+ fs.writeFileSync('benchmark-report.html', html);
316
+ ```
317
+
318
+ The HTML report includes:
319
+
320
+ - Block rate per scenario with confidence intervals
321
+ - Latency p50, p95, p99 across all attempts
322
+ - Reason code frequency (what's catching the most attacks)
323
+ - Side-by-side comparison when two targets are tested
324
+ - A timeline view of the adaptive attacker's reasoning
325
+
326
+ For CI integration, generate JUnit XML and feed it to GitHub Actions, GitLab CI, or Jenkins:
327
+
328
+ ```js
329
+ const xml = await generateReport(result, { format: 'junit' });
330
+ fs.writeFileSync('fraud-sim-results.xml', xml);
331
+ ```
332
+
333
+ A worked GitHub Actions example lives in [`examples/ci-integration-github-actions/`](examples/ci-integration-github-actions/).
334
+
335
+ ---
336
+
337
+ ## Writing your own scenario
338
+
339
+ A scenario is a single file. Here's a minimal one:
340
+
341
+ ```js
342
+ // my-custom-scenario.js
343
+ import { faker } from '@faker-js/faker';
344
+
345
+ export default {
346
+ id: 'my-custom-attack',
347
+ name: 'Account enumeration',
348
+ description: 'Tries to enumerate valid user accounts by varying the email',
349
+
350
+ defaultOptions: {
351
+ attemptCount: 100,
352
+ delayMs: 100,
353
+ },
354
+
355
+ async run({ options, target, emit, signal }) {
356
+ const opts = { ...this.defaultOptions, ...options };
357
+ const results = { attempts: 0, blocked: 0, stepUp: 0, allowed: 0 };
358
+
359
+ for (let i = 0; i < opts.attemptCount && !signal.aborted; i++) {
360
+ const payload = {
361
+ user_id: faker.internet.email(), // varying the user, not the attacker
362
+ ip_address: '192.168.1.100', // same IP — this is the key signal
363
+ device_fingerprint: 'enum-bot-001',
364
+ timestamp: new Date().toISOString(),
365
+ metadata: { _simulated: true },
366
+ };
367
+
368
+ emit({ type: 'attempt', round: i, payload, timestamp: Date.now() });
369
+ const t0 = Date.now();
370
+ const response = await target.scoreLogin(payload);
371
+ emit({ type: 'response', round: i, response, latencyMs: Date.now() - t0, timestamp: Date.now() });
372
+
373
+ results.attempts++;
374
+ if (response.decision === 'BLOCK') results.blocked++;
375
+ if (response.decision === 'STEP_UP') results.stepUp++;
376
+ if (response.decision === 'ALLOW') results.allowed++;
377
+
378
+ await new Promise(r => setTimeout(r, opts.delayMs));
379
+ }
380
+
381
+ return { scenarioId: this.id, ...results, blockRate: results.blocked / results.attempts };
382
+ },
383
+ };
384
+ ```
385
+
386
+ Register it and run it:
387
+
388
+ ```js
389
+ import { run, registerScenario } from 'fraud-sim';
390
+ import myScenario from './my-custom-scenario.js';
391
+
392
+ registerScenario(myScenario);
393
+ await run('my-custom-attack', { options: { attemptCount: 200 }, target });
394
+ ```
395
+
396
+ Full guide: [`docs/EXTENDING.md`](docs/EXTENDING.md).
397
+
398
+ ---
399
+
400
+ ## Built-in adapters
401
+
402
+ ```js
403
+ import { httpAdapter, mockAdapter, loggingAdapter, recordingAdapter } from 'fraud-sim/adapters';
404
+ ```
405
+
406
+ - **`httpAdapter`** — sends events to a REST API. Configurable for any target's response shape via a `responseMap` function.
407
+ - **`mockAdapter`** — returns predetermined responses. Used for testing scenarios without a real target.
408
+ - **`loggingAdapter`** — logs payloads to console or file without sending anywhere. Used to inspect what a scenario produces before pointing at a real system.
409
+ - **`recordingAdapter`** — wraps another adapter and records all request/response pairs to disk. Useful for capturing real interactions to replay later in tests.
410
+
411
+ Write your own adapter to support non-HTTP targets (gRPC, message queues, internal function calls). See [`docs/TARGET_ADAPTERS.md`](docs/TARGET_ADAPTERS.md).
412
+
413
+ ---
414
+
415
+ ## Use it responsibly
416
+
417
+ This library generates traffic that looks like fraud, against systems that detect fraud. Use it only against systems you own or have explicit written permission to test.
418
+
419
+ - Do not point `fraud-sim` at any third-party API without authorisation. Doing so may violate computer-fraud laws in most jurisdictions.
420
+ - Do not use it as part of an actual attack on any system. The library is explicitly designed for testing your own defences and for vendors testing their own products.
421
+ - Do not use it to evade or attack real fintech production systems, including ones you have customer accounts with. Customer terms of service prohibit this.
422
+
423
+ Default rate limits in the library (modest attack counts, 200ms delays between actions) are deliberate. They are not aggressive enough to support real attacks at scale, and aggressive configurations require explicit setup. This is a feature, not a limitation.
424
+
425
+ If you find a way to use `fraud-sim` that the maintainers should consider out of scope, please open an issue.
426
+
427
+ ---
428
+
429
+ ## Roadmap
430
+
431
+ `fraud-sim` is built in phases. Each phase has a clear scope and a release.
432
+
433
+ | Version | Focus | Status |
434
+ |---|---|---|
435
+ | v0.1 | Core abstractions, 2 scenarios, HTTP adapter | 🚧 In development |
436
+ | v0.2 | LLM-driven adaptive attacker, SSE streaming | Planned |
437
+ | v0.3 | 7 additional scenarios, CLI runner | Planned |
438
+ | v0.4 | Test suites, reporting, benchmarking, CI integration | Planned |
439
+ | v1.0 | Community scenario library, governance, stable API | Planned |
440
+
441
+ Full phase plan in [`docs/ROADMAP.md`](docs/ROADMAP.md).
442
+
443
+ ---
444
+
445
+ ## Contributing
446
+
447
+ Pull requests are welcome — especially new scenarios that cover attack patterns the library doesn't yet have. Read [`CONTRIBUTING.md`](CONTRIBUTING.md) before opening a PR.
448
+
449
+ Scenarios from real production attack patterns are especially valuable, but only if they have been fully anonymised and contain no real customer data. Synthetic scenarios that model a known attack class are also welcome.
450
+
451
+ If you've found a bug, open an issue with a minimal reproduction. If you have a feature idea, open a discussion first — not every idea fits the library's scope, and we'd rather discuss it before you spend time on a PR.
452
+
453
+ ---
454
+
455
+ ## Maintainers
456
+
457
+ `fraud-sim` is maintained by [Arthur (@arthurr-beep)](https://github.com/arthurr-beep). It is independent open source under the MIT license.
458
+
459
+ Why we built this: real fraud detection systems need real adversarial testing, and the existing tooling for fraud teams was either ad-hoc scripts or vendor-locked. We wanted a tool that worked for everyone, including against our own product. That's the version we shipped.
460
+
461
+ If you find this library useful, the best thing you can do is contribute a scenario you wish existed. The second best thing is to tell us how it's working — open an issue, post on social, or just star the repo so we know there's interest.
462
+
463
+ ---
464
+
465
+ ## License
466
+
467
+ MIT — see [`LICENSE`](LICENSE) for the full text.
468
+
469
+ You may use this library commercially, modify it, and distribute it. You may not hold the maintainers liable for any consequences of its use. Use it against systems you own or have permission to test.
470
+
471
+ ---
472
+
473
+ ## Citation
474
+
475
+ If you use `fraud-sim` in academic research, please cite it:
476
+
477
+ ```bibtex
478
+ @software{fraud_sim,
479
+ title = {fraud-sim: A library for adversarial testing of fintech fraud detection systems},
480
+ author = {{FriskLayer maintainers and contributors}},
481
+ year = {2025},
482
+ url = {https://github.com/arthurr-beep/fraudsim},
483
+ license = {MIT}
484
+ }
485
+ ```
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Example: Run an account drain scenario against the local mock target.
3
+ *
4
+ * Prerequisite: Start the mock target first
5
+ * npm run example:mock-target
6
+ *
7
+ * Then in another terminal:
8
+ * npm run example:drain
9
+ */
10
+
11
+ import { run } from '../../src/index.js';
12
+ import { httpAdapter } from '../../src/adapters/http.js';
13
+
14
+ const target = httpAdapter({
15
+ baseUrl: process.env.TARGET_URL ?? 'http://localhost:4000',
16
+ endpoints: {
17
+ scoreLogin: '/score/login',
18
+ scoreWithdrawal: '/score/withdrawal',
19
+ },
20
+ });
21
+
22
+ console.log('\n=== Account drain simulation ===\n');
23
+
24
+ const report = await run('account-drain', {
25
+ target,
26
+ options: {
27
+ targetUserId: 'victim_user_002',
28
+ withdrawalCount: 5,
29
+ amountKobo: 4500000,
30
+ delayBetweenWithdrawalsMs: 500,
31
+ postLoginPauseMs: 1000,
32
+ },
33
+ onEvent: (e) => {
34
+ if (e.type === 'milestone') {
35
+ console.log(`\n ▶ ${e.message}`);
36
+ }
37
+ if (e.type === 'response') {
38
+ const reasons = (e.response.reasons ?? []).map((r) => r.code).join(', ') || '—';
39
+ console.log(
40
+ ` → ${e.response.decision.padEnd(7)} score ${e.response.riskScore?.toFixed(2) ?? '?'} ${reasons}`
41
+ );
42
+ }
43
+ },
44
+ });
45
+
46
+ console.log('\n=== Report ===\n');
47
+ console.log(` Login decision: ${report.loginDecision}`);
48
+ console.log(` Attempts total: ${report.attempts}`);
49
+ console.log(` Withdrawals tried: ${report.withdrawalDecisions.length}`);
50
+ console.log(` Attacker succeeded: ${report.attackerSucceeded ? 'YES (account drained!)' : 'no'}`);
51
+ console.log();