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,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* repro/pen.ts — permanent repro tests for pen findings.
|
|
3
|
+
*
|
|
4
|
+
* A pen repro RE-PLAYS the exact attack the pen test fired (same route, same
|
|
5
|
+
* payload, same sandbox) and asserts the SAFE outcome. While the vuln exists
|
|
6
|
+
* the test FAILS; after the fix it PASSES. That is the fail-first contract,
|
|
7
|
+
* applied to a live penetration test.
|
|
8
|
+
*
|
|
9
|
+
* The test is always generated as a `node:test` ESM file (like the ledger
|
|
10
|
+
* repro) because it must boot the app under the pen preload, regardless of the
|
|
11
|
+
* repo's own test framework.
|
|
12
|
+
*/
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { reproSlug } from "./ids.js";
|
|
17
|
+
function preloadPath() {
|
|
18
|
+
return fileURLToPath(new URL("../../templates/pen/preload.cjs", import.meta.url));
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Turn a pen finding into the normalized ReproFinding shape the slug generator
|
|
22
|
+
* (and any future caller) expects.
|
|
23
|
+
*/
|
|
24
|
+
export function penFindingToRepro(f) {
|
|
25
|
+
return {
|
|
26
|
+
id: f.id,
|
|
27
|
+
source: "pen",
|
|
28
|
+
severity: f.severity,
|
|
29
|
+
type: f.type,
|
|
30
|
+
description: f.title,
|
|
31
|
+
file: f.file,
|
|
32
|
+
line: f.line,
|
|
33
|
+
data: f,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const J = (s) => JSON.stringify(s);
|
|
37
|
+
/**
|
|
38
|
+
* The assertion body for each attack type. Returns JS that must be embedded in
|
|
39
|
+
* the test; `f` carries the attack + observed evidence, `marker` is a fresh
|
|
40
|
+
* random marker for THIS replay.
|
|
41
|
+
*/
|
|
42
|
+
function assertionBody(f, marker) {
|
|
43
|
+
switch (f.type) {
|
|
44
|
+
case "reflected-xss":
|
|
45
|
+
return [
|
|
46
|
+
`assert.ok(!res.body.includes(${J(marker)}), "the XSS payload was reflected UNESCAPED in the response — reflecting user input is live");`,
|
|
47
|
+
];
|
|
48
|
+
case "path-traversal":
|
|
49
|
+
return [
|
|
50
|
+
`assert.ok(!res.body.includes("root:x:0:0") && !res.body.includes("[extensions]") && !res.body.includes("; for 16-bit app support"), "the traversal payload leaked a host file inside the response — arbitrary file read is live");`,
|
|
51
|
+
];
|
|
52
|
+
case "command-injection":
|
|
53
|
+
return [
|
|
54
|
+
`await sleep(400);`,
|
|
55
|
+
`const spawns = readOutbound(outboundPath).filter((e) => e.kind === "spawn" && e.cmd && e.cmd.includes(${J(marker)}));`,
|
|
56
|
+
`assert.equal(spawns.length, 0, "the sandbox recorded the marker inside a child_process spawn: command injection is live: " + JSON.stringify(spawns.map((e) => e.cmd)));`,
|
|
57
|
+
];
|
|
58
|
+
case "ssrf":
|
|
59
|
+
return [
|
|
60
|
+
`await sleep(400);`,
|
|
61
|
+
`const hits = readOutbound(outboundPath).filter((e) => e.kind === "http" && e.host && e.host.includes("ssrf-canary"));`,
|
|
62
|
+
`assert.equal(hits.length, 0, "the server opened an outbound connection to the canary host: SSRF is live: " + JSON.stringify(hits.map((e) => e.host)));`,
|
|
63
|
+
];
|
|
64
|
+
case "sql-injection":
|
|
65
|
+
case "nosql-injection":
|
|
66
|
+
return [
|
|
67
|
+
`assert.ok(res.status < 500, "the injection payload crashed the server (HTTP " + res.status + ") — input reaches a query raw");`,
|
|
68
|
+
`assert.ok(res.status !== 200 || !/syntax|unterminated|parse|mongo|sequelize|sqlite|knex|cannot read/i.test(res.snippet), "the injection payload surfaced a DB error in the response");`,
|
|
69
|
+
];
|
|
70
|
+
case "crash-on-input":
|
|
71
|
+
return [
|
|
72
|
+
`assert.ok(res.status < 500, "the crafted input crashed the server (HTTP " + res.status + ") — unhandled exceptions are a DoS vector");`,
|
|
73
|
+
];
|
|
74
|
+
case "missing-auth":
|
|
75
|
+
return [
|
|
76
|
+
`assert.ok(res.status === 401 || res.status === 403, "the sensitive route answered unauthenticated with HTTP " + res.status + " — anyone can reach it");`,
|
|
77
|
+
];
|
|
78
|
+
case "no-rate-limit":
|
|
79
|
+
return [
|
|
80
|
+
`let got429 = 0;`,
|
|
81
|
+
`for (let i = 0; i < 30; i++) {`,
|
|
82
|
+
` const r = await send("POST", ${J(f.attack?.path ?? "/")}, { username: "admin", password: "x-" + i });`,
|
|
83
|
+
` if (r.status === 429) got429++;`,
|
|
84
|
+
`}`,
|
|
85
|
+
`assert.ok(got429 > 0, "none of the 30 rapid requests were rate-limited (no 429) — credential stuffing is unthrottled");`,
|
|
86
|
+
];
|
|
87
|
+
case "info-leak-header":
|
|
88
|
+
return [
|
|
89
|
+
`assert.ok(!res.headers["x-powered-by"], "X-Powered-By leaked the framework — disable it");`,
|
|
90
|
+
];
|
|
91
|
+
case "missing-security-headers":
|
|
92
|
+
return [
|
|
93
|
+
`const want = ["x-frame-options", "content-security-policy", "x-content-type-options", "strict-transport-security"];`,
|
|
94
|
+
`const missing = want.filter((h) => !res.headers[h]);`,
|
|
95
|
+
`assert.ok(missing.length < 2, "security headers missing: " + missing.join(", "));`,
|
|
96
|
+
];
|
|
97
|
+
default:
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** For static secret findings: guard the exact credential string. */
|
|
102
|
+
function secretGuardBody(f, fileRel) {
|
|
103
|
+
const secretFragment = (f.description.match(/\(([^)]{4,})…\)/) || [])[1] ?? "";
|
|
104
|
+
const needle = secretFragment.slice(0, 8);
|
|
105
|
+
if (!needle)
|
|
106
|
+
return [];
|
|
107
|
+
return [
|
|
108
|
+
`const content = readFileSync(${J(fileRel)}, "utf8");`,
|
|
109
|
+
`assert.ok(!content.includes(${J(needle)}), "the committed credential fragment '" + ${J(needle)} + "' is still in " + ${J(fileRel)} + " — rotate and remove it");`,
|
|
110
|
+
];
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Generate (and optionally write) the repro test file for a pen finding.
|
|
114
|
+
*/
|
|
115
|
+
export function generatePenRepro(repo, f) {
|
|
116
|
+
const isDynamic = f.source === "pen-dynamic" || (f.runtimeProof === "proven" && !!f.attack);
|
|
117
|
+
const isStaticSecret = f.type === "hardcoded-secret" && f.file;
|
|
118
|
+
if (!isDynamic && !isStaticSecret) {
|
|
119
|
+
return {
|
|
120
|
+
ok: false,
|
|
121
|
+
reason: "this pen finding is a static heuristic / config observation — there is no live attack to replay. " +
|
|
122
|
+
"Fix it and re-run `pitstop pen`; the dynamic phase produces replayable repros for live bugs.",
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const r = penFindingToRepro(f);
|
|
126
|
+
const slug = reproSlug(r);
|
|
127
|
+
const file = `pitstop-repro-${slug}.test.mjs`;
|
|
128
|
+
if (isStaticSecret) {
|
|
129
|
+
const fileRel = path.relative(repo, f.file).replace(/\\/g, "/");
|
|
130
|
+
const header = [
|
|
131
|
+
`// OpenPitStop permanent repro test — finding ${f.id}`,
|
|
132
|
+
`// Linked from .pitstop/pen-latest.json.`,
|
|
133
|
+
`//`,
|
|
134
|
+
`// A committed credential guard: while the secret fragment below is present`,
|
|
135
|
+
`// in the file, this test FAILS. Rotate the secret, remove it, and it PASSES.`,
|
|
136
|
+
].join("\n");
|
|
137
|
+
const body = secretGuardBody(f, fileRel);
|
|
138
|
+
if (body.length === 0) {
|
|
139
|
+
return { ok: false, reason: "cannot derive a stable credential fragment from this secret finding" };
|
|
140
|
+
}
|
|
141
|
+
const content = [
|
|
142
|
+
header,
|
|
143
|
+
``,
|
|
144
|
+
`import { test } from "node:test";`,
|
|
145
|
+
`import assert from "node:assert/strict";`,
|
|
146
|
+
`import { readFileSync } from "node:fs";`,
|
|
147
|
+
``,
|
|
148
|
+
`test("pitstop repro ${f.id}: no committed secret in ${fileRel}", () => {`,
|
|
149
|
+
` ${body.join("\n ")}`,
|
|
150
|
+
`});`,
|
|
151
|
+
``,
|
|
152
|
+
].join("\n");
|
|
153
|
+
fs.writeFileSync(path.join(repo, file), content, "utf8");
|
|
154
|
+
return { ok: true, file };
|
|
155
|
+
}
|
|
156
|
+
// Dynamic: replay the exact attack under the sandbox and assert the safe outcome.
|
|
157
|
+
const attack = f.attack;
|
|
158
|
+
if (!attack) {
|
|
159
|
+
return {
|
|
160
|
+
ok: false,
|
|
161
|
+
reason: "dynamic finding carries no recorded attack to replay (evidence missing from pen-latest.json)",
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
// Runtime-proven static findings assert the attack class that proved them
|
|
165
|
+
// (e.g. a proven xss-sink asserts reflected-xss behavior), not the pattern.
|
|
166
|
+
const assertFinding = f.proofType ? { ...f, type: f.proofType } : f;
|
|
167
|
+
const bodyLines = [];
|
|
168
|
+
// The marker lives inside the ORIGINAL attack payload/path (e.g. gpn12345678).
|
|
169
|
+
// Reuse it so the replay's proof matches what the pen test observed.
|
|
170
|
+
const attackSource = JSON.stringify([attack.path, attack.payload ?? null]);
|
|
171
|
+
const marker = attackSource.match(/gpn\d{7,}/)?.[0] ?? `gpn${Math.floor(Math.random() * 1e8)}`;
|
|
172
|
+
const isHammer = f.type === "no-rate-limit";
|
|
173
|
+
const NODE_BASED = [
|
|
174
|
+
"node", "node.exe", "nodejs", "npm", "npm.cmd", "npx", "npx.cmd",
|
|
175
|
+
"yarn", "yarn.cmd", "pnpm", "pnpm.cmd", "tsx", "ts-node", "babel-node", "ojs", "vitest", "jest",
|
|
176
|
+
].map((s) => J(s)).join(", ");
|
|
177
|
+
const prelude = [
|
|
178
|
+
`const REPO = process.cwd();`,
|
|
179
|
+
`const PRELOAD = process.env.PITSTOP_PEN_PRELOAD;`,
|
|
180
|
+
`let BASE_URL = null;`,
|
|
181
|
+
`const NODE_BASED = new Set([${NODE_BASED}]);`,
|
|
182
|
+
``,
|
|
183
|
+
`import { test } from "node:test";`,
|
|
184
|
+
`import assert from "node:assert/strict";`,
|
|
185
|
+
`import { spawn } from "node:child_process";`,
|
|
186
|
+
`import { readFileSync, mkdirSync, existsSync } from "node:fs";`,
|
|
187
|
+
`import net from "node:net";`,
|
|
188
|
+
`import path from "node:path";`,
|
|
189
|
+
``,
|
|
190
|
+
`function freePort() {`,
|
|
191
|
+
` return new Promise((resolve, reject) => {`,
|
|
192
|
+
` const s = net.createServer();`,
|
|
193
|
+
` s.unref();`,
|
|
194
|
+
` s.on("error", reject);`,
|
|
195
|
+
` s.listen(0, () => {`,
|
|
196
|
+
` const p = s.address().port;`,
|
|
197
|
+
` s.close(() => resolve(p));`,
|
|
198
|
+
` });`,
|
|
199
|
+
` });`,
|
|
200
|
+
`}`,
|
|
201
|
+
`const sleep = (ms) => new Promise((r) => setTimeout(r, ms));`,
|
|
202
|
+
``,
|
|
203
|
+
`function resolveStart() {`,
|
|
204
|
+
` const pkg = JSON.parse(readFileSync(path.join(REPO, "package.json"), "utf8"));`,
|
|
205
|
+
` const start = (pkg.scripts && pkg.scripts.start) || "";`,
|
|
206
|
+
` if (!start) throw new Error("no start script in package.json");`,
|
|
207
|
+
` const tokens = start.trim().split(/\\s+/);`,
|
|
208
|
+
` if (!NODE_BASED.has(tokens[0])) {`,
|
|
209
|
+
` throw new Error("start script runs " + tokens[0] + ", which is not Node-based — refusing");`,
|
|
210
|
+
` }`,
|
|
211
|
+
` return { cmd: tokens[0], args: tokens.slice(1) };`,
|
|
212
|
+
`}`,
|
|
213
|
+
``,
|
|
214
|
+
`function controlHas(controlPath, needle) {`,
|
|
215
|
+
` if (!existsSync(controlPath)) return false;`,
|
|
216
|
+
` try {`,
|
|
217
|
+
` return readFileSync(controlPath, "utf8").split(/\\r?\\n/).some((l) => l.includes(needle));`,
|
|
218
|
+
` } catch {`,
|
|
219
|
+
` return false;`,
|
|
220
|
+
` }`,
|
|
221
|
+
`}`,
|
|
222
|
+
``,
|
|
223
|
+
`function readOutbound(p) {`,
|
|
224
|
+
` if (!existsSync(p)) return [];`,
|
|
225
|
+
` return readFileSync(p, "utf8").split(/\\r?\\n/).filter(Boolean).map((l) => {`,
|
|
226
|
+
` try { return JSON.parse(l); } catch { return null; }`,
|
|
227
|
+
` }).filter(Boolean);`,
|
|
228
|
+
`}`,
|
|
229
|
+
``,
|
|
230
|
+
`async function send(method, p, body) {`,
|
|
231
|
+
` try {`,
|
|
232
|
+
` const res = await fetch(BASE_URL + p, {`,
|
|
233
|
+
` method,`,
|
|
234
|
+
` headers: body !== undefined ? { "Content-Type": "application/json" } : undefined,`,
|
|
235
|
+
` body: body !== undefined ? JSON.stringify(body) : undefined,`,
|
|
236
|
+
` redirect: "manual",`,
|
|
237
|
+
` signal: AbortSignal.timeout(6000),`,
|
|
238
|
+
` });`,
|
|
239
|
+
` const text = await res.text().catch(() => "");`,
|
|
240
|
+
` const headers = {};`,
|
|
241
|
+
` res.headers.forEach((v, k) => { headers[k] = v; });`,
|
|
242
|
+
` return { status: res.status, body: text, snippet: text.slice(0, 300), headers };`,
|
|
243
|
+
` } catch (e) {`,
|
|
244
|
+
` return { status: 0, body: "", snippet: "", headers: {} };`,
|
|
245
|
+
` }`,
|
|
246
|
+
`}`,
|
|
247
|
+
``,
|
|
248
|
+
`test("pitstop repro ${f.id}: ${assertFinding.type} blocked on ${attack.path}", async () => {`,
|
|
249
|
+
` assert.ok(PRELOAD, "PITSTOP_PEN_PRELOAD not set — run this via \`npx openpitstop repro ${f.id}\`");`,
|
|
250
|
+
``,
|
|
251
|
+
` const start = resolveStart();`,
|
|
252
|
+
` const runDir = path.join(REPO, ".pitstop", "repro", new Date().toISOString().replace(/[:.]/g, "-"));`,
|
|
253
|
+
` mkdirSync(runDir, { recursive: true });`,
|
|
254
|
+
` const controlPath = path.join(runDir, "control.jsonl");`,
|
|
255
|
+
` const outboundPath = path.join(runDir, "outbound.jsonl");`,
|
|
256
|
+
` const port = await freePort();`,
|
|
257
|
+
` BASE_URL = "http://127.0.0.1:" + port;`,
|
|
258
|
+
``,
|
|
259
|
+
];
|
|
260
|
+
const boot = [
|
|
261
|
+
` const env = {`,
|
|
262
|
+
` ...process.env,`,
|
|
263
|
+
` NODE_OPTIONS: [process.env.NODE_OPTIONS, "--require=" + PRELOAD].filter(Boolean).join(" "),`,
|
|
264
|
+
` PORT: String(port),`,
|
|
265
|
+
` PITSTOP_PEN_CONTROL: controlPath,`,
|
|
266
|
+
` PITSTOP_PEN_OUTBOUND: outboundPath,`,
|
|
267
|
+
` RAZORPAY_KEY_ID: process.env.RAZORPAY_KEY_ID || "rzp_test_pitstop000000",`,
|
|
268
|
+
` RAZORPAY_KEY_SECRET: process.env.RAZORPAY_KEY_SECRET || "pitstop_fake_secret",`,
|
|
269
|
+
` STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY || "sk_test_pitstop_fake",`,
|
|
270
|
+
` NODE_ENV: process.env.NODE_ENV || "test",`,
|
|
271
|
+
` };`,
|
|
272
|
+
` const child = spawn(start.cmd, start.args, { cwd: REPO, env, stdio: ["ignore", "ignore", "inherit"] });`,
|
|
273
|
+
` try {`,
|
|
274
|
+
` const deadline = Date.now() + 25000;`,
|
|
275
|
+
` let up = false;`,
|
|
276
|
+
` while (Date.now() < deadline) {`,
|
|
277
|
+
` if (child.exitCode !== null) break;`,
|
|
278
|
+
` if (controlHas(controlPath, '"event":"armed"')) {`,
|
|
279
|
+
` try {`,
|
|
280
|
+
` await fetch(BASE_URL + "/__pitstop_pen_probe__");`,
|
|
281
|
+
` up = true;`,
|
|
282
|
+
` break;`,
|
|
283
|
+
` } catch {}`,
|
|
284
|
+
` }`,
|
|
285
|
+
` await sleep(150);`,
|
|
286
|
+
` }`,
|
|
287
|
+
` assert.ok(up, "app did not come up under the sandbox within 25s");`,
|
|
288
|
+
];
|
|
289
|
+
const attackCode = isHammer
|
|
290
|
+
? []
|
|
291
|
+
: [
|
|
292
|
+
` const res = await send(${J(attack.method)}, ${J(attack.path)}, ${attack.payload !== undefined ? J(attack.payload) : "undefined"});`,
|
|
293
|
+
];
|
|
294
|
+
const close = [
|
|
295
|
+
` } finally {`,
|
|
296
|
+
` try { child.kill(); } catch {}`,
|
|
297
|
+
` }`,
|
|
298
|
+
`});`,
|
|
299
|
+
];
|
|
300
|
+
const assertCode = isHammer
|
|
301
|
+
? assertionBody(assertFinding, marker)
|
|
302
|
+
: assertionBody(assertFinding, marker).map((l) => ` ` + l);
|
|
303
|
+
// header for the test file
|
|
304
|
+
const headerLines = [
|
|
305
|
+
`// OpenPitStop permanent repro test — finding ${f.id}`,
|
|
306
|
+
`// Linked from .pitstop/pen-latest.json.`,
|
|
307
|
+
`//`,
|
|
308
|
+
`// REPLAYS the exact attack the pen test fired: ${attack.method} ${attack.path}`,
|
|
309
|
+
`// and asserts the SAFE outcome. This test MUST FAIL while the vulnerability`,
|
|
310
|
+
`// exists and MUST PASS only after the fix.`,
|
|
311
|
+
];
|
|
312
|
+
bodyLines.push(...prelude, ...boot, ...attackCode, ...assertCode, ...close);
|
|
313
|
+
const content = [headerLines.join("\n"), "", ...bodyLines, ""].join("\n");
|
|
314
|
+
fs.writeFileSync(path.join(repo, file), content, "utf8");
|
|
315
|
+
return { ok: true, file };
|
|
316
|
+
}
|
|
317
|
+
/** Absolute path of the pen sandbox preload (for run.ts). */
|
|
318
|
+
export function penPreloadPath() {
|
|
319
|
+
return preloadPath();
|
|
320
|
+
}
|
|
321
|
+
//# sourceMappingURL=pen.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pen.js","sourceRoot":"","sources":["../../src/repro/pen.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface ReproRunResult {
|
|
2
|
+
ran: boolean;
|
|
3
|
+
passed?: boolean;
|
|
4
|
+
/** Relative path of the test file that ran. */
|
|
5
|
+
file?: string;
|
|
6
|
+
/** Exit code from the runner. */
|
|
7
|
+
exitCode?: number;
|
|
8
|
+
stdout: string;
|
|
9
|
+
stderr: string;
|
|
10
|
+
timedOut?: boolean;
|
|
11
|
+
framework?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* run.ts — execute a generated repro test against the repo's own test
|
|
15
|
+
* framework, feeding it everything it needs (e.g. the absolute path to the
|
|
16
|
+
* ledger sandbox preload, set so the committed test stays runnable in CI).
|
|
17
|
+
*/
|
|
18
|
+
export declare function runRepro(repo: string, file: string): Promise<ReproRunResult>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { runTestFile, detectTestFramework, frameworkLabel } from "./framework.js";
|
|
4
|
+
import { preloadPath } from "./generate.js";
|
|
5
|
+
import { penPreloadPath } from "./pen.js";
|
|
6
|
+
/**
|
|
7
|
+
* run.ts — execute a generated repro test against the repo's own test
|
|
8
|
+
* framework, feeding it everything it needs (e.g. the absolute path to the
|
|
9
|
+
* ledger sandbox preload, set so the committed test stays runnable in CI).
|
|
10
|
+
*/
|
|
11
|
+
export function runRepro(repo, file) {
|
|
12
|
+
const abs = path.join(repo, file);
|
|
13
|
+
if (!fs.existsSync(abs)) {
|
|
14
|
+
return Promise.resolve({
|
|
15
|
+
ran: false,
|
|
16
|
+
stdout: "",
|
|
17
|
+
stderr: `repro test file not found: ${abs}`,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
const framework = detectTestFramework(repo);
|
|
21
|
+
const extra = {
|
|
22
|
+
PITSTOP_LEDGER_PRELOAD: preloadPath(),
|
|
23
|
+
PITSTOP_PEN_PRELOAD: penPreloadPath(),
|
|
24
|
+
};
|
|
25
|
+
return runTestFile(repo, abs, extra, 150000).then((r) => ({
|
|
26
|
+
ran: true,
|
|
27
|
+
passed: r.timedOut ? false : r.code === 0,
|
|
28
|
+
file,
|
|
29
|
+
exitCode: r.code,
|
|
30
|
+
stdout: r.stdout,
|
|
31
|
+
stderr: r.stderr,
|
|
32
|
+
timedOut: r.timedOut,
|
|
33
|
+
framework: frameworkLabel(framework),
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=run.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/repro/run.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAClF,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAe1C;;;;GAIG;AACH,MAAM,UAAU,QAAQ,CAAC,IAAY,EAAE,IAAY;IACjD,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAClC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACxB,OAAO,OAAO,CAAC,OAAO,CAAC;YACrB,GAAG,EAAE,KAAK;YACV,MAAM,EAAE,EAAE;YACV,MAAM,EAAE,8BAA8B,GAAG,EAAE;SAC5C,CAAC,CAAC;IACL,CAAC;IACD,MAAM,SAAS,GAAG,mBAAmB,CAAC,IAAI,CAAC,CAAC;IAC5C,MAAM,KAAK,GAA2B;QACpC,sBAAsB,EAAE,WAAW,EAAE;QACrC,mBAAmB,EAAE,cAAc,EAAE;KACtC,CAAC;IACF,OAAO,WAAW,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACxD,GAAG,EAAE,IAAI;QACT,MAAM,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC;QACzC,IAAI;QACJ,QAAQ,EAAE,CAAC,CAAC,IAAI;QAChB,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,SAAS,EAAE,cAAc,CAAC,SAAS,CAAC;KACrC,CAAC,CAAC,CAAC;AACN,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sandbox/proxy.ts — HTTP(S)_PROXY recording proxy for non-Node apps.
|
|
3
|
+
*
|
|
4
|
+
* The Node sandbox (preload.cjs + nock) cannot load into Go/Rust/Python/.NET
|
|
5
|
+
* processes. For those stacks OpenPitStop boots the app with HTTP_PROXY /
|
|
6
|
+
* HTTPS_PROXY pointed at this in-process proxy:
|
|
7
|
+
*
|
|
8
|
+
* - every non-loopback outbound request is recorded (JSONL, same shape the
|
|
9
|
+
* Node sandbox writes) and NEVER forwarded anywhere real;
|
|
10
|
+
* - payment-gateway hosts are answered with a mocked success receipt and the
|
|
11
|
+
* call is written to the ledger gateway log (identical LedgerChargeCall
|
|
12
|
+
* shape, so evidence.ts is unchanged);
|
|
13
|
+
* - SSRF canary hosts are answered and recorded as outbound events;
|
|
14
|
+
* - every other host gets a 502 — nothing real ever leaves the machine.
|
|
15
|
+
*
|
|
16
|
+
* HONEST CAVEATS (labeled in every report that uses this proxy):
|
|
17
|
+
* - only apps whose HTTP clients honor HTTP_PROXY are intercepted (Go,
|
|
18
|
+
* Python, Rust, .NET by default; Java/Dart do not by default);
|
|
19
|
+
* - native binaries (curl subprocesses) and raw sockets bypass the proxy
|
|
20
|
+
* entirely and are NOT observed;
|
|
21
|
+
* - HTTPS is a CONNECT tunnel: without a trusted CA we cannot terminate it,
|
|
22
|
+
* so https gateway calls are blocked (502) and recorded as blocked —
|
|
23
|
+
* a double-charge over HTTPS therefore cannot be proven via this proxy.
|
|
24
|
+
*/
|
|
25
|
+
export interface ProxyOptions {
|
|
26
|
+
/** JSONL outbound event stream (same shape as the Node sandbox's). */
|
|
27
|
+
logPath: string;
|
|
28
|
+
/** Optional control stream: writes `{event:"armed"}` and `{event:"gateway-call"}`. */
|
|
29
|
+
controlPath?: string;
|
|
30
|
+
/** Optional ledger gateway receipt stream (LedgerChargeCall JSONL). */
|
|
31
|
+
gatewayLogPath?: string;
|
|
32
|
+
/** Hosts that receive mocked gateway success responses. */
|
|
33
|
+
gatewayHosts: string[];
|
|
34
|
+
/** Host suffix treated as a canary (e.g. "pitstop.invalid"). */
|
|
35
|
+
canarySuffix: string;
|
|
36
|
+
}
|
|
37
|
+
export interface RecordingProxy {
|
|
38
|
+
port: number;
|
|
39
|
+
close(): Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
export declare function startRecordingProxy(opts: ProxyOptions): Promise<RecordingProxy>;
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
const CHARGE_RE = /(?:^|\/)(payments|charges|payment_intents|paymentintents|captures?|transactions)(\/|$|\?)/i;
|
|
4
|
+
function getHeader(headers, name) {
|
|
5
|
+
const target = name.toLowerCase();
|
|
6
|
+
for (const k of Object.keys(headers)) {
|
|
7
|
+
if (k.toLowerCase() === target)
|
|
8
|
+
return headers[k];
|
|
9
|
+
}
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
function deriveKey(method, path, headers, body) {
|
|
13
|
+
for (const name of [
|
|
14
|
+
"idempotency-key",
|
|
15
|
+
"x-idempotency-key",
|
|
16
|
+
"idempotency",
|
|
17
|
+
"x-idempotency",
|
|
18
|
+
"x-razorpay-idempotency",
|
|
19
|
+
]) {
|
|
20
|
+
const v = getHeader(headers, name);
|
|
21
|
+
if (v != null && v !== "")
|
|
22
|
+
return String(v);
|
|
23
|
+
}
|
|
24
|
+
const b = body;
|
|
25
|
+
const oid = b && (b.order_id ?? b.orderId ?? b.reference_id);
|
|
26
|
+
if (oid != null)
|
|
27
|
+
return String(oid);
|
|
28
|
+
const m = String(path).match(/\/(payments|charges|payment_intents|transactions)\/([^/]+)/i);
|
|
29
|
+
if (m)
|
|
30
|
+
return m[2];
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
function isCharge(method, path) {
|
|
34
|
+
const u = String(method).toUpperCase();
|
|
35
|
+
if (u === "GET" || u === "HEAD" || u === "OPTIONS")
|
|
36
|
+
return false;
|
|
37
|
+
return CHARGE_RE.test(String(path));
|
|
38
|
+
}
|
|
39
|
+
let seq = 0;
|
|
40
|
+
function nextId(prefix) {
|
|
41
|
+
seq += 1;
|
|
42
|
+
return (prefix || "req") + "_" + Date.now().toString(36) + "_" + seq;
|
|
43
|
+
}
|
|
44
|
+
function safeBody(raw) {
|
|
45
|
+
if (Buffer.isBuffer(raw))
|
|
46
|
+
raw = raw.toString("utf8");
|
|
47
|
+
if (typeof raw === "string") {
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(raw);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return { raw };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return raw;
|
|
56
|
+
}
|
|
57
|
+
function gatewaySuccess(host, method, path, body, requestId) {
|
|
58
|
+
const b = body;
|
|
59
|
+
const amount = b && b.amount != null ? b.amount : 1000;
|
|
60
|
+
const currency = (b && b.currency) || "INR";
|
|
61
|
+
const h = String(host).toLowerCase();
|
|
62
|
+
if (h.includes("razorpay")) {
|
|
63
|
+
return {
|
|
64
|
+
id: "pay_" + requestId,
|
|
65
|
+
entity: "payment",
|
|
66
|
+
amount,
|
|
67
|
+
currency,
|
|
68
|
+
status: /capture|settle/i.test(path) ? "captured" : "authorized",
|
|
69
|
+
order_id: (b && b.order_id) || null,
|
|
70
|
+
method: "card",
|
|
71
|
+
vpa: null,
|
|
72
|
+
created_at: Date.now() / 1000,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (h.includes("stripe")) {
|
|
76
|
+
return {
|
|
77
|
+
id: "ch_" + requestId,
|
|
78
|
+
object: "charge",
|
|
79
|
+
amount,
|
|
80
|
+
currency: String(currency).toLowerCase(),
|
|
81
|
+
status: "succeeded",
|
|
82
|
+
paid: true,
|
|
83
|
+
captured: true,
|
|
84
|
+
requestId,
|
|
85
|
+
created: Date.now() / 1000,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (h.includes("braintree")) {
|
|
89
|
+
return {
|
|
90
|
+
id: requestId,
|
|
91
|
+
object: "transaction",
|
|
92
|
+
status: "settled",
|
|
93
|
+
amount: String((Number(amount) || 0) / 100),
|
|
94
|
+
currencyIsoCode: currency,
|
|
95
|
+
createdAt: new Date().toISOString(),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
id: requestId,
|
|
100
|
+
object: "payment",
|
|
101
|
+
status: "succeeded",
|
|
102
|
+
amount,
|
|
103
|
+
currency,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
export function startRecordingProxy(opts) {
|
|
107
|
+
let seqOut = 0;
|
|
108
|
+
const append = (file, obj) => {
|
|
109
|
+
try {
|
|
110
|
+
fs.appendFileSync(file, JSON.stringify(obj) + "\n");
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
/* best-effort */
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
const logEvent = (kind, extra) => {
|
|
117
|
+
seqOut += 1;
|
|
118
|
+
append(opts.logPath, {
|
|
119
|
+
kind,
|
|
120
|
+
n: seqOut,
|
|
121
|
+
at: new Date().toISOString(),
|
|
122
|
+
...extra,
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
const control = (event) => {
|
|
126
|
+
if (opts.controlPath)
|
|
127
|
+
append(opts.controlPath, { at: new Date().toISOString(), ...event });
|
|
128
|
+
};
|
|
129
|
+
const isGateway = (host) => opts.gatewayHosts.some((g) => {
|
|
130
|
+
const gh = g.replace(/^https?:\/\//i, "").toLowerCase();
|
|
131
|
+
const h = host.toLowerCase();
|
|
132
|
+
return h === gh || h.endsWith("." + gh);
|
|
133
|
+
});
|
|
134
|
+
const isCanary = (host) => host.toLowerCase().endsWith(opts.canarySuffix.toLowerCase());
|
|
135
|
+
const isLoopback = (host) => /^(localhost|127\.\d+\.\d+\.\d+|::1|0\.0\.0\.0)$/i.test(host);
|
|
136
|
+
const server = http.createServer((req, res) => {
|
|
137
|
+
let target;
|
|
138
|
+
try {
|
|
139
|
+
target = new URL(req.url ?? "/", "http://localhost");
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
res.writeHead(400);
|
|
143
|
+
res.end("pitstop-proxy: unparseable request");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const host = target.hostname || String(req.headers.host ?? "").split(":")[0] || "unknown";
|
|
147
|
+
const path = target.pathname + target.search;
|
|
148
|
+
const method = String(req.method ?? "GET").toUpperCase();
|
|
149
|
+
// Loopback targets (e.g. clients ignoring NO_PROXY): forward on-machine.
|
|
150
|
+
if (isLoopback(host)) {
|
|
151
|
+
const proxy = http.request({
|
|
152
|
+
host: target.hostname,
|
|
153
|
+
port: target.port || (target.protocol === "https:" ? 443 : 80),
|
|
154
|
+
method,
|
|
155
|
+
path: target.pathname + target.search,
|
|
156
|
+
headers: { ...req.headers, host: target.host },
|
|
157
|
+
}, (upstream) => {
|
|
158
|
+
res.writeHead(upstream.statusCode ?? 502, upstream.headers);
|
|
159
|
+
upstream.pipe(res);
|
|
160
|
+
});
|
|
161
|
+
proxy.on("error", () => {
|
|
162
|
+
res.writeHead(502);
|
|
163
|
+
res.end("pitstop-proxy: loopback forward failed");
|
|
164
|
+
});
|
|
165
|
+
req.pipe(proxy);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (isCanary(host)) {
|
|
169
|
+
logEvent("http", { host, method, path });
|
|
170
|
+
res.writeHead(200, { "content-type": "text/plain" });
|
|
171
|
+
res.end("pitstop-canary-hit");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (isGateway(host)) {
|
|
175
|
+
const chunks = [];
|
|
176
|
+
req.on("data", (c) => chunks.push(c));
|
|
177
|
+
req.on("end", () => {
|
|
178
|
+
const body = safeBody(Buffer.concat(chunks).toString("utf8"));
|
|
179
|
+
const requestId = nextId("req");
|
|
180
|
+
const record = {
|
|
181
|
+
at: new Date().toISOString(),
|
|
182
|
+
host,
|
|
183
|
+
method,
|
|
184
|
+
path,
|
|
185
|
+
key: deriveKey(method, path, req.headers, body),
|
|
186
|
+
orderId: body?.order_id ?? null,
|
|
187
|
+
requestId,
|
|
188
|
+
requestHeaders: { ...req.headers },
|
|
189
|
+
requestBody: body,
|
|
190
|
+
charge: isCharge(method, path),
|
|
191
|
+
responseStatus: 200,
|
|
192
|
+
responseBody: gatewaySuccess(host, method, path, body, requestId),
|
|
193
|
+
};
|
|
194
|
+
if (opts.gatewayLogPath)
|
|
195
|
+
append(opts.gatewayLogPath, record);
|
|
196
|
+
control({ event: "gateway-call", charge: record.charge, key: record.key, host });
|
|
197
|
+
logEvent("http", { host, method, path });
|
|
198
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
199
|
+
res.end(JSON.stringify(record.responseBody));
|
|
200
|
+
});
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
// Unmocked host: blocked. No byte is forwarded anywhere real.
|
|
204
|
+
logEvent("blocked", { host, method, path });
|
|
205
|
+
res.writeHead(502, { "content-type": "application/json" });
|
|
206
|
+
res.end(JSON.stringify({ pitstop: "blocked-unmocked-host", host }));
|
|
207
|
+
});
|
|
208
|
+
// HTTPS goes through CONNECT; without a trusted CA we cannot terminate the
|
|
209
|
+
// tunnel, so every CONNECT is recorded and refused.
|
|
210
|
+
server.on("connect", (req, socket) => {
|
|
211
|
+
const host = String(req.url ?? "").split(":")[0] || "unknown";
|
|
212
|
+
logEvent("blocked", { host, method: "CONNECT", path: req.url ?? "" });
|
|
213
|
+
socket.write("HTTP/1.1 502 OpenPitStop Proxy Cannot Terminate TLS\r\n\r\n");
|
|
214
|
+
socket.destroy();
|
|
215
|
+
});
|
|
216
|
+
return new Promise((resolve, reject) => {
|
|
217
|
+
server.once("error", reject);
|
|
218
|
+
server.listen(0, "127.0.0.1", () => {
|
|
219
|
+
const port = server.address().port;
|
|
220
|
+
control({ event: "armed", mode: "proxy", port });
|
|
221
|
+
resolve({
|
|
222
|
+
port,
|
|
223
|
+
close: () => new Promise((r) => {
|
|
224
|
+
server.close(() => r());
|
|
225
|
+
setTimeout(() => r(), 1000);
|
|
226
|
+
}),
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
//# sourceMappingURL=proxy.js.map
|