openpitstop 1.0.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/LICENSE +21 -0
- package/README.md +512 -0
- package/demo-repo/README.md +24 -0
- package/demo-repo/package-lock.json +4483 -0
- package/demo-repo/package.json +17 -0
- package/demo-repo/src/config.js +5 -0
- package/demo-repo/src/db.js +31 -0
- package/demo-repo/src/server.js +13 -0
- package/demo-repo/src/userController.js +22 -0
- package/demo-repo/src/userRepo.js +18 -0
- package/demo-repo/src/userService.js +59 -0
- package/demo-repo/test/user.test.js +12 -0
- package/demo-repo-fintech/README.md +27 -0
- package/demo-repo-fintech/package-lock.json +1023 -0
- package/demo-repo-fintech/package.json +14 -0
- package/demo-repo-fintech/pitstop-repro-ledger-order-ord-conc-1-charged-2.test.mjs +175 -0
- package/demo-repo-fintech/pitstop-repro-ledger-order-ord-retry-2-charged-2.test.mjs +175 -0
- package/demo-repo-fintech/pitstop-repro-ledger-order-ord-webhook-3-charged-2.test.mjs +175 -0
- package/demo-repo-fintech/src/gateway.js +26 -0
- package/demo-repo-fintech/src/server.js +61 -0
- package/demo-repo-generators/package-lock.json +1508 -0
- package/demo-repo-generators/package.json +17 -0
- package/demo-repo-generators/public/index.html +10 -0
- package/demo-repo-generators/scripts/build.js +17 -0
- package/demo-repo-generators/src/counter.js +16 -0
- package/demo-repo-integrity/README.md +49 -0
- package/demo-repo-integrity/package.json +12 -0
- package/demo-repo-integrity/src/money.js +19 -0
- package/demo-repo-integrity/test/money.test.js +17 -0
- package/dist/analyzers/accessibility.d.ts +18 -0
- package/dist/analyzers/accessibility.js +248 -0
- package/dist/analyzers/accessibility.js.map +1 -0
- package/dist/analyzers/dependencyGraph.d.ts +14 -0
- package/dist/analyzers/dependencyGraph.js +206 -0
- package/dist/analyzers/dependencyGraph.js.map +1 -0
- package/dist/analyzers/devex.d.ts +19 -0
- package/dist/analyzers/devex.js +301 -0
- package/dist/analyzers/devex.js.map +1 -0
- package/dist/analyzers/duplication.d.ts +2 -0
- package/dist/analyzers/duplication.js +47 -0
- package/dist/analyzers/duplication.js.map +1 -0
- package/dist/analyzers/index.d.ts +12 -0
- package/dist/analyzers/index.js +38 -0
- package/dist/analyzers/index.js.map +1 -0
- package/dist/analyzers/integrity/assertionLiteralTamper.d.ts +2 -0
- package/dist/analyzers/integrity/assertionLiteralTamper.js +164 -0
- package/dist/analyzers/integrity/assertionLiteralTamper.js.map +1 -0
- package/dist/analyzers/integrity/exceptionSwallow.d.ts +2 -0
- package/dist/analyzers/integrity/exceptionSwallow.js +198 -0
- package/dist/analyzers/integrity/exceptionSwallow.js.map +1 -0
- package/dist/analyzers/integrity/exitCheat.d.ts +2 -0
- package/dist/analyzers/integrity/exitCheat.js +31 -0
- package/dist/analyzers/integrity/exitCheat.js.map +1 -0
- package/dist/analyzers/integrity/git.d.ts +10 -0
- package/dist/analyzers/integrity/git.js +181 -0
- package/dist/analyzers/integrity/git.js.map +1 -0
- package/dist/analyzers/integrity/hardcodedMatch.d.ts +2 -0
- package/dist/analyzers/integrity/hardcodedMatch.js +93 -0
- package/dist/analyzers/integrity/hardcodedMatch.js.map +1 -0
- package/dist/analyzers/integrity/helpers.d.ts +21 -0
- package/dist/analyzers/integrity/helpers.js +66 -0
- package/dist/analyzers/integrity/helpers.js.map +1 -0
- package/dist/analyzers/integrity/index.d.ts +10 -0
- package/dist/analyzers/integrity/index.js +27 -0
- package/dist/analyzers/integrity/index.js.map +1 -0
- package/dist/analyzers/integrity/mockOverreach.d.ts +2 -0
- package/dist/analyzers/integrity/mockOverreach.js +72 -0
- package/dist/analyzers/integrity/mockOverreach.js.map +1 -0
- package/dist/analyzers/integrity/suppressionCreep.d.ts +2 -0
- package/dist/analyzers/integrity/suppressionCreep.js +27 -0
- package/dist/analyzers/integrity/suppressionCreep.js.map +1 -0
- package/dist/analyzers/integrity/testTamper.d.ts +2 -0
- package/dist/analyzers/integrity/testTamper.js +153 -0
- package/dist/analyzers/integrity/testTamper.js.map +1 -0
- package/dist/analyzers/integrity/types.d.ts +63 -0
- package/dist/analyzers/integrity/types.js +13 -0
- package/dist/analyzers/integrity/types.js.map +1 -0
- package/dist/analyzers/ledger/attacks.d.ts +17 -0
- package/dist/analyzers/ledger/attacks.js +135 -0
- package/dist/analyzers/ledger/attacks.js.map +1 -0
- package/dist/analyzers/ledger/discover.d.ts +15 -0
- package/dist/analyzers/ledger/discover.js +122 -0
- package/dist/analyzers/ledger/discover.js.map +1 -0
- package/dist/analyzers/ledger/evidence.d.ts +22 -0
- package/dist/analyzers/ledger/evidence.js +127 -0
- package/dist/analyzers/ledger/evidence.js.map +1 -0
- package/dist/analyzers/ledger/harness.d.ts +4 -0
- package/dist/analyzers/ledger/harness.js +270 -0
- package/dist/analyzers/ledger/harness.js.map +1 -0
- package/dist/analyzers/ledger/index.d.ts +14 -0
- package/dist/analyzers/ledger/index.js +116 -0
- package/dist/analyzers/ledger/index.js.map +1 -0
- package/dist/analyzers/ledger/types.d.ts +31 -0
- package/dist/analyzers/ledger/types.js +2 -0
- package/dist/analyzers/ledger/types.js.map +1 -0
- package/dist/analyzers/perf.d.ts +2 -0
- package/dist/analyzers/perf.js +106 -0
- package/dist/analyzers/perf.js.map +1 -0
- package/dist/analyzers/reliability.d.ts +22 -0
- package/dist/analyzers/reliability.js +339 -0
- package/dist/analyzers/reliability.js.map +1 -0
- package/dist/analyzers/routes.d.ts +17 -0
- package/dist/analyzers/routes.js +115 -0
- package/dist/analyzers/routes.js.map +1 -0
- package/dist/analyzers/security.d.ts +2 -0
- package/dist/analyzers/security.js +302 -0
- package/dist/analyzers/security.js.map +1 -0
- package/dist/analyzers/suiteRunner.d.ts +64 -0
- package/dist/analyzers/suiteRunner.js +381 -0
- package/dist/analyzers/suiteRunner.js.map +1 -0
- package/dist/analyzers/tests.d.ts +2 -0
- package/dist/analyzers/tests.js +215 -0
- package/dist/analyzers/tests.js.map +1 -0
- package/dist/analyzers/types.d.ts +203 -0
- package/dist/analyzers/types.js +2 -0
- package/dist/analyzers/types.js.map +1 -0
- package/dist/analyzers/util.d.ts +35 -0
- package/dist/analyzers/util.js +209 -0
- package/dist/analyzers/util.js.map +1 -0
- package/dist/ciCheckA.d.ts +2 -0
- package/dist/ciCheckA.js +5 -0
- package/dist/ciCheckA.js.map +1 -0
- package/dist/ciCheckB.d.ts +2 -0
- package/dist/ciCheckB.js +6 -0
- package/dist/ciCheckB.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +65 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/budget.d.ts +33 -0
- package/dist/commands/budget.js +108 -0
- package/dist/commands/budget.js.map +1 -0
- package/dist/commands/ci.d.ts +14 -0
- package/dist/commands/ci.js +157 -0
- package/dist/commands/ci.js.map +1 -0
- package/dist/commands/demo.d.ts +2 -0
- package/dist/commands/demo.js +163 -0
- package/dist/commands/demo.js.map +1 -0
- package/dist/commands/digest.d.ts +8 -0
- package/dist/commands/digest.js +189 -0
- package/dist/commands/digest.js.map +1 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +98 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/drive.d.ts +3 -0
- package/dist/commands/drive.js +170 -0
- package/dist/commands/drive.js.map +1 -0
- package/dist/commands/gate.d.ts +2 -0
- package/dist/commands/gate.js +136 -0
- package/dist/commands/gate.js.map +1 -0
- package/dist/commands/honesty.d.ts +2 -0
- package/dist/commands/honesty.js +182 -0
- package/dist/commands/honesty.js.map +1 -0
- package/dist/commands/inspect.d.ts +2 -0
- package/dist/commands/inspect.js +244 -0
- package/dist/commands/inspect.js.map +1 -0
- package/dist/commands/install.d.ts +2 -0
- package/dist/commands/install.js +81 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/integrity.d.ts +2 -0
- package/dist/commands/integrity.js +74 -0
- package/dist/commands/integrity.js.map +1 -0
- package/dist/commands/memory.d.ts +6 -0
- package/dist/commands/memory.js +90 -0
- package/dist/commands/memory.js.map +1 -0
- package/dist/commands/pen.d.ts +17 -0
- package/dist/commands/pen.js +158 -0
- package/dist/commands/pen.js.map +1 -0
- package/dist/commands/prompt.d.ts +2 -0
- package/dist/commands/prompt.js +37 -0
- package/dist/commands/prompt.js.map +1 -0
- package/dist/commands/readyCheck.d.ts +21 -0
- package/dist/commands/readyCheck.js +68 -0
- package/dist/commands/readyCheck.js.map +1 -0
- package/dist/commands/report.d.ts +2 -0
- package/dist/commands/report.js +48 -0
- package/dist/commands/report.js.map +1 -0
- package/dist/commands/repro.d.ts +3 -0
- package/dist/commands/repro.js +61 -0
- package/dist/commands/repro.js.map +1 -0
- package/dist/commands/scan.d.ts +27 -0
- package/dist/commands/scan.js +348 -0
- package/dist/commands/scan.js.map +1 -0
- package/dist/commands/share.d.ts +2 -0
- package/dist/commands/share.js +163 -0
- package/dist/commands/share.js.map +1 -0
- package/dist/commands/trends.d.ts +2 -0
- package/dist/commands/trends.js +103 -0
- package/dist/commands/trends.js.map +1 -0
- package/dist/commands/try.d.ts +12 -0
- package/dist/commands/try.js +208 -0
- package/dist/commands/try.js.map +1 -0
- package/dist/commands/verify.d.ts +44 -0
- package/dist/commands/verify.js +381 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/commands/watch.d.ts +10 -0
- package/dist/commands/watch.js +70 -0
- package/dist/commands/watch.js.map +1 -0
- package/dist/evidence.d.ts +37 -0
- package/dist/evidence.js +61 -0
- package/dist/evidence.js.map +1 -0
- package/dist/graph/correlate.d.ts +7 -0
- package/dist/graph/correlate.js +246 -0
- package/dist/graph/correlate.js.map +1 -0
- package/dist/graph/integrity.d.ts +22 -0
- package/dist/graph/integrity.js +39 -0
- package/dist/graph/integrity.js.map +1 -0
- package/dist/installer/targets.d.ts +23 -0
- package/dist/installer/targets.js +162 -0
- package/dist/installer/targets.js.map +1 -0
- package/dist/memory/store.d.ts +33 -0
- package/dist/memory/store.js +131 -0
- package/dist/memory/store.js.map +1 -0
- package/dist/pen/dynamic.d.ts +36 -0
- package/dist/pen/dynamic.js +622 -0
- package/dist/pen/dynamic.js.map +1 -0
- package/dist/pen/fix.d.ts +35 -0
- package/dist/pen/fix.js +239 -0
- package/dist/pen/fix.js.map +1 -0
- package/dist/pen/proof.d.ts +46 -0
- package/dist/pen/proof.js +112 -0
- package/dist/pen/proof.js.map +1 -0
- package/dist/pen/report.d.ts +11 -0
- package/dist/pen/report.js +258 -0
- package/dist/pen/report.js.map +1 -0
- package/dist/pen/static.d.ts +17 -0
- package/dist/pen/static.js +565 -0
- package/dist/pen/static.js.map +1 -0
- package/dist/pen/store.d.ts +11 -0
- package/dist/pen/store.js +36 -0
- package/dist/pen/store.js.map +1 -0
- package/dist/pen/types.d.ts +112 -0
- package/dist/pen/types.js +53 -0
- package/dist/pen/types.js.map +1 -0
- package/dist/report/format.d.ts +126 -0
- package/dist/report/format.js +706 -0
- package/dist/report/format.js.map +1 -0
- package/dist/report/score.d.ts +47 -0
- package/dist/report/score.js +164 -0
- package/dist/report/score.js.map +1 -0
- package/dist/repro/framework.d.ts +26 -0
- package/dist/repro/framework.js +224 -0
- package/dist/repro/framework.js.map +1 -0
- package/dist/repro/generate.d.ts +14 -0
- package/dist/repro/generate.js +669 -0
- package/dist/repro/generate.js.map +1 -0
- package/dist/repro/ids.d.ts +32 -0
- package/dist/repro/ids.js +175 -0
- package/dist/repro/ids.js.map +1 -0
- package/dist/repro/index.d.ts +24 -0
- package/dist/repro/index.js +69 -0
- package/dist/repro/index.js.map +1 -0
- package/dist/repro/pen.d.ts +26 -0
- package/dist/repro/pen.js +321 -0
- package/dist/repro/pen.js.map +1 -0
- package/dist/repro/run.d.ts +18 -0
- package/dist/repro/run.js +36 -0
- package/dist/repro/run.js.map +1 -0
- package/dist/sandbox/proxy.d.ts +41 -0
- package/dist/sandbox/proxy.js +231 -0
- package/dist/sandbox/proxy.js.map +1 -0
- package/dist/sandbox/startCmd.d.ts +24 -0
- package/dist/sandbox/startCmd.js +71 -0
- package/dist/sandbox/startCmd.js.map +1 -0
- package/dist/ui/spinner.d.ts +18 -0
- package/dist/ui/spinner.js +26 -0
- package/dist/ui/spinner.js.map +1 -0
- package/dist/verify/metrics.d.ts +13 -0
- package/dist/verify/metrics.js +72 -0
- package/dist/verify/metrics.js.map +1 -0
- package/package.json +68 -0
- package/scripts/cheat-demo.cjs +102 -0
- package/templates/ledger/preload.cjs +331 -0
- package/templates/pen/preload.cjs +196 -0
- package/templates/pitstop.prompt.md +371 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* OpenPitStop `scan --ledger` HTTP sandbox (CommonJS preload).
|
|
5
|
+
*
|
|
6
|
+
* Loaded into the app-under-test via `NODE_OPTIONS=--require=<this file>`, i.e.
|
|
7
|
+
* BEFORE the app's own modules, so every outbound HTTP request is intercepted by
|
|
8
|
+
* nock before a single byte can reach a real payment gateway.
|
|
9
|
+
*
|
|
10
|
+
* SAFETY CONTRACT (this file is the enforcement point):
|
|
11
|
+
* 1. `nock.disableNetConnect()` — any http/https request to a host that is not
|
|
12
|
+
* explicitly mocked below throws `NetConnectNotAllowedError` and the
|
|
13
|
+
* connection never happens. Nothing reaches the real network.
|
|
14
|
+
* 2. Any request to an unmocked host, any child-process spawn, any raw socket
|
|
15
|
+
* (net/tls), any UDP send, or any global `fetch` to a non-local host is a
|
|
16
|
+
* signal that the app does something we cannot guarantee to intercept.
|
|
17
|
+
* In every such case we write an `abort` event to the control stream and
|
|
18
|
+
* exit the process (code 77) — ledger mode is never allowed to keep going
|
|
19
|
+
* when interception is not airtight.
|
|
20
|
+
*
|
|
21
|
+
* Everything recorded here is written as JSONL to
|
|
22
|
+
* `PITSTOP_LEDGER_GATEWAY_LOG` (the "mocked gateway's" receipt ledger) and to
|
|
23
|
+
* `PITSTOP_LEDGER_CONTROL` (the harness's control/abort stream).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
|
|
28
|
+
const CONTROL = process.env.PITSTOP_LEDGER_CONTROL || null;
|
|
29
|
+
const GATEWAY_LOG = process.env.PITSTOP_LEDGER_GATEWAY_LOG || null;
|
|
30
|
+
const HOSTS = (process.env.PITSTOP_LEDGER_GATEWAY_HOSTS || '')
|
|
31
|
+
.split(',')
|
|
32
|
+
.map((s) => s.trim())
|
|
33
|
+
.filter(Boolean);
|
|
34
|
+
const CHARGE_RE = new RegExp(
|
|
35
|
+
process.env.PITSTOP_LEDGER_CHARGE_RE ||
|
|
36
|
+
'(?:^|/)(payments|charges|payment_intents|paymentintents|captures?|transactions)(/|$|\\?)',
|
|
37
|
+
'i',
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
let aborted = false;
|
|
41
|
+
|
|
42
|
+
function writeJsonl(file, obj) {
|
|
43
|
+
if (!file) return;
|
|
44
|
+
try {
|
|
45
|
+
fs.appendFileSync(file, JSON.stringify(obj) + '\n');
|
|
46
|
+
} catch (_) {
|
|
47
|
+
/* best-effort: if we cannot even log, do not risk continuing */
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function emit(event) {
|
|
52
|
+
writeJsonl(CONTROL, Object.assign({ at: new Date().toISOString() }, event));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Unrecoverable safety trip: record it, scream, and die. Never continue. */
|
|
56
|
+
function abort(reason) {
|
|
57
|
+
if (aborted) return;
|
|
58
|
+
aborted = true;
|
|
59
|
+
emit({ event: 'abort', reason });
|
|
60
|
+
console.error('[pitstop-ledger] ABORT: ' + reason);
|
|
61
|
+
setTimeout(() => process.exit(77), 0);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* ------------------------------------------------------------------ */
|
|
65
|
+
/* nock: the only sanctioned way out of the process is a mocked reply */
|
|
66
|
+
/* ------------------------------------------------------------------ */
|
|
67
|
+
|
|
68
|
+
let nock;
|
|
69
|
+
try {
|
|
70
|
+
nock = require('nock');
|
|
71
|
+
} catch (_) {
|
|
72
|
+
abort(
|
|
73
|
+
'nock is not resolvable from the ledger preload — outbound HTTP cannot be ' +
|
|
74
|
+
'guaranteed to be intercepted; refusing to run ledger mode',
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
nock.disableNetConnect();
|
|
79
|
+
|
|
80
|
+
nock.emitter.on('no match', (req) => {
|
|
81
|
+
const host = (req && (req.hostname || req.host)) || 'unknown';
|
|
82
|
+
abort(
|
|
83
|
+
'outbound request to host ' +
|
|
84
|
+
host +
|
|
85
|
+
' has no mock — it would require real network access; aborting ledger mode',
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
function safeBody(b) {
|
|
90
|
+
if (Buffer.isBuffer(b)) b = b.toString('utf8');
|
|
91
|
+
if (typeof b === 'string') {
|
|
92
|
+
try {
|
|
93
|
+
return JSON.parse(b);
|
|
94
|
+
} catch (_) {
|
|
95
|
+
return { raw: b };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return b;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getHeader(headers, name) {
|
|
102
|
+
if (!headers || typeof headers !== 'object') return undefined;
|
|
103
|
+
const target = name.toLowerCase();
|
|
104
|
+
for (const k of Object.keys(headers)) {
|
|
105
|
+
if (k.toLowerCase() === target) return headers[k];
|
|
106
|
+
}
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function deriveKey(req, body) {
|
|
111
|
+
const h = req.headers || {};
|
|
112
|
+
for (const name of [
|
|
113
|
+
'idempotency-key',
|
|
114
|
+
'x-idempotency-key',
|
|
115
|
+
'idempotency',
|
|
116
|
+
'x-idempotency',
|
|
117
|
+
'x-razorpay-idempotency',
|
|
118
|
+
]) {
|
|
119
|
+
const v = getHeader(h, name);
|
|
120
|
+
if (v != null && v !== '') return String(v);
|
|
121
|
+
}
|
|
122
|
+
const oid = body && (body.order_id || body.orderId || body.reference_id);
|
|
123
|
+
if (oid != null) return String(oid);
|
|
124
|
+
const m = String(req.path || '')
|
|
125
|
+
.replace(/^https?:\/\/[^/]+/i, '')
|
|
126
|
+
.match(/\/(payments|charges|payment_intents|transactions)\/([^/]+)/i);
|
|
127
|
+
if (m) return m[2];
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function isCharge(req) {
|
|
132
|
+
const method = String(req.method || 'GET').toUpperCase();
|
|
133
|
+
if (method === 'GET' || method === 'HEAD' || method === 'OPTIONS') return false;
|
|
134
|
+
const p = String(req.path || req.url || '');
|
|
135
|
+
return CHARGE_RE.test(p);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let seq = 0;
|
|
139
|
+
function nextId(prefix) {
|
|
140
|
+
seq += 1;
|
|
141
|
+
return (prefix || 'req') + '_' + Date.now().toString(36) + '_' + seq;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Build a plausible gateway success response for the host being called. */
|
|
145
|
+
function gatewaySuccess(host, req, body, record) {
|
|
146
|
+
const amount = body && body.amount != null ? body.amount : 1000;
|
|
147
|
+
const currency = (body && body.currency) || 'INR';
|
|
148
|
+
const h = String(host).toLowerCase();
|
|
149
|
+
|
|
150
|
+
if (h.includes('razorpay')) {
|
|
151
|
+
return {
|
|
152
|
+
id: 'pay_' + record.requestId,
|
|
153
|
+
entity: 'payment',
|
|
154
|
+
amount,
|
|
155
|
+
currency,
|
|
156
|
+
status: /capture|settle/i.test(record.path) ? 'captured' : 'authorized',
|
|
157
|
+
order_id: (body && body.order_id) || null,
|
|
158
|
+
method: 'card',
|
|
159
|
+
vpa: null,
|
|
160
|
+
created_at: Date.now() / 1000,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
if (h.includes('stripe')) {
|
|
164
|
+
return {
|
|
165
|
+
id: 'ch_' + record.requestId,
|
|
166
|
+
object: 'charge',
|
|
167
|
+
amount,
|
|
168
|
+
currency: currency.toLowerCase(),
|
|
169
|
+
status: 'succeeded',
|
|
170
|
+
paid: true,
|
|
171
|
+
captured: true,
|
|
172
|
+
requestId: record.requestId,
|
|
173
|
+
created: Date.now() / 1000,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (h.includes('braintree')) {
|
|
177
|
+
return {
|
|
178
|
+
id: record.requestId,
|
|
179
|
+
object: 'transaction',
|
|
180
|
+
status: 'settled',
|
|
181
|
+
amount: String((Number(amount) || 0) / 100),
|
|
182
|
+
currencyIsoCode: currency,
|
|
183
|
+
createdAt: new Date().toISOString(),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
id: record.requestId,
|
|
188
|
+
object: 'payment',
|
|
189
|
+
status: 'succeeded',
|
|
190
|
+
amount,
|
|
191
|
+
currency,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function arm(host) {
|
|
196
|
+
const scope = nock(host).persist();
|
|
197
|
+
const methodArmed = {};
|
|
198
|
+
const armMethod = (method) => {
|
|
199
|
+
if (methodArmed[method]) return;
|
|
200
|
+
methodArmed[method] = true;
|
|
201
|
+
scope[method.toLowerCase()](/.*/).reply(function (uri, requestBody) {
|
|
202
|
+
const req = this.req;
|
|
203
|
+
if (req && !req.headers) {
|
|
204
|
+
try {
|
|
205
|
+
req.headers = req.getHeaders();
|
|
206
|
+
} catch (_) {
|
|
207
|
+
req.headers = {};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const body = safeBody(requestBody);
|
|
211
|
+
const record = {
|
|
212
|
+
at: new Date().toISOString(),
|
|
213
|
+
host,
|
|
214
|
+
method: String((req && req.method) || method).toUpperCase(),
|
|
215
|
+
path: uri,
|
|
216
|
+
key: deriveKey(req, body),
|
|
217
|
+
orderId:
|
|
218
|
+
(body && (body.order_id || body.orderId || body.reference_id)) || null,
|
|
219
|
+
requestId: nextId('req'),
|
|
220
|
+
requestHeaders: (req && req.headers) || {},
|
|
221
|
+
requestBody: body,
|
|
222
|
+
};
|
|
223
|
+
record.charge = isCharge(req);
|
|
224
|
+
const respBody = gatewaySuccess(host, req, body, record);
|
|
225
|
+
record.responseStatus = 200;
|
|
226
|
+
record.responseBody = respBody;
|
|
227
|
+
writeJsonl(GATEWAY_LOG, record);
|
|
228
|
+
emit({ event: 'gateway-call', charge: record.charge, key: record.key, host });
|
|
229
|
+
return [200, respBody];
|
|
230
|
+
});
|
|
231
|
+
};
|
|
232
|
+
for (const m of ['POST', 'GET', 'PUT', 'PATCH', 'DELETE']) armMethod(m);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
for (const host of HOSTS) {
|
|
236
|
+
if (!host) continue;
|
|
237
|
+
try {
|
|
238
|
+
arm(host);
|
|
239
|
+
} catch (e) {
|
|
240
|
+
abort('failed to arm mock for ' + host + ': ' + (e && e.message));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/* ------------------------------------------------------------------ */
|
|
245
|
+
/* Out-of-band channels nock cannot see — each one aborts ledger mode */
|
|
246
|
+
/* ------------------------------------------------------------------ */
|
|
247
|
+
|
|
248
|
+
// Native binaries (curl, wget, gh, ...) would bypass nock entirely.
|
|
249
|
+
const cp = require('child_process');
|
|
250
|
+
for (const m of ['exec', 'execFile', 'execFileSync', 'spawn', 'spawnSync', 'fork']) {
|
|
251
|
+
if (typeof cp[m] === 'function') {
|
|
252
|
+
const name = m;
|
|
253
|
+
cp[m] = function (...args) {
|
|
254
|
+
abort(
|
|
255
|
+
'ledger sandbox blocks child_process.' +
|
|
256
|
+
name +
|
|
257
|
+
'(' +
|
|
258
|
+
String(args[0] || '') +
|
|
259
|
+
') — native tools cannot be intercepted; aborting ledger mode',
|
|
260
|
+
);
|
|
261
|
+
const err = new Error('[pitstop-ledger] blocked external process spawn');
|
|
262
|
+
err.code = 'PITSTOP_LEDGER_BLOCKED_SPAWN';
|
|
263
|
+
throw err;
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Raw sockets (net/tls) bypass nock.
|
|
269
|
+
const net = require('net');
|
|
270
|
+
const tls = require('tls');
|
|
271
|
+
const block = (what) => {
|
|
272
|
+
abort(
|
|
273
|
+
'ledger sandbox blocks a raw ' +
|
|
274
|
+
what +
|
|
275
|
+
' — such a connection cannot be intercepted by nock; aborting ledger mode',
|
|
276
|
+
);
|
|
277
|
+
const err = new Error('[pitstop-ledger] blocked raw ' + what);
|
|
278
|
+
err.code = 'PITSTOP_LEDGER_BLOCKED_SOCKET';
|
|
279
|
+
throw err;
|
|
280
|
+
};
|
|
281
|
+
net.connect = function () {
|
|
282
|
+
return block('net.connect');
|
|
283
|
+
};
|
|
284
|
+
net.createConnection = function () {
|
|
285
|
+
return block('net.createConnection');
|
|
286
|
+
};
|
|
287
|
+
net.Socket.prototype.connect = function () {
|
|
288
|
+
return block('net.Socket.connect');
|
|
289
|
+
};
|
|
290
|
+
tls.connect = function () {
|
|
291
|
+
return block('tls.connect');
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// Undici / global fetch bypasses nock's ClientRequest interception.
|
|
295
|
+
if (typeof globalThis.fetch === 'function') {
|
|
296
|
+
const realFetch = globalThis.fetch;
|
|
297
|
+
globalThis.fetch = async function (input, init) {
|
|
298
|
+
let u = null;
|
|
299
|
+
try {
|
|
300
|
+
u = new URL(typeof input === 'string' ? input : (input && input.url) || 'http://localhost/');
|
|
301
|
+
} catch (_) {
|
|
302
|
+
u = null;
|
|
303
|
+
}
|
|
304
|
+
if (u && !(u.hostname === 'localhost' || u.hostname === '127.0.0.1' || u.hostname === '::1')) {
|
|
305
|
+
abort(
|
|
306
|
+
'global fetch to external host ' + u.href + ' cannot be intercepted by nock; aborting ledger mode',
|
|
307
|
+
);
|
|
308
|
+
throw new Error('[pitstop-ledger] blocked external fetch');
|
|
309
|
+
}
|
|
310
|
+
return realFetch.call(globalThis, input, init);
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// UDP sends bypass nock.
|
|
315
|
+
try {
|
|
316
|
+
const dgram = require('dgram');
|
|
317
|
+
dgram.Socket.prototype.send = function () {
|
|
318
|
+
return block('dgram.Socket.send');
|
|
319
|
+
};
|
|
320
|
+
} catch (_) {
|
|
321
|
+
/* dgram absent — nothing to block */
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
emit({
|
|
325
|
+
event: 'armed',
|
|
326
|
+
hosts: HOSTS,
|
|
327
|
+
gatewayLog: GATEWAY_LOG,
|
|
328
|
+
});
|
|
329
|
+
console.error(
|
|
330
|
+
'[pitstop-ledger] sandbox armed; mocked hosts: ' + (HOSTS.join(', ') || '(none)'),
|
|
331
|
+
);
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* OpenPitStop `pen` HTTP sandbox (CommonJS preload).
|
|
5
|
+
*
|
|
6
|
+
* Loaded into the app-under-test via `NODE_OPTIONS=--require=<this file>`,
|
|
7
|
+
* BEFORE the app's own modules, so the "penetration test" can fire attack
|
|
8
|
+
* traffic at the app while being certain of two things:
|
|
9
|
+
*
|
|
10
|
+
* 1. NOTHING reaches the real network. Every outbound HTTP/HTTPS request is
|
|
11
|
+
* intercepted (nock catch-all + fetch wrapper) and recorded into
|
|
12
|
+
* `PITSTOP_PEN_OUTBOUND`; raw sockets and UDP are blocked outright.
|
|
13
|
+
* An SSRF is therefore not an exfiltration — it is a recorded log line.
|
|
14
|
+
* 2. Everything the app tries to reach out to is evidence: outbound HTTP is
|
|
15
|
+
* logged as `{kind:"http", host, method, path}`, child-process spawns as
|
|
16
|
+
* `{kind:"spawn", cmd}`. A `pitstop` canary marker inside a logged line
|
|
17
|
+
* is PROOF of command injection / SSRF without a single real byte leaving
|
|
18
|
+
* the machine.
|
|
19
|
+
*
|
|
20
|
+
* SAFETY NOTES (documented honestly, unlike a magic "pen"):
|
|
21
|
+
* - child_process calls are RECORDED but not blocked: apps legitimately spawn
|
|
22
|
+
* (dev servers, migrators), and this is the user's own start script.
|
|
23
|
+
* The attacker payload canary still shows up in the spawn log, which is the
|
|
24
|
+
* proof we need. A truly hostile repo is no more dangerous than running
|
|
25
|
+
* `npm start` yourself — which is exactly what `pitstop pen` asks you to do.
|
|
26
|
+
* - raw sockets (net/tls) and UDP are BLOCKED: those bypass interception
|
|
27
|
+
* entirely, so the "nothing reaches the real network" guarantee cannot be
|
|
28
|
+
* kept with them enabled.
|
|
29
|
+
*
|
|
30
|
+
* Control stream (`PITSTOP_PEN_CONTROL`): JSONL events `armed` / `probe`,
|
|
31
|
+
* plus every `outbound` / `spawn` / `blocked` line mirrored for the harness.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const fs = require('fs');
|
|
35
|
+
|
|
36
|
+
const CONTROL = process.env.PITSTOP_PEN_CONTROL || null;
|
|
37
|
+
const OUTBOUND = process.env.PITSTOP_PEN_OUTBOUND || null;
|
|
38
|
+
|
|
39
|
+
let seq = 0;
|
|
40
|
+
|
|
41
|
+
function writeJsonl(file, obj) {
|
|
42
|
+
if (!file) return;
|
|
43
|
+
try {
|
|
44
|
+
fs.appendFileSync(file, JSON.stringify(obj) + '\n');
|
|
45
|
+
} catch (_) {
|
|
46
|
+
/* best-effort */
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function emit(event) {
|
|
51
|
+
const line = Object.assign({ at: new Date().toISOString(), n: ++seq }, event);
|
|
52
|
+
writeJsonl(CONTROL, line);
|
|
53
|
+
if (line.kind === 'http' || line.kind === 'spawn' || line.kind === 'blocked') {
|
|
54
|
+
writeJsonl(OUTBOUND, line);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function trunc(s, n) {
|
|
59
|
+
const str = String(s == null ? '' : s);
|
|
60
|
+
return str.length > n ? str.slice(0, n) + '…' : str;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function hostOf(u) {
|
|
64
|
+
try {
|
|
65
|
+
return new URL(u).host;
|
|
66
|
+
} catch (_) {
|
|
67
|
+
return trunc(u, 160);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const isLocal = (hostname) =>
|
|
72
|
+
hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';
|
|
73
|
+
|
|
74
|
+
/* ------------------------------------------------------------------ */
|
|
75
|
+
/* nock: every outbound HTTP/HTTPS request is intercepted + recorded */
|
|
76
|
+
/* ------------------------------------------------------------------ */
|
|
77
|
+
|
|
78
|
+
let nock;
|
|
79
|
+
try {
|
|
80
|
+
nock = require('nock');
|
|
81
|
+
} catch (_) {
|
|
82
|
+
emit({ event: 'abort', reason: 'nock is not resolvable from the pen preload' });
|
|
83
|
+
setTimeout(() => process.exit(77), 0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
nock.disableNetConnect();
|
|
87
|
+
|
|
88
|
+
// Catch-all: ANY host the app calls is intercepted (recorded, fake reply).
|
|
89
|
+
const scope = nock(/(.*)/).persist();
|
|
90
|
+
const armMethod = (method) => {
|
|
91
|
+
if (!scope[method.toLowerCase()]) return;
|
|
92
|
+
scope[method.toLowerCase()](/.*/).reply(function (uri) {
|
|
93
|
+
const req = this.req;
|
|
94
|
+
const host =
|
|
95
|
+
(req && (req.hostname || req.host || (req.headers && req.headers.host) || (req.options && req.options.hostname))) || 'unknown';
|
|
96
|
+
emit({
|
|
97
|
+
kind: 'http',
|
|
98
|
+
host,
|
|
99
|
+
method: String((req && req.method) || method).toUpperCase(),
|
|
100
|
+
path: trunc(uri, 300),
|
|
101
|
+
});
|
|
102
|
+
// A bland 200 JSON reply: the app keeps working, we keep the receipt.
|
|
103
|
+
return [
|
|
104
|
+
200,
|
|
105
|
+
{ ok: true, intercepted: true, note: 'pitstop pen sandbox — this call never reached the real network' },
|
|
106
|
+
];
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
for (const m of ['POST', 'GET', 'PUT', 'PATCH', 'DELETE', 'HEAD']) armMethod(m);
|
|
110
|
+
|
|
111
|
+
// undici / global fetch bypasses nock's ClientRequest interception.
|
|
112
|
+
if (typeof globalThis.fetch === 'function') {
|
|
113
|
+
const realFetch = globalThis.fetch;
|
|
114
|
+
globalThis.fetch = async function (input, init) {
|
|
115
|
+
let u = null;
|
|
116
|
+
try {
|
|
117
|
+
u = new URL(typeof input === 'string' ? input : (input && input.url) || 'http://localhost/');
|
|
118
|
+
} catch (_) {
|
|
119
|
+
u = null;
|
|
120
|
+
}
|
|
121
|
+
if (u && !isLocal(u.hostname)) {
|
|
122
|
+
emit({
|
|
123
|
+
kind: 'http',
|
|
124
|
+
host: u.host,
|
|
125
|
+
method: String((init && init.method) || 'GET').toUpperCase(),
|
|
126
|
+
path: trunc(u.pathname + u.search, 300),
|
|
127
|
+
via: 'fetch',
|
|
128
|
+
});
|
|
129
|
+
return new Response(
|
|
130
|
+
JSON.stringify({ ok: true, intercepted: true, note: 'pitstop pen sandbox' }),
|
|
131
|
+
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
return realFetch.call(globalThis, input, init);
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* ------------------------------------------------------------------ */
|
|
139
|
+
/* child_process: RECORD (do not block) — the canary is the proof */
|
|
140
|
+
/* ------------------------------------------------------------------ */
|
|
141
|
+
|
|
142
|
+
const cp = require('child_process');
|
|
143
|
+
for (const m of ['exec', 'execFile', 'execFileSync', 'spawn', 'spawnSync', 'fork']) {
|
|
144
|
+
if (typeof cp[m] === 'function') {
|
|
145
|
+
const name = m;
|
|
146
|
+
const real = cp[m];
|
|
147
|
+
cp[m] = function (...args) {
|
|
148
|
+
const target = args[0];
|
|
149
|
+
const rest = Array.isArray(args[1]) ? args[1] : [];
|
|
150
|
+
const cmd = String(typeof target === 'string' ? target : (target && target.command) || '');
|
|
151
|
+
emit({
|
|
152
|
+
kind: 'spawn',
|
|
153
|
+
via: 'child_process.' + name,
|
|
154
|
+
cmd: trunc([cmd].concat(rest.map(String)).join(' '), 600),
|
|
155
|
+
});
|
|
156
|
+
return real.apply(cp, args);
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* ------------------------------------------------------------------ */
|
|
162
|
+
/* raw sockets / UDP: BLOCK + record — interception would not hold */
|
|
163
|
+
/* ------------------------------------------------------------------ */
|
|
164
|
+
|
|
165
|
+
const net = require('net');
|
|
166
|
+
const tls = require('tls');
|
|
167
|
+
const block = (what, detail) => {
|
|
168
|
+
emit({ kind: 'blocked', what, detail: trunc(detail || '', 200) });
|
|
169
|
+
const err = new Error('[pitstop-pen] blocked raw ' + what + ' — it would bypass interception');
|
|
170
|
+
err.code = 'PITSTOP_PEN_BLOCKED_CONNECT';
|
|
171
|
+
throw err;
|
|
172
|
+
};
|
|
173
|
+
net.connect = function (...a) {
|
|
174
|
+
return block('net.connect', hostOf(typeof a[0] === 'object' && a[0] ? (a[0].host || a[0].port) : a[0]));
|
|
175
|
+
};
|
|
176
|
+
net.createConnection = function (...a) {
|
|
177
|
+
return block('net.createConnection', hostOf(typeof a[0] === 'object' && a[0] ? (a[0].host || a[0].port) : a[0]));
|
|
178
|
+
};
|
|
179
|
+
net.Socket.prototype.connect = function (...a) {
|
|
180
|
+
return block('net.Socket.connect', hostOf(typeof a[0] === 'object' && a[0] ? (a[0].host || a[0].port) : a[0]));
|
|
181
|
+
};
|
|
182
|
+
tls.connect = function (...a) {
|
|
183
|
+
return block('tls.connect', hostOf(typeof a[0] === 'object' && a[0] ? (a[0].host || a[0].port) : a[0]));
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const dgram = require('dgram');
|
|
188
|
+
dgram.Socket.prototype.send = function (...a) {
|
|
189
|
+
return block('dgram.Socket.send', String(a[1] || ''));
|
|
190
|
+
};
|
|
191
|
+
} catch (_) {
|
|
192
|
+
/* dgram absent */
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
emit({ event: 'armed', note: 'pitstop pen sandbox armed — outbound HTTP intercepted, raw sockets blocked' });
|
|
196
|
+
console.error('[pitstop-pen] sandbox armed — all outbound HTTP is intercepted and recorded; raw sockets are blocked');
|