fraud-sim 0.5.0 → 0.5.1

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 CHANGED
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.1] — Cleaner customer-only output, end-to-end walkthrough — 2026-10-16
11
+
12
+ ### Added
13
+
14
+ - **End-to-end walkthrough** (`test/e2e/walkthrough.e2e.js`). Starts both
15
+ practice engines as real HTTP servers and drives the real CLI and library
16
+ against them in six acts: a contained botnet, a defeated adaptive attacker, a
17
+ smarter attacker finding the hardened engine's gap, customers getting
18
+ through, the full suite gated on block rate and false positives, and an
19
+ unreachable target exiting 3. `npm run demo` paces it for a screen recording
20
+ and opens the HTML report; `npm run test:e2e` runs it flat out and is part of
21
+ CI. Every act asserts what it shows.
22
+
23
+ ### Fixed
24
+
25
+ - The terminal report no longer prints an empty attack table and a
26
+ "TOTAL 0 events" line for a run that contained only legitimate traffic.
27
+
10
28
  ## [0.5.0] — False positives, markets, a harder target — 2026-10-16
11
29
 
12
30
  The first npm release since 0.2.0. Versions 0.3.0 and 0.4.0 below were tagged
package/README.md CHANGED
@@ -155,6 +155,24 @@ you have.
155
155
  Graduating to the real thing is a one-line change — swap the `baseUrl`. Full
156
156
  walk-through in **[docs/TESTING.md](docs/TESTING.md)**.
157
157
 
158
+ ### Watch the whole thing in 90 seconds
159
+
160
+ From a clone of the repository, `npm run demo` starts both practice engines
161
+ and walks through six acts in the terminal, paced to be followed:
162
+
163
+ 1. A credential-stuffing botnet is contained.
164
+ 2. An adaptive attacker that rotates IP, device and browser is defeated.
165
+ 3. A smarter attacker replays a device that was only challenged, and gets in.
166
+ 4. Ordinary customers get through the hardened engine.
167
+ 5. The full suite, gated like CI: an 85% block rate passes, and customers
168
+ blocked fail the run.
169
+ 6. An unreachable target exits 3 — fix the environment, not the rules.
170
+
171
+ It finishes by opening the HTML report. Each act asserts what it shows, and CI
172
+ runs the same walkthrough flat out (`npm run test:e2e`), so the demo cannot
173
+ drift from what the tool does. `DEMO_PACE=<ms>` sets the pause per beat;
174
+ `DEMO_LLM=anthropic` with `ANTHROPIC_API_KEY` puts a real model behind act 3.
175
+
158
176
  ---
159
177
 
160
178
  ## The command line
package/bin/fraud-sim.js CHANGED
@@ -24,7 +24,7 @@ import { run, runSuite, listScenarios, resolveLocale, localeIds } from '../src/i
24
24
  import { httpAdapter } from '../src/adapters/http.js';
25
25
  import { generateReport, summarise } from '../src/reporting/index.js';
26
26
 
27
- const VERSION = '0.5.0';
27
+ const VERSION = '0.5.1';
28
28
 
29
29
  const FORMAT_BY_EXT = {
30
30
  '.html': 'html',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraud-sim",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Realistic synthetic attack traffic for testing fintech fraud detection systems",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -46,6 +46,9 @@
46
46
  "test": "cd test/unit && node --test",
47
47
  "test:integration": "cd test/integration && node --test",
48
48
  "test:all": "cd test && node --test",
49
+ "test:e2e": "node --test test/e2e/walkthrough.e2e.js",
50
+ "demo": "node --test-reporter=spec test/e2e/walkthrough.e2e.js",
51
+ "demo:npm": "node --test-reporter=spec test/e2e/walkthrough.e2e.js --from-npm",
49
52
  "lint": "eslint .",
50
53
  "lint:fix": "eslint . --fix",
51
54
  "example:basic": "node examples/basic-credential-stuffing/index.js",
@@ -119,28 +119,33 @@ function renderText(result, options = {}) {
119
119
  const width = Math.max(20, ...all.map((r) => String(r.scenarioId).length));
120
120
 
121
121
  const lines = [];
122
- lines.push('');
123
- lines.push(
124
- ` ${pad('SCENARIO', width)} ${rpad('EVENTS', 6)} ${rpad('BLOCK', 6)} ${rpad('CHALL', 6)} ${rpad('ALLOW', 6)} ${rpad('RATE', 7)} OUTCOME`
125
- );
126
- lines.push(` ${'-'.repeat(width + 48)}`);
127
122
 
128
- for (const r of reports) {
129
- const o = outcomeFor(r);
123
+ // A run of customer traffic alone has no attack half; an empty table and a
124
+ // "TOTAL 0 events" line would read as a run that measured nothing.
125
+ if (reports.length) {
126
+ lines.push('');
130
127
  lines.push(
131
- ` ${pad(r.scenarioId, width)} ${rpad(r.attempts, 6)} ${rpad(r.blocked, 6)} ` +
132
- `${rpad(r.stepUp, 6)} ${rpad(r.allowed, 6)} ${rpad((r.blockRate * 100).toFixed(1) + '%', 7)} ${o.label}`
128
+ ` ${pad('SCENARIO', width)} ${rpad('EVENTS', 6)} ${rpad('BLOCK', 6)} ${rpad('CHALL', 6)} ${rpad('ALLOW', 6)} ${rpad('RATE', 7)} OUTCOME`
133
129
  );
134
- }
130
+ lines.push(` ${'-'.repeat(width + 48)}`);
135
131
 
136
- lines.push('');
137
- lines.push(
138
- ` TOTAL ${totals.attempts} events · ${totals.scoredAttempts} scored · ` +
139
- `${(totals.blockRate * 100).toFixed(1)}% blocked · ${totals.errors} errors`
140
- );
132
+ for (const r of reports) {
133
+ const o = outcomeFor(r);
134
+ lines.push(
135
+ ` ${pad(r.scenarioId, width)} ${rpad(r.attempts, 6)} ${rpad(r.blocked, 6)} ` +
136
+ `${rpad(r.stepUp, 6)} ${rpad(r.allowed, 6)} ${rpad((r.blockRate * 100).toFixed(1) + '%', 7)} ${o.label}`
137
+ );
138
+ }
141
139
 
142
- if (totals.estimatedLossMinor > 0) {
143
- lines.push(` LOSS ${formatMoney(totals.estimatedLossMinor, options)} extracted`);
140
+ lines.push('');
141
+ lines.push(
142
+ ` TOTAL ${totals.attempts} events · ${totals.scoredAttempts} scored · ` +
143
+ `${(totals.blockRate * 100).toFixed(1)}% blocked · ${totals.errors} errors`
144
+ );
145
+
146
+ if (totals.estimatedLossMinor > 0) {
147
+ lines.push(` LOSS ${formatMoney(totals.estimatedLossMinor, options)} extracted`);
148
+ }
144
149
  }
145
150
 
146
151
  // The other half of the measurement, in its own table: here a block is the