openpitstop 1.5.0 → 1.5.2

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 (42) hide show
  1. package/README.md +27 -0
  2. package/demo/web-app-broken/.pitstop/cache/jest/haste-map-09bbba084113da9182b28b169fd19719-a1b56e2f04426a89a1864d67162ba1a1-48d4752caf3bc581fc945eb44175e230 +0 -0
  3. package/demo/web-app-broken/.pitstop/cache/jest/jest-transform-cache-09bbba084113da9182b28b169fd19719-79ef2876fae7ca75eedb2aa53dc48338/5e/config_5ea59c6b3000eadfbaecbc2e78a03802 +113 -0
  4. package/demo/web-app-broken/.pitstop/cache/jest/jest-transform-cache-09bbba084113da9182b28b169fd19719-79ef2876fae7ca75eedb2aa53dc48338/5e/config_5ea59c6b3000eadfbaecbc2e78a03802.map +1 -0
  5. package/demo/web-app-broken/.pitstop/cache/jest/perf-cache-09bbba084113da9182b28b169fd19719-da39a3ee5e6b4b0d3255bfef95601890 +1 -1
  6. package/demo/web-app-broken/.pitstop/pen/run-2026-08-18T06-12-11-576Z/app.out.log +22 -0
  7. package/demo/web-app-broken/.pitstop/pen-2026-08-18T06-12-11-787Z.json +162 -0
  8. package/demo/web-app-broken/.pitstop/pen-latest.json +162 -0
  9. package/demo/web-app-broken/.pitstop/verify-2026-08-18T06-11-59-327Z.json +73 -0
  10. package/demo/web-app-broken/PITSTOP_BADGE.svg +12 -0
  11. package/dist/commands/pen.js +29 -1
  12. package/dist/commands/pen.js.map +1 -1
  13. package/dist/commands/report.js +17 -1
  14. package/dist/commands/report.js.map +1 -1
  15. package/dist/commands/verify.d.ts +3 -0
  16. package/dist/commands/verify.js +53 -21
  17. package/dist/commands/verify.js.map +1 -1
  18. package/dist/pen/drift.d.ts +16 -0
  19. package/dist/pen/drift.js +69 -0
  20. package/dist/pen/drift.js.map +1 -0
  21. package/dist/pen/dynamic.js +130 -3
  22. package/dist/pen/dynamic.js.map +1 -1
  23. package/dist/pen/proof.js +6 -0
  24. package/dist/pen/proof.js.map +1 -1
  25. package/dist/pen/report.js +90 -2
  26. package/dist/pen/report.js.map +1 -1
  27. package/dist/pen/static.js +120 -0
  28. package/dist/pen/static.js.map +1 -1
  29. package/dist/pen/types.d.ts +32 -0
  30. package/dist/pen/types.js.map +1 -1
  31. package/dist/report/format.d.ts +16 -0
  32. package/dist/report/format.js +62 -9
  33. package/dist/report/format.js.map +1 -1
  34. package/dist/report/sarif.d.ts +29 -0
  35. package/dist/report/sarif.js +161 -0
  36. package/dist/report/sarif.js.map +1 -0
  37. package/dist/repro/pen.js +19 -0
  38. package/dist/repro/pen.js.map +1 -1
  39. package/dist/verify/honesty.d.ts +21 -0
  40. package/dist/verify/honesty.js +40 -0
  41. package/dist/verify/honesty.js.map +1 -0
  42. package/package.json +1 -1
package/README.md CHANGED
@@ -50,6 +50,33 @@ your agent.
50
50
 
51
51
  ---
52
52
 
53
+ ## OpenPitStop vs Strix — why teams pick the referee
54
+
55
+ Strix is a great *finder*. OpenPitStop is the *referee*. Both pen-test your app; the
56
+ difference is what happens after a vulnerability is found:
57
+
58
+ > **Strix finds. OpenPitStop proves.**
59
+
60
+ | | [Strix](https://github.com/usestrix/strix) | **OpenPitStop** |
61
+ |---|---|---|
62
+ | What it produces | Findings + a PoC *report* | Findings + a **failing-first regression test** (`pitstop pen --fix`) |
63
+ | Trust in the fix | One-click auto-fix PR | **Honesty Score (0–100)** + integrity gate that catches deleted tests, hardcoded passes, reverted baselines |
64
+ | How it runs | Docker + LLM key, non-deterministic | **Zero infra, deterministic, no LLM bill** — runs in any CI |
65
+ | Evidence | `strix_runs/` logs | **Tamper-evident, signed `.pitstop/` artifacts** you can audit |
66
+ | Secret exfiltration | not emphasized | **Ledger mode** proves the app doesn't phone home with your keys |
67
+ | DevSecOps | Cloud platform (paid tiers) | **Free SARIF → GitHub Security tab** + one-number `pitstop gate` |
68
+ | Proof coverage | — | **`PITSTOP_PROOF` badge**: % of findings that ship a permanent repro test |
69
+ | Continuous proof (drift) | report only — re-run and hope | **Drift gate**: every `pitstop pen` compares to the last sealed run, *proves a fix* (finding gone) and *fails the CI gate* on a new high/critical regression or a hypothesis the live attack just confirmed |
70
+ | Prove-my-fix loop | manual | **`pitstop repro <id>`** re-runs the exact attack and asserts the safe outcome — a PASS means the fix is real, a deleted repro test is flagged as a cheat |
71
+ | Bug classes covered | strong general set | **40+ classes** — race/TOCTOU, IDOR/BOLA, price-tampering, XXE, insecure deserialization, JWT alg-confusion/weak-secret, SSRF, SQL/NoSQLi, command-injection, path traversal, XSS, secrets, CORS, missing headers, rate-limit, and more |
72
+
73
+ The honest pitch: a pen-test that only reports is a list of things to argue about. A
74
+ pen-test that ships the regression test, signs the evidence, and scores the fix on a
75
+ gate your CI can block on is something you can actually ship. That's the OpenPitStop
76
+ loop — and it's the reason to choose the referee over the hacker.
77
+
78
+ ---
79
+
53
80
  ## Feature tour
54
81
 
55
82
  Every clip below is real opencode output, captured from a live agent session —
@@ -0,0 +1,113 @@
1
+ b9e26b5a20d1b5508146e54c87fb8c88
2
+ /* istanbul ignore next */
3
+ function cov_e86xmt8qo() {
4
+ var path = "D:\\OpenPitStop cli\\demo\\web-app-broken\\src\\config.js";
5
+ var hash = "b8ce36803606bd246d8daa8c315e078307e95d46";
6
+ var global = new Function("return this")();
7
+ var gcv = "__coverage__";
8
+ var coverageData = {
9
+ path: "D:\\OpenPitStop cli\\demo\\web-app-broken\\src\\config.js",
10
+ statementMap: {
11
+ "0": {
12
+ start: {
13
+ line: 4,
14
+ column: 26
15
+ },
16
+ end: {
17
+ line: 4,
18
+ column: 62
19
+ }
20
+ },
21
+ "1": {
22
+ start: {
23
+ line: 5,
24
+ column: 26
25
+ },
26
+ end: {
27
+ line: 5,
28
+ column: 62
29
+ }
30
+ },
31
+ "2": {
32
+ start: {
33
+ line: 6,
34
+ column: 19
35
+ },
36
+ end: {
37
+ line: 6,
38
+ column: 48
39
+ }
40
+ },
41
+ "3": {
42
+ start: {
43
+ line: 7,
44
+ column: 20
45
+ },
46
+ end: {
47
+ line: 7,
48
+ column: 50
49
+ }
50
+ },
51
+ "4": {
52
+ start: {
53
+ line: 9,
54
+ column: 0
55
+ },
56
+ end: {
57
+ line: 9,
58
+ column: 83
59
+ }
60
+ }
61
+ },
62
+ fnMap: {},
63
+ branchMap: {},
64
+ s: {
65
+ "0": 0,
66
+ "1": 0,
67
+ "2": 0,
68
+ "3": 0,
69
+ "4": 0
70
+ },
71
+ f: {},
72
+ b: {},
73
+ _coverageSchema: "1a1c01bbd47fc00a2c39e90264f33305004495a9",
74
+ hash: "b8ce36803606bd246d8daa8c315e078307e95d46"
75
+ };
76
+ var coverage = global[gcv] || (global[gcv] = {});
77
+ if (!coverage[path] || coverage[path].hash !== hash) {
78
+ coverage[path] = coverageData;
79
+ }
80
+ var actualCoverage = coverage[path];
81
+ {
82
+ // @ts-ignore
83
+ cov_e86xmt8qo = function () {
84
+ return actualCoverage;
85
+ };
86
+ }
87
+ return actualCoverage;
88
+ }
89
+ cov_e86xmt8qo();
90
+ // Hardcoded secrets in source — flagged by the secret analyzer even without .env.
91
+ // NOTE: values are obvious FAKE placeholders (no real AKIA/sk_live prefixes) so the
92
+ // repo passes GitHub push protection; OpenPitStop still flags them by variable name.
93
+ const AWS_ACCESS_KEY_ID =
94
+ /* istanbul ignore next */
95
+ (cov_e86xmt8qo().s[0]++, "example-aws-access-key-id-not-real");
96
+ const STRIPE_SECRET_KEY =
97
+ /* istanbul ignore next */
98
+ (cov_e86xmt8qo().s[1]++, "example-stripe-secret-key-not-real");
99
+ const JWT_SECRET =
100
+ /* istanbul ignore next */
101
+ (cov_e86xmt8qo().s[2]++, "example-jwt-secret-not-real");
102
+ const DB_PASSWORD =
103
+ /* istanbul ignore next */
104
+ (cov_e86xmt8qo().s[3]++, "example-db-password-not-real");
105
+ /* istanbul ignore next */
106
+ cov_e86xmt8qo().s[4]++;
107
+ module.exports = {
108
+ AWS_ACCESS_KEY_ID,
109
+ STRIPE_SECRET_KEY,
110
+ JWT_SECRET,
111
+ DB_PASSWORD
112
+ };
113
+ //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJjb3ZfZTg2eG10OHFvIiwiYWN0dWFsQ292ZXJhZ2UiLCJBV1NfQUNDRVNTX0tFWV9JRCIsInMiLCJTVFJJUEVfU0VDUkVUX0tFWSIsIkpXVF9TRUNSRVQiLCJEQl9QQVNTV09SRCIsIm1vZHVsZSIsImV4cG9ydHMiXSwic291cmNlcyI6WyJjb25maWcuanMiXSwic291cmNlc0NvbnRlbnQiOlsiLy8gSGFyZGNvZGVkIHNlY3JldHMgaW4gc291cmNlIOKAlCBmbGFnZ2VkIGJ5IHRoZSBzZWNyZXQgYW5hbHl6ZXIgZXZlbiB3aXRob3V0IC5lbnYuXG4vLyBOT1RFOiB2YWx1ZXMgYXJlIG9idmlvdXMgRkFLRSBwbGFjZWhvbGRlcnMgKG5vIHJlYWwgQUtJQS9za19saXZlIHByZWZpeGVzKSBzbyB0aGVcbi8vIHJlcG8gcGFzc2VzIEdpdEh1YiBwdXNoIHByb3RlY3Rpb247IE9wZW5QaXRTdG9wIHN0aWxsIGZsYWdzIHRoZW0gYnkgdmFyaWFibGUgbmFtZS5cbmNvbnN0IEFXU19BQ0NFU1NfS0VZX0lEID0gXCJleGFtcGxlLWF3cy1hY2Nlc3Mta2V5LWlkLW5vdC1yZWFsXCI7XG5jb25zdCBTVFJJUEVfU0VDUkVUX0tFWSA9IFwiZXhhbXBsZS1zdHJpcGUtc2VjcmV0LWtleS1ub3QtcmVhbFwiO1xuY29uc3QgSldUX1NFQ1JFVCA9IFwiZXhhbXBsZS1qd3Qtc2VjcmV0LW5vdC1yZWFsXCI7XG5jb25zdCBEQl9QQVNTV09SRCA9IFwiZXhhbXBsZS1kYi1wYXNzd29yZC1ub3QtcmVhbFwiO1xuXG5tb2R1bGUuZXhwb3J0cyA9IHsgQVdTX0FDQ0VTU19LRVlfSUQsIFNUUklQRV9TRUNSRVRfS0VZLCBKV1RfU0VDUkVULCBEQl9QQVNTV09SRCB9O1xuIl0sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztJQWVZO0lBQUFBLGFBQUEsWUFBQUEsQ0FBQTtNQUFBLE9BQUFDLGNBQUE7SUFBQTtFQUFBO0VBQUEsT0FBQUEsY0FBQTtBQUFBO0FBQUFELGFBQUE7QUFmWjtBQUNBO0FBQ0E7QUFDQSxNQUFNRSxpQkFBaUI7QUFBQTtBQUFBLENBQUFGLGFBQUEsR0FBQUcsQ0FBQSxPQUFHLG9DQUFvQztBQUM5RCxNQUFNQyxpQkFBaUI7QUFBQTtBQUFBLENBQUFKLGFBQUEsR0FBQUcsQ0FBQSxPQUFHLG9DQUFvQztBQUM5RCxNQUFNRSxVQUFVO0FBQUE7QUFBQSxDQUFBTCxhQUFBLEdBQUFHLENBQUEsT0FBRyw2QkFBNkI7QUFDaEQsTUFBTUcsV0FBVztBQUFBO0FBQUEsQ0FBQU4sYUFBQSxHQUFBRyxDQUFBLE9BQUcsOEJBQThCO0FBQUM7QUFBQUgsYUFBQSxHQUFBRyxDQUFBO0FBRW5ESSxNQUFNLENBQUNDLE9BQU8sR0FBRztFQUFFTixpQkFBaUI7RUFBRUUsaUJBQWlCO0VBQUVDLFVBQVU7RUFBRUM7QUFBWSxDQUFDIiwiaWdub3JlTGlzdCI6W119
@@ -0,0 +1 @@
1
+ {"version":3,"names":["cov_e86xmt8qo","actualCoverage","AWS_ACCESS_KEY_ID","s","STRIPE_SECRET_KEY","JWT_SECRET","DB_PASSWORD","module","exports"],"sources":["config.js"],"sourcesContent":["// Hardcoded secrets in source — flagged by the secret analyzer even without .env.\n// NOTE: values are obvious FAKE placeholders (no real AKIA/sk_live prefixes) so the\n// repo passes GitHub push protection; OpenPitStop still flags them by variable name.\nconst AWS_ACCESS_KEY_ID = \"example-aws-access-key-id-not-real\";\nconst STRIPE_SECRET_KEY = \"example-stripe-secret-key-not-real\";\nconst JWT_SECRET = \"example-jwt-secret-not-real\";\nconst DB_PASSWORD = \"example-db-password-not-real\";\n\nmodule.exports = { AWS_ACCESS_KEY_ID, STRIPE_SECRET_KEY, JWT_SECRET, DB_PASSWORD };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAeY;IAAAA,aAAA,YAAAA,CAAA;MAAA,OAAAC,cAAA;IAAA;EAAA;EAAA,OAAAA,cAAA;AAAA;AAAAD,aAAA;AAfZ;AACA;AACA;AACA,MAAME,iBAAiB;AAAA;AAAA,CAAAF,aAAA,GAAAG,CAAA,OAAG,oCAAoC;AAC9D,MAAMC,iBAAiB;AAAA;AAAA,CAAAJ,aAAA,GAAAG,CAAA,OAAG,oCAAoC;AAC9D,MAAME,UAAU;AAAA;AAAA,CAAAL,aAAA,GAAAG,CAAA,OAAG,6BAA6B;AAChD,MAAMG,WAAW;AAAA;AAAA,CAAAN,aAAA,GAAAG,CAAA,OAAG,8BAA8B;AAAC;AAAAH,aAAA,GAAAG,CAAA;AAEnDI,MAAM,CAACC,OAAO,GAAG;EAAEN,iBAAiB;EAAEE,iBAAiB;EAAEC,UAAU;EAAEC;AAAY,CAAC","ignoreList":[]}
@@ -1 +1 @@
1
- {"D:\\OpenPitStop cli\\demo\\web-app-broken\\test\\app.test.js":[0,2024]}
1
+ {"D:\\OpenPitStop cli\\demo\\web-app-broken\\test\\app.test.js":[0,1820]}
@@ -0,0 +1,22 @@
1
+ node:internal/modules/cjs/loader:1252
2
+ throw err;
3
+ ^
4
+
5
+ Error: Cannot find module 'D:\OpenPitStop'
6
+ Require stack:
7
+ - internal/preload
8
+ at Function._resolveFilename (node:internal/modules/cjs/loader:1249:15)
9
+ at Function._load (node:internal/modules/cjs/loader:1075:27)
10
+ at TracingChannel.traceSync (node:diagnostics_channel:322:14)
11
+ at wrapModuleLoad (node:internal/modules/cjs/loader:219:24)
12
+ at Module.require (node:internal/modules/cjs/loader:1340:12)
13
+ at node:internal/modules/cjs/loader:1843:12
14
+ at loadPreloadModules (node:internal/process/pre_execution:727:5)
15
+ at setupUserModules (node:internal/process/pre_execution:206:5)
16
+ at prepareExecution (node:internal/process/pre_execution:159:5)
17
+ at prepareMainThreadExecution (node:internal/process/pre_execution:54:10) {
18
+ code: 'MODULE_NOT_FOUND',
19
+ requireStack: [ 'internal/preload' ]
20
+ }
21
+
22
+ Node.js v22.12.0
@@ -0,0 +1,162 @@
1
+ {
2
+ "timestamp": "2026-08-18T06:12:11.785Z",
3
+ "repo": "D:\\OpenPitStop cli\\demo\\web-app-broken",
4
+ "mode": "pen",
5
+ "staticEnabled": true,
6
+ "dynamicEnabled": true,
7
+ "dynamic": {
8
+ "status": "aborted",
9
+ "note": "app did not come up within 25s (start: \"node src/server.js\")",
10
+ "routesProbed": 0,
11
+ "attacks": 0,
12
+ "bootMs": 198,
13
+ "durationMs": 209,
14
+ "outboundEvents": 0
15
+ },
16
+ "staticProof": {
17
+ "proven": 0,
18
+ "indicated": 0,
19
+ "unproven": 0,
20
+ "notTested": 8
21
+ },
22
+ "packages": [
23
+ "express",
24
+ "lodash",
25
+ "jsonwebtoken",
26
+ "mysql2",
27
+ "cors",
28
+ "jest"
29
+ ],
30
+ "findings": [
31
+ {
32
+ "id": "pen-62be9b7b",
33
+ "source": "pen-static",
34
+ "type": "sql-injection",
35
+ "severity": "critical",
36
+ "confidence": "indicated",
37
+ "title": "SQL built by string interpolation",
38
+ "description": "A query string is assembled by concatenating or interpolating variables into SQL. Any route feeding these variables becomes a live injection point. (src/server.js:24)",
39
+ "file": "D:\\OpenPitStop cli\\demo\\web-app-broken\\src\\server.js",
40
+ "line": 24,
41
+ "repro": "open src/server.js:24",
42
+ "fix": "Use parameterized queries (?, $1 placeholders) or an ORM. Never interpolate values into SQL text.",
43
+ "runtimeProof": "not-tested",
44
+ "runtimeNote": "no runtime verification: the dynamic phase did not run (app did not come up within 25s (start: \"node src/server.js\"))"
45
+ },
46
+ {
47
+ "id": "pen-654abdce",
48
+ "source": "pen-static",
49
+ "type": "sql-injection",
50
+ "severity": "critical",
51
+ "confidence": "indicated",
52
+ "title": "SQL built by string interpolation",
53
+ "description": "A query string is assembled by concatenating or interpolating variables into SQL. Any route feeding these variables becomes a live injection point. (src/users.js:16)",
54
+ "file": "D:\\OpenPitStop cli\\demo\\web-app-broken\\src\\users.js",
55
+ "line": 16,
56
+ "repro": "open src/users.js:16",
57
+ "fix": "Use parameterized queries (?, $1 placeholders) or an ORM. Never interpolate values into SQL text.",
58
+ "runtimeProof": "not-tested",
59
+ "runtimeNote": "no runtime verification: the dynamic phase did not run (app did not come up within 25s (start: \"node src/server.js\"))"
60
+ },
61
+ {
62
+ "id": "pen-81855543",
63
+ "source": "pen-static",
64
+ "type": "command-injection",
65
+ "severity": "critical",
66
+ "confidence": "indicated",
67
+ "title": "child_process called with request-derived input",
68
+ "description": "exec/spawn/execFile is called with a value that can trace back to request data (query, params, body). If the shell interprets it, that is remote code execution. (src/server.js:24)",
69
+ "file": "D:\\OpenPitStop cli\\demo\\web-app-broken\\src\\server.js",
70
+ "line": 24,
71
+ "repro": "open src/server.js:24",
72
+ "fix": "Do not shell out with user input. If unavoidable: execFile with a fixed command, no shell, and validate input against a strict allowlist (e.g. /^[a-z0-9-]{1,64}$/).",
73
+ "runtimeProof": "not-tested",
74
+ "runtimeNote": "no runtime verification: the dynamic phase did not run (app did not come up within 25s (start: \"node src/server.js\"))"
75
+ },
76
+ {
77
+ "id": "pen-6d14fd91",
78
+ "source": "pen-static",
79
+ "type": "arbitrary-code-execution",
80
+ "severity": "high",
81
+ "confidence": "heuristic",
82
+ "title": "eval / new Function in application code",
83
+ "description": "`eval` or `new Function` turns attacker-influenced strings into executable code. It is never needed for this; replace with a lookup table or JSON.parse. (src/server.js:59)",
84
+ "file": "D:\\OpenPitStop cli\\demo\\web-app-broken\\src\\server.js",
85
+ "line": 59,
86
+ "repro": "open src/server.js:59",
87
+ "fix": "Remove eval/new Function. If a template engine is genuinely required, use a sandboxed one (e.g. a compiled template with no global access).",
88
+ "runtimeProof": "not-tested",
89
+ "runtimeNote": "no runtime verification: the dynamic phase did not run (app did not come up within 25s (start: \"node src/server.js\"))"
90
+ },
91
+ {
92
+ "id": "pen-e02320ba",
93
+ "source": "pen-static",
94
+ "type": "cookie-without-httponly",
95
+ "severity": "medium",
96
+ "confidence": "indicated",
97
+ "title": "cookie set without httpOnly",
98
+ "description": "A session/security cookie is being set without `httpOnly` (and likely without `secure`), so an XSS in any page can steal it via document.cookie.",
99
+ "file": "D:\\OpenPitStop cli\\demo\\web-app-broken\\src\\auth.js",
100
+ "line": 31,
101
+ "fix": "Set `{ httpOnly: true, secure: true, sameSite: \"strict\" }` on every session/auth cookie.",
102
+ "runtimeProof": "not-tested",
103
+ "runtimeNote": "no runtime verification: the dynamic phase did not run (app did not come up within 25s (start: \"node src/server.js\"))"
104
+ },
105
+ {
106
+ "id": "pen-c3f2b7cf",
107
+ "source": "pen-static",
108
+ "type": "insecure-cors",
109
+ "severity": "high",
110
+ "confidence": "indicated",
111
+ "title": "CORS allows any origin",
112
+ "description": "CORS is wide open — either a bare `cors()` (defaults to origin: * with credentials off) or an explicit `origin: \"*\"`. Any website can read the responses of this endpoint from a victim's browser.",
113
+ "file": "D:\\OpenPitStop cli\\demo\\web-app-broken\\src\\server.js",
114
+ "line": 13,
115
+ "fix": "Restrict origins to an explicit allowlist: `cors({ origin: [/^https?:\\/\\/(localhost|127\\.0\\.0\\.1)(:\\d+)?$/, ...] })` — or better, list only your real production origins and never combine `origin: *` with credentials.",
116
+ "runtimeProof": "not-tested",
117
+ "runtimeNote": "no runtime verification: the dynamic phase did not run (app did not come up within 25s (start: \"node src/server.js\"))"
118
+ },
119
+ {
120
+ "id": "pen-c55906fe",
121
+ "source": "pen-static",
122
+ "type": "missing-security-headers",
123
+ "severity": "medium",
124
+ "confidence": "indicated",
125
+ "title": "no helmet / security headers middleware",
126
+ "description": "An Express app without helmet() (or manual CSP / X-Frame-Options / X-Content-Type-Options headers). Clickjacking, MIME sniffing and mixed-content protections are all off.",
127
+ "file": "D:\\OpenPitStop cli\\demo\\web-app-broken\\src\\server.js",
128
+ "line": 10,
129
+ "fix": "Install helmet and `app.use(helmet());` after app creation (deterministic patch generated by `pitstop pen --fix` when helmet is already installed).",
130
+ "runtimeProof": "not-tested",
131
+ "runtimeNote": "no runtime verification: the dynamic phase did not run (app did not come up within 25s (start: \"node src/server.js\"))"
132
+ },
133
+ {
134
+ "id": "pen-295619f9",
135
+ "source": "pen-static",
136
+ "type": "no-rate-limit",
137
+ "severity": "medium",
138
+ "confidence": "heuristic",
139
+ "title": "no rate limiting on authentication paths",
140
+ "description": "express-rate-limit is not installed and login-ish routes exist — credential stuffing / brute force is unthrottled. The dynamic phase verifies this live. Login-ish routes: POST /login.",
141
+ "fix": "Add express-rate-limit and apply a strict limiter to /login /verify /otp /token routes (e.g. 5 requests / 15 min per IP).",
142
+ "runtimeProof": "not-tested",
143
+ "runtimeNote": "no runtime verification: the dynamic phase did not run (app did not come up within 25s (start: \"node src/server.js\"))"
144
+ }
145
+ ],
146
+ "summary": {
147
+ "critical": 3,
148
+ "high": 2,
149
+ "medium": 3,
150
+ "low": 0,
151
+ "info": 0,
152
+ "proven": 0,
153
+ "indicated": 6,
154
+ "heuristic": 2
155
+ },
156
+ "evidence": {
157
+ "scheme": "pitstop-canonical-sha256-v1",
158
+ "digest": "e4ce8e1e6534ee152c199374a4de6ca6c2dec036c3a487554c215a9c0fedf2bd",
159
+ "of": "pitstop pen result for D:\\OpenPitStop cli\\demo\\web-app-broken",
160
+ "signedAt": "2026-08-18T06:12:11.786Z"
161
+ }
162
+ }
@@ -0,0 +1,162 @@
1
+ {
2
+ "timestamp": "2026-08-18T06:12:11.785Z",
3
+ "repo": "D:\\OpenPitStop cli\\demo\\web-app-broken",
4
+ "mode": "pen",
5
+ "staticEnabled": true,
6
+ "dynamicEnabled": true,
7
+ "dynamic": {
8
+ "status": "aborted",
9
+ "note": "app did not come up within 25s (start: \"node src/server.js\")",
10
+ "routesProbed": 0,
11
+ "attacks": 0,
12
+ "bootMs": 198,
13
+ "durationMs": 209,
14
+ "outboundEvents": 0
15
+ },
16
+ "staticProof": {
17
+ "proven": 0,
18
+ "indicated": 0,
19
+ "unproven": 0,
20
+ "notTested": 8
21
+ },
22
+ "packages": [
23
+ "express",
24
+ "lodash",
25
+ "jsonwebtoken",
26
+ "mysql2",
27
+ "cors",
28
+ "jest"
29
+ ],
30
+ "findings": [
31
+ {
32
+ "id": "pen-62be9b7b",
33
+ "source": "pen-static",
34
+ "type": "sql-injection",
35
+ "severity": "critical",
36
+ "confidence": "indicated",
37
+ "title": "SQL built by string interpolation",
38
+ "description": "A query string is assembled by concatenating or interpolating variables into SQL. Any route feeding these variables becomes a live injection point. (src/server.js:24)",
39
+ "file": "D:\\OpenPitStop cli\\demo\\web-app-broken\\src\\server.js",
40
+ "line": 24,
41
+ "repro": "open src/server.js:24",
42
+ "fix": "Use parameterized queries (?, $1 placeholders) or an ORM. Never interpolate values into SQL text.",
43
+ "runtimeProof": "not-tested",
44
+ "runtimeNote": "no runtime verification: the dynamic phase did not run (app did not come up within 25s (start: \"node src/server.js\"))"
45
+ },
46
+ {
47
+ "id": "pen-654abdce",
48
+ "source": "pen-static",
49
+ "type": "sql-injection",
50
+ "severity": "critical",
51
+ "confidence": "indicated",
52
+ "title": "SQL built by string interpolation",
53
+ "description": "A query string is assembled by concatenating or interpolating variables into SQL. Any route feeding these variables becomes a live injection point. (src/users.js:16)",
54
+ "file": "D:\\OpenPitStop cli\\demo\\web-app-broken\\src\\users.js",
55
+ "line": 16,
56
+ "repro": "open src/users.js:16",
57
+ "fix": "Use parameterized queries (?, $1 placeholders) or an ORM. Never interpolate values into SQL text.",
58
+ "runtimeProof": "not-tested",
59
+ "runtimeNote": "no runtime verification: the dynamic phase did not run (app did not come up within 25s (start: \"node src/server.js\"))"
60
+ },
61
+ {
62
+ "id": "pen-81855543",
63
+ "source": "pen-static",
64
+ "type": "command-injection",
65
+ "severity": "critical",
66
+ "confidence": "indicated",
67
+ "title": "child_process called with request-derived input",
68
+ "description": "exec/spawn/execFile is called with a value that can trace back to request data (query, params, body). If the shell interprets it, that is remote code execution. (src/server.js:24)",
69
+ "file": "D:\\OpenPitStop cli\\demo\\web-app-broken\\src\\server.js",
70
+ "line": 24,
71
+ "repro": "open src/server.js:24",
72
+ "fix": "Do not shell out with user input. If unavoidable: execFile with a fixed command, no shell, and validate input against a strict allowlist (e.g. /^[a-z0-9-]{1,64}$/).",
73
+ "runtimeProof": "not-tested",
74
+ "runtimeNote": "no runtime verification: the dynamic phase did not run (app did not come up within 25s (start: \"node src/server.js\"))"
75
+ },
76
+ {
77
+ "id": "pen-6d14fd91",
78
+ "source": "pen-static",
79
+ "type": "arbitrary-code-execution",
80
+ "severity": "high",
81
+ "confidence": "heuristic",
82
+ "title": "eval / new Function in application code",
83
+ "description": "`eval` or `new Function` turns attacker-influenced strings into executable code. It is never needed for this; replace with a lookup table or JSON.parse. (src/server.js:59)",
84
+ "file": "D:\\OpenPitStop cli\\demo\\web-app-broken\\src\\server.js",
85
+ "line": 59,
86
+ "repro": "open src/server.js:59",
87
+ "fix": "Remove eval/new Function. If a template engine is genuinely required, use a sandboxed one (e.g. a compiled template with no global access).",
88
+ "runtimeProof": "not-tested",
89
+ "runtimeNote": "no runtime verification: the dynamic phase did not run (app did not come up within 25s (start: \"node src/server.js\"))"
90
+ },
91
+ {
92
+ "id": "pen-e02320ba",
93
+ "source": "pen-static",
94
+ "type": "cookie-without-httponly",
95
+ "severity": "medium",
96
+ "confidence": "indicated",
97
+ "title": "cookie set without httpOnly",
98
+ "description": "A session/security cookie is being set without `httpOnly` (and likely without `secure`), so an XSS in any page can steal it via document.cookie.",
99
+ "file": "D:\\OpenPitStop cli\\demo\\web-app-broken\\src\\auth.js",
100
+ "line": 31,
101
+ "fix": "Set `{ httpOnly: true, secure: true, sameSite: \"strict\" }` on every session/auth cookie.",
102
+ "runtimeProof": "not-tested",
103
+ "runtimeNote": "no runtime verification: the dynamic phase did not run (app did not come up within 25s (start: \"node src/server.js\"))"
104
+ },
105
+ {
106
+ "id": "pen-c3f2b7cf",
107
+ "source": "pen-static",
108
+ "type": "insecure-cors",
109
+ "severity": "high",
110
+ "confidence": "indicated",
111
+ "title": "CORS allows any origin",
112
+ "description": "CORS is wide open — either a bare `cors()` (defaults to origin: * with credentials off) or an explicit `origin: \"*\"`. Any website can read the responses of this endpoint from a victim's browser.",
113
+ "file": "D:\\OpenPitStop cli\\demo\\web-app-broken\\src\\server.js",
114
+ "line": 13,
115
+ "fix": "Restrict origins to an explicit allowlist: `cors({ origin: [/^https?:\\/\\/(localhost|127\\.0\\.0\\.1)(:\\d+)?$/, ...] })` — or better, list only your real production origins and never combine `origin: *` with credentials.",
116
+ "runtimeProof": "not-tested",
117
+ "runtimeNote": "no runtime verification: the dynamic phase did not run (app did not come up within 25s (start: \"node src/server.js\"))"
118
+ },
119
+ {
120
+ "id": "pen-c55906fe",
121
+ "source": "pen-static",
122
+ "type": "missing-security-headers",
123
+ "severity": "medium",
124
+ "confidence": "indicated",
125
+ "title": "no helmet / security headers middleware",
126
+ "description": "An Express app without helmet() (or manual CSP / X-Frame-Options / X-Content-Type-Options headers). Clickjacking, MIME sniffing and mixed-content protections are all off.",
127
+ "file": "D:\\OpenPitStop cli\\demo\\web-app-broken\\src\\server.js",
128
+ "line": 10,
129
+ "fix": "Install helmet and `app.use(helmet());` after app creation (deterministic patch generated by `pitstop pen --fix` when helmet is already installed).",
130
+ "runtimeProof": "not-tested",
131
+ "runtimeNote": "no runtime verification: the dynamic phase did not run (app did not come up within 25s (start: \"node src/server.js\"))"
132
+ },
133
+ {
134
+ "id": "pen-295619f9",
135
+ "source": "pen-static",
136
+ "type": "no-rate-limit",
137
+ "severity": "medium",
138
+ "confidence": "heuristic",
139
+ "title": "no rate limiting on authentication paths",
140
+ "description": "express-rate-limit is not installed and login-ish routes exist — credential stuffing / brute force is unthrottled. The dynamic phase verifies this live. Login-ish routes: POST /login.",
141
+ "fix": "Add express-rate-limit and apply a strict limiter to /login /verify /otp /token routes (e.g. 5 requests / 15 min per IP).",
142
+ "runtimeProof": "not-tested",
143
+ "runtimeNote": "no runtime verification: the dynamic phase did not run (app did not come up within 25s (start: \"node src/server.js\"))"
144
+ }
145
+ ],
146
+ "summary": {
147
+ "critical": 3,
148
+ "high": 2,
149
+ "medium": 3,
150
+ "low": 0,
151
+ "info": 0,
152
+ "proven": 0,
153
+ "indicated": 6,
154
+ "heuristic": 2
155
+ },
156
+ "evidence": {
157
+ "scheme": "pitstop-canonical-sha256-v1",
158
+ "digest": "e4ce8e1e6534ee152c199374a4de6ca6c2dec036c3a487554c215a9c0fedf2bd",
159
+ "of": "pitstop pen result for D:\\OpenPitStop cli\\demo\\web-app-broken",
160
+ "signedAt": "2026-08-18T06:12:11.786Z"
161
+ }
162
+ }
@@ -0,0 +1,73 @@
1
+ {
2
+ "timestamp": "2026-08-18T06:11:59.328Z",
3
+ "repo": "D:\\OpenPitStop cli\\demo\\web-app-broken",
4
+ "baselineTimestamp": "2026-08-18T04:07:42.300Z",
5
+ "risk": "Low",
6
+ "blocked": false,
7
+ "stale": true,
8
+ "staleNote": "baseline is STALE — .env changed 2026-08-18T05:21:00.275Z, after the baseline scan (2026-08-18T04:07:42.300Z). Score delta vs baseline is approximate; re-run `pitstop scan` for a fresh baseline.",
9
+ "evidence": {
10
+ "scheme": "pitstop-canonical-sha256-v1",
11
+ "digest": "297e93ba61700be8a7614c9abb32a888cc07a59008a57e60cc729efd444feb29",
12
+ "of": "pitstop verify result for D:\\OpenPitStop cli\\demo\\web-app-broken",
13
+ "signedAt": "2026-08-18T06:11:59.328Z"
14
+ },
15
+ "status": "OK",
16
+ "exitCode": 0,
17
+ "integrity": {
18
+ "verdict": "CLEAN",
19
+ "findings": [],
20
+ "summary": {
21
+ "confirmed": 0,
22
+ "suspicious": 0,
23
+ "total": 0
24
+ }
25
+ },
26
+ "deltas": {
27
+ "passed": 0,
28
+ "failed": 0,
29
+ "durationMs": -2912,
30
+ "coverage": 0,
31
+ "buildTimeMs": 0,
32
+ "bundleSizeBytes": 0,
33
+ "security": -2,
34
+ "duplication": 0
35
+ },
36
+ "baseline": {
37
+ "tests": {
38
+ "total": 5,
39
+ "passed": 0,
40
+ "failed": 5,
41
+ "durationMs": 6283,
42
+ "coverage": 62.02
43
+ },
44
+ "perf": {},
45
+ "securityCount": 45,
46
+ "duplicationCount": 0
47
+ },
48
+ "current": {
49
+ "tests": {
50
+ "total": 5,
51
+ "passed": 0,
52
+ "failed": 5,
53
+ "durationMs": 3371,
54
+ "coverage": 62.02
55
+ },
56
+ "perf": {},
57
+ "securityCount": 43,
58
+ "duplicationCount": 0
59
+ },
60
+ "score": {
61
+ "baseline": 35,
62
+ "current": 35,
63
+ "delta": 0,
64
+ "grade": "F"
65
+ },
66
+ "honesty": {
67
+ "score": 90,
68
+ "rating": "TRUSTWORTHY",
69
+ "reasons": [
70
+ "baseline is stale — the score delta is approximate, not exact"
71
+ ]
72
+ }
73
+ }
@@ -0,0 +1,12 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="139" height="20" role="img" aria-label="OpenPitStop score 35/100 (F)">
2
+ <linearGradient id="g" x1="0" y1="0" x2="1" y2="0">
3
+ <stop offset="0%" stop-color="#5c5c5c" stop-opacity="0.95"/>
4
+ <stop offset="100%" stop-color="#3a3a3a" stop-opacity="0.95"/>
5
+ </linearGradient>
6
+ <rect width="139" height="20" rx="3" fill="url(#g)"/>
7
+ <rect x="66" width="73" height="20" fill="#E53935"/>
8
+ <g fill="#fff" font-family="Verdana,DejaVu Sans,sans-serif" font-size="11" text-anchor="middle">
9
+ <text x="33" y="14">PITSTOP</text>
10
+ <text x="102.5" y="14">35/100 F</text>
11
+ </g>
12
+ </svg>
@@ -5,7 +5,8 @@ import fs from "node:fs";
5
5
  import { analyzeStatic } from "../pen/static.js";
6
6
  import { runDynamic } from "../pen/dynamic.js";
7
7
  import { applyStaticProof } from "../pen/proof.js";
8
- import { persistPen } from "../pen/store.js";
8
+ import { loadPenLatest, persistPen } from "../pen/store.js";
9
+ import { computePenDrift } from "../pen/drift.js";
9
10
  import { runFixes } from "../pen/fix.js";
10
11
  import { renderPenBox, renderPenMarkdown, renderPenHtml } from "../pen/report.js";
11
12
  import { summarizeFindings } from "../pen/types.js";
@@ -20,6 +21,8 @@ export async function runPen(repo, opts) {
20
21
  const staticStart = Date.now();
21
22
  const staticOutcome = analyzeStatic(repo);
22
23
  const staticMs = Date.now() - staticStart;
24
+ // Previous sealed run, for drift detection (read before we overwrite it).
25
+ const prev = loadPenLatest(repo);
23
26
  const staticFindings = [...staticOutcome.findings];
24
27
  let dynamicEnabled = !opts.staticOnly;
25
28
  let staticProof;
@@ -111,6 +114,10 @@ export async function runPen(repo, opts) {
111
114
  else {
112
115
  findings.push(...staticFindings);
113
116
  }
117
+ // Drift vs the previously sealed run — only when the two runs are comparable
118
+ // (same dynamic phase status), so an aborted run never fabricates resolutions.
119
+ const comparable = prev && prev.dynamic && dynamic.status === prev.dynamic.status;
120
+ const drift = comparable ? computePenDrift(prev.findings, findings, prev.timestamp) : undefined;
114
121
  return {
115
122
  timestamp: new Date().toISOString(),
116
123
  repo,
@@ -119,6 +126,7 @@ export async function runPen(repo, opts) {
119
126
  dynamicEnabled,
120
127
  dynamic,
121
128
  staticProof,
129
+ drift,
122
130
  packages: staticOutcome.packages,
123
131
  findings,
124
132
  summary: summarizeFindings(findings),
@@ -199,6 +207,26 @@ export const pen = new Command("pen")
199
207
  else if (result.dynamicEnabled && result.dynamic.status === "aborted" && result.findings.length === 0) {
200
208
  process.exitCode = 2;
201
209
  }
210
+ // A regression (new high/critical, or a hypothesis the live attack just
211
+ // confirmed) since the last sealed run always fails the gate.
212
+ if (result.drift?.regression)
213
+ process.exitCode = 1;
214
+ if (result.drift) {
215
+ const d = result.drift;
216
+ const parts = [];
217
+ if (d.new.length)
218
+ parts.push(`${d.new.length} NEW${d.regression ? " (regression)" : ""}`);
219
+ if (d.resolved.length)
220
+ parts.push(`${d.resolved.length} resolved`);
221
+ if (d.escalations.length)
222
+ parts.push(`${d.escalations.length} escalated`);
223
+ if (parts.length) {
224
+ console.log(chalk.dim(`Drift vs last run${d.baselineTimestamp ? ` (${d.baselineTimestamp})` : ""}: ${parts.join(" · ")}`));
225
+ }
226
+ else {
227
+ console.log(chalk.dim("Drift vs last run: no change"));
228
+ }
229
+ }
202
230
  });
203
231
  export default pen;
204
232
  //# sourceMappingURL=pen.js.map