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 +18 -0
- package/README.md +18 -0
- package/bin/fraud-sim.js +1 -1
- package/package.json +4 -1
- package/src/reporting/index.js +22 -17
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.
|
|
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.
|
|
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",
|
package/src/reporting/index.js
CHANGED
|
@@ -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
|
-
|
|
129
|
-
|
|
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(
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
143
|
-
lines.push(
|
|
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
|