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,39 @@
1
+ /**
2
+ * Simulated clock.
3
+ *
4
+ * Some attacks are defined by their *timing* rather than their volume. A slow
5
+ * drain spreads withdrawals over days; a velocity-evading attacker paces
6
+ * attempts just under a rolling limit. Reproducing those in wall-clock time
7
+ * would make a single run take days, so instead we advance the `timestamp`
8
+ * field the target receives while sleeping only briefly — or not at all.
9
+ *
10
+ * This is honest as long as the target derives its windows from the event
11
+ * timestamp. A system that stamps arrival time server-side will see the whole
12
+ * run compressed into seconds and will (correctly) treat it as a burst. When
13
+ * that happens the scenario is measuring the wrong thing, so scenarios using a
14
+ * simulated clock say so in their report via `simulatedTime: true` and emit a
15
+ * milestone explaining it.
16
+ *
17
+ * @example
18
+ * const clock = simulatedClock({ stepMs: 90 * 60 * 1000 }); // 90 minutes
19
+ * payload.timestamp = clock.iso();
20
+ * clock.advance();
21
+ */
22
+ /**
23
+ * @param {object} [config]
24
+ * @param {number} [config.start] - Epoch ms to start from. Defaults to now.
25
+ * @param {number} [config.stepMs] - Default advance applied by `advance()`.
26
+ * @returns {{iso: () => string, epoch: () => number, advance: (ms?: number) => void,
27
+ * elapsedMs: () => number}}
28
+ */
29
+ export function simulatedClock(config?: {
30
+ start?: number;
31
+ stepMs?: number;
32
+ }): {
33
+ iso: () => string;
34
+ epoch: () => number;
35
+ advance: (ms?: number) => void;
36
+ elapsedMs: () => number;
37
+ };
38
+ /** Format a millisecond span as a short human string, for milestone messages. */
39
+ export function humaniseMs(ms: any): string;
@@ -0,0 +1,114 @@
1
+ export function newSessionId(prefix?: string): string;
2
+ export function newDeviceFingerprint(): string;
3
+ export function newReference(): string;
4
+ export function newPayee(): {
5
+ payee_account: any;
6
+ payee_bank: any;
7
+ payee_name: string;
8
+ };
9
+ export function buildLoginPayload(userId: any, overrides?: {}): {
10
+ user_id: any;
11
+ session_id: string;
12
+ ip_address: string;
13
+ user_agent: any;
14
+ device_fingerprint: string;
15
+ timestamp: string;
16
+ metadata: {
17
+ login_method: string;
18
+ platform: any;
19
+ _simulated: boolean;
20
+ };
21
+ };
22
+ export function buildWithdrawalPayload(userId: any, transaction?: {}, overrides?: {}): {
23
+ user_id: any;
24
+ session_id: string;
25
+ ip_address: string;
26
+ device_fingerprint: string;
27
+ timestamp: string;
28
+ transaction: {
29
+ is_new_payee: boolean;
30
+ reference: string;
31
+ payee_account: any;
32
+ payee_bank: any;
33
+ payee_name: string;
34
+ amount: any;
35
+ currency: any;
36
+ };
37
+ metadata: {
38
+ _simulated: boolean;
39
+ };
40
+ };
41
+ /**
42
+ * Generic non-login, non-withdrawal event, sent to the target's `scoreAction`
43
+ * endpoint. Signups, promo claims, card authorisations and profile changes all
44
+ * ride on this shape so a target only has to map one extra endpoint.
45
+ */
46
+ export function buildActionPayload(userId: any, actionType: any, details?: {}, overrides?: {}): {
47
+ details: any;
48
+ user_id: any;
49
+ session_id: string;
50
+ action_type: any;
51
+ ip_address: string;
52
+ user_agent: any;
53
+ device_fingerprint: string;
54
+ timestamp: string;
55
+ metadata: {
56
+ _simulated: boolean;
57
+ };
58
+ };
59
+ /** A new-account signup, as promo abuse would generate it. */
60
+ export function buildSignupPayload(userId: any, overrides?: {}): {
61
+ details: any;
62
+ user_id: any;
63
+ session_id: string;
64
+ action_type: any;
65
+ ip_address: string;
66
+ user_agent: any;
67
+ device_fingerprint: string;
68
+ timestamp: string;
69
+ metadata: {
70
+ _simulated: boolean;
71
+ };
72
+ };
73
+ /**
74
+ * A card authorisation attempt.
75
+ *
76
+ * Carries a token, BIN and last four — never a full card number. See
77
+ * `randomCardBin` for why.
78
+ */
79
+ export function buildCardAuthPayload(userId: any, card?: {}, amountMinor?: number, overrides?: {}): {
80
+ details: any;
81
+ user_id: any;
82
+ session_id: string;
83
+ action_type: any;
84
+ ip_address: string;
85
+ user_agent: any;
86
+ device_fingerprint: string;
87
+ timestamp: string;
88
+ metadata: {
89
+ _simulated: boolean;
90
+ };
91
+ };
92
+ /**
93
+ * An account-to-account transfer. Structurally a withdrawal, but names the
94
+ * destination account explicitly so mule-network runs can be traced.
95
+ */
96
+ export function buildTransferPayload(userId: any, destinationAccount: any, amountMinor: any, overrides?: {}): {
97
+ user_id: any;
98
+ session_id: string;
99
+ ip_address: string;
100
+ device_fingerprint: string;
101
+ timestamp: string;
102
+ transaction: {
103
+ is_new_payee: boolean;
104
+ reference: string;
105
+ payee_account: any;
106
+ payee_bank: any;
107
+ payee_name: string;
108
+ amount: any;
109
+ currency: any;
110
+ };
111
+ metadata: {
112
+ _simulated: boolean;
113
+ };
114
+ };
@@ -0,0 +1,8 @@
1
+ export { sleep } from "./sleep.js";
2
+ export { DeterministicRng } from "./rng.js";
3
+ export { simulatedClock, humaniseMs } from "./clock.js";
4
+ export { newResults, countDecision, scoreAndCount } from "./results.js";
5
+ export { buildLoginPayload, buildWithdrawalPayload, buildActionPayload, buildSignupPayload, buildCardAuthPayload, buildTransferPayload, newSessionId, newDeviceFingerprint, newReference, newPayee } from "./faker-helpers.js";
6
+ export { randomFloat, randomInt, pickOne, alphanumeric, randomIp, ipInSameSubnet, randomUserAgent, randomAccountNumber, randomBank, randomFullName, randomPhoneNumber, randomCardBin, cardBins, randomLast4, newCardToken, DEMO_IP_POOL, demoIpPool, pickDemoIp } from "./random.js";
7
+ export { withSeed, isSeeded, withLocale, currentLocale } from "./random-source.js";
8
+ export { LOCALES, localeIds, resolveLocale, DEFAULT_LOCALE } from "../locales/index.js";
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The generator in scope right now — a seeded one inside `withSeed`, the
3
+ * system one otherwise.
4
+ *
5
+ * Consumers only ever need `next()`, so any object with that method works.
6
+ * Keeping the interface to a single function means a seeded run and an
7
+ * unseeded one draw from identical distributions.
8
+ *
9
+ * @returns {{ next: () => number }}
10
+ */
11
+ export function currentSource(): {
12
+ next: () => number;
13
+ };
14
+ /** A float in [0, 1) from the current source. */
15
+ export function randomFloat(): number;
16
+ /**
17
+ * Run `fn` with a seeded generator in scope. Everything it does — including
18
+ * across `await` boundaries — draws from that generator.
19
+ *
20
+ * @param {number} seed - Any integer. The same seed replays the same sequence.
21
+ * @param {() => T} fn
22
+ * @returns {T}
23
+ * @template T
24
+ */
25
+ export function withSeed<T>(seed: number, fn: () => T): T;
26
+ /** True when a seeded generator is currently in scope. */
27
+ export function isSeeded(): boolean;
28
+ /** The locale pack in scope, or the default when a run did not name one. */
29
+ export function currentLocale(): any;
30
+ /**
31
+ * Run `fn` with a market in scope. Every generator inside it — account numbers,
32
+ * banks, names, phone numbers, payload currency — draws from that pack.
33
+ *
34
+ * @param {string|object} locale - A bundled locale id, or a locale object.
35
+ * @param {() => T} fn
36
+ * @returns {T}
37
+ * @template T
38
+ */
39
+ export function withLocale<T>(locale: string | object, fn: () => T): T;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Random integer in [min, max] inclusive.
3
+ */
4
+ export function randomInt(min: any, max: any): any;
5
+ /**
6
+ * Pick one element from an array.
7
+ */
8
+ export function pickOne(arr: any): any;
9
+ /**
10
+ * Generate a random alphanumeric string of the given length.
11
+ */
12
+ export function alphanumeric(length: any): string;
13
+ /**
14
+ * Generate a synthetic but *publicly routable* IPv4 address.
15
+ *
16
+ * Deliberately excludes private, loopback, CGNAT, link-local, multicast and
17
+ * documentation ranges. Addresses from those blocks cannot be resolved by any
18
+ * geo/reputation provider, so a target that scores them will fall back to
19
+ * neutral intel and never fire its proxy, abuse-score, or geo rules — making a
20
+ * defence look weaker than it is.
21
+ *
22
+ * These addresses are used only as payload field values. `fraud-sim` never
23
+ * connects to them.
24
+ *
25
+ * For a stable, pre-seedable set with known geolocation, use
26
+ * {@link DEMO_IP_POOL} / {@link pickDemoIp} instead.
27
+ */
28
+ export function randomIp(): string;
29
+ /**
30
+ * The attacker IP pool for the run's market: domestic entries from the active
31
+ * locale, plus the fixed set of hosting, proxy and foreign exits.
32
+ *
33
+ * Exposed as a getter so it tracks the locale in scope without callers having
34
+ * to pass one. `demoIpPool()` is the explicit form.
35
+ */
36
+ export function demoIpPool(locale: any): readonly any[];
37
+ /**
38
+ * Pick an entry from {@link DEMO_IP_POOL}, optionally filtered by persona.
39
+ *
40
+ * @param {'any'|'domestic'|'hosting'|'proxy'|'foreign'} [persona='any']
41
+ * @returns {(typeof DEMO_IP_POOL)[number]}
42
+ */
43
+ export function pickDemoIp(persona?: "any" | "domestic" | "hosting" | "proxy" | "foreign"): (typeof DEMO_IP_POOL)[number];
44
+ /** Pick one of the published test BINs. */
45
+ export function randomCardBin(): any;
46
+ /** All test BINs, for scenarios that need to walk a BIN range deliberately. */
47
+ export function cardBins(): string[];
48
+ /** Four synthetic trailing digits. Not derived from any real card. */
49
+ export function randomLast4(): string;
50
+ /** An opaque card token, the way a processor would hand one to a risk API. */
51
+ export function newCardToken(): string;
52
+ /**
53
+ * Generate an IP inside the same /24 as `ip`.
54
+ *
55
+ * Promo-abuse and other single-actor attacks look like many users, but the
56
+ * addresses cluster. Subnet clustering is usually the signal that catches them.
57
+ */
58
+ export function ipInSameSubnet(ip: any): string;
59
+ /** A subscriber number in the active market's format. */
60
+ export function randomPhoneNumber(): any;
61
+ /**
62
+ * Pick a realistic user agent string.
63
+ */
64
+ export function randomUserAgent(): any;
65
+ /**
66
+ * A real browser's user agent — never a tool. For legitimate traffic, where a
67
+ * `curl` agent would be a false positive the simulator manufactured.
68
+ */
69
+ export function randomBrowserUserAgent(): any;
70
+ /**
71
+ * An account identifier shaped like the active market's — 10 digits in Nigeria,
72
+ * 8 in the UK, a variable length in the US. Format matters: a target that
73
+ * validates account numbers rejects a wrongly-shaped one before it ever scores
74
+ * it, and a run full of validation errors measures nothing.
75
+ */
76
+ export function randomAccountNumber(): any;
77
+ /** A bank from the active market. */
78
+ export function randomBank(): any;
79
+ /** A plausible name for the active market. Synthetic; nobody real. */
80
+ export function randomFullName(): string;
81
+ export { randomFloat } from "./random-source.js";
82
+ /**
83
+ * The pool for the market currently in scope.
84
+ *
85
+ * Retained as a named export because callers pre-date `demoIpPool()`; it is a
86
+ * snapshot of the default market rather than a live view, so a target
87
+ * pre-seeding intel for a non-default locale should call `demoIpPool()`.
88
+ */
89
+ export const DEMO_IP_POOL: readonly any[];
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Shared decision tallying.
3
+ *
4
+ * Every scenario counts the same four outcomes and emits the same
5
+ * attempt/response event pair around each call to the target. Centralising it
6
+ * keeps the counts consistent and means a scenario file contains only the
7
+ * attack logic.
8
+ */
9
+ /** A fresh tally. */
10
+ export function newResults(extra?: {}): {
11
+ attempts: number;
12
+ blocked: number;
13
+ stepUp: number;
14
+ allowed: number;
15
+ errors: number;
16
+ };
17
+ /**
18
+ * Record one decision against a tally. Anything that isn't a recognised
19
+ * decision counts as an error rather than being silently dropped.
20
+ */
21
+ export function countDecision(results: any, decision: any): any;
22
+ /**
23
+ * Call one of the target's scoring methods, emitting the attempt/response
24
+ * events and updating the tally.
25
+ *
26
+ * Returns the target's response, or `null` if the call threw — callers decide
27
+ * whether a failed call ends the scenario or is skipped. A throw is counted as
28
+ * an error but does NOT increment `attempts`, because nothing was scored.
29
+ *
30
+ * @param {object} args
31
+ * @param {object} args.ctx - The scenario context ({ target, emit }).
32
+ * @param {string} args.method - 'scoreLogin' | 'scoreWithdrawal' | 'scoreAction'
33
+ * @param {object} args.payload - The payload to send.
34
+ * @param {object} args.results - Tally from `newResults()`.
35
+ * @param {string} [args.stage] - Optional label carried on both events.
36
+ * @param {object} [args.ipIntel] - Declared intel for the payload's IP, if known.
37
+ * @returns {Promise<object|null>}
38
+ */
39
+ export function scoreAndCount({ ctx, method, payload, results, stage, ipIntel }: {
40
+ ctx: object;
41
+ method: string;
42
+ payload: object;
43
+ results: object;
44
+ stage?: string;
45
+ ipIntel?: object;
46
+ }): Promise<object | null>;
@@ -0,0 +1,36 @@
1
+ export class DeterministicRng {
2
+ /**
3
+ * @param {number} [seed] - Any integer. The same seed always replays the same sequence.
4
+ */
5
+ constructor(seed?: number);
6
+ state: number;
7
+ /**
8
+ * Next float in [0, 1).
9
+ * @returns {number}
10
+ */
11
+ next(): number;
12
+ /**
13
+ * Integer in [min, max] inclusive.
14
+ * @param {number} min
15
+ * @param {number} max
16
+ * @returns {number}
17
+ */
18
+ int(min: number, max: number): number;
19
+ /**
20
+ * Pick one element from an array.
21
+ * @param {Array} arr
22
+ * @returns {*}
23
+ */
24
+ pick(arr: any[]): any;
25
+ /**
26
+ * Random alphanumeric string of the given length.
27
+ * @param {number} length
28
+ * @returns {string}
29
+ */
30
+ alphanumeric(length: number): string;
31
+ /**
32
+ * Synthetic IPv4 address.
33
+ * @returns {string}
34
+ */
35
+ ip(): string;
36
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Sleep utility — abort-aware.
3
+ *
4
+ * Used by scenarios to space out attack attempts. Respects AbortSignal
5
+ * so scenarios cancel promptly when the user aborts.
6
+ */
7
+ export function sleep(ms: any, signal: any): Promise<any>;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Wrap a target adapter so its outbound calls never exceed `maxPerSecond`.
3
+ *
4
+ * Passing `Infinity` (or a non-finite value) returns the target untouched —
5
+ * useful in tests, and for users who have their own pacing and mean it.
6
+ *
7
+ * @param {object} target - Any TargetAdapter.
8
+ * @param {number} maxPerSecond
9
+ * @param {{ onThrottle?: (waitMs: number) => void, signal?: AbortSignal }} [hooks]
10
+ * @returns {object} A adapter with the same shape as `target`.
11
+ */
12
+ export function throttleTarget(target: object, maxPerSecond: number, hooks?: {
13
+ onThrottle?: (waitMs: number) => void;
14
+ signal?: AbortSignal;
15
+ }): object;
16
+ /**
17
+ * Wrap a target with an already-constructed limiter.
18
+ *
19
+ * Separate from `throttleTarget` because several concurrent runs need to share
20
+ * a single limiter instance: one limiter per run would let the effective rate
21
+ * scale with the concurrency, which is the opposite of what a ceiling is for.
22
+ *
23
+ * @param {object} target
24
+ * @param {() => Promise<void>} limiter - From `rateLimiter()`.
25
+ * @param {(waitMs: number) => void} [onThrottle] - Per-run accounting.
26
+ * @returns {object}
27
+ */
28
+ export function applyLimiter(target: object, limiter: () => Promise<void>, onThrottle?: (waitMs: number) => void): object;
29
+ /**
30
+ * A sliding-window limiter. Resolves immediately while under the limit;
31
+ * otherwise waits until the oldest request in the window ages out.
32
+ */
33
+ export function rateLimiter(maxPerSecond: any, hooks?: {}): () => Promise<void>;