openpitstop 1.1.0 → 1.3.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.
@@ -0,0 +1,595 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { lineOf } from "./util.js";
4
+ /**
5
+ * Deterministic, fully-offline static security scan.
6
+ *
7
+ * Every finding here is labeled `[indicated]`: static analysis can point at
8
+ * the line and the class of bug, but it cannot *prove* an exploit — the
9
+ * dynamic phases (`pen`, `ledger`) exist for that. What it CAN do is never
10
+ * miss a known pattern, and every finding carries a concrete `fix`, so the
11
+ * report is an identify-and-solve checklist, not a scare.
12
+ *
13
+ * Covered (see docs/security.md):
14
+ * - SQL injection (JS + Python)
15
+ * - Command injection, path traversal, SSRF, XSS
16
+ * - Secret management: known credential formats, generic secret
17
+ * assignments, committed `.env` files
18
+ * - Authentication: weak password compare, plaintext storage, missing
19
+ * hashing, predictable tokens, inline JWT secrets, missing rate limits
20
+ * - Authorization: routes without authn/authz checks, admin routes
21
+ * without role checks
22
+ * - Input validation: file uploads without limits, unvalidated money
23
+ * fields, eval/Function sinks, prototype pollution
24
+ * - Config & transport: CORS+credentials, missing security headers,
25
+ * cleartext HTTP, insecure cookies, CSRF exposure, stack leaks,
26
+ * sensitive logging, weak hashing
27
+ */
28
+ const MAX_FILES = 600;
29
+ const MAX_BYTES_PER_FILE = 512 * 1024;
30
+ const MAX_FINDINGS = 40;
31
+ const SKIP_DIRS = new Set([
32
+ "node_modules",
33
+ "dist",
34
+ "build",
35
+ ".git",
36
+ ".pitstop",
37
+ "coverage",
38
+ ".next",
39
+ ".nuxt",
40
+ ".venv",
41
+ "venv",
42
+ "__pycache__",
43
+ ".cache",
44
+ "vendor",
45
+ "target",
46
+ "out",
47
+ "demo-repo",
48
+ "templates",
49
+ ]);
50
+ const CODE_EXTS = new Set([
51
+ ".js",
52
+ ".jsx",
53
+ ".ts",
54
+ ".tsx",
55
+ ".mjs",
56
+ ".cjs",
57
+ ".py",
58
+ ".go",
59
+ ".rb",
60
+ ".php",
61
+ ".java",
62
+ ".cs",
63
+ ".html",
64
+ ".vue",
65
+ ".svelte",
66
+ ]);
67
+ /** Paths that a sane app treats as needing protection. */
68
+ const PROTECTED_PATH = "(?:admin|account|profile|settings|dashboard|orders?|checkout|payment|charge|billing|subscription|private|internal|api(?:/|)|users?|sessions?|transactions?|transfer|refund)";
69
+ /** Unambiguous user-controlled data sources (no bare `body`/`input`/`data`). */
70
+ const USERISH = "req\\.(?:query|params|body|headers|cookies|files)|userInput|userUrl|targetUrl|webhookUrl|callbackUrl|redirectUrl|uploadedFile|fileName|filePath|pathVar|target";
71
+ /**
72
+ * Rules are tried per file, in order. Match offsets are mapped to line
73
+ * numbers; matches are deduped by (category, file, line) so overlapping
74
+ * patterns never double-report the same line.
75
+ */
76
+ export const STATIC_SECURITY_RULES = [
77
+ /* ----------------------- SQL injection ----------------------- */
78
+ {
79
+ category: "sql-injection",
80
+ severity: "high",
81
+ re: /\b(?:db|client|pool|connection|mysql|pg|sqlite|sqlite3)\.(?:query|execute|exec|run|all|get)\s*\([`"']?[\s\S]{0,140}?(?:\$\{[\s\S]{0,40}?\}|["'`]\s*\+)/g,
82
+ describe: () => "SQL built by string concatenation or interpolation — user input reaches the query text",
83
+ fix: 'use parameterized queries, never string-built SQL: db.query("SELECT * FROM users WHERE id = $1", [id])',
84
+ },
85
+ {
86
+ category: "sql-injection",
87
+ severity: "high",
88
+ re: /\b(?:whereRaw|orderByRaw|groupByRaw|joinRaw|havingRaw|fromRaw|raw|literal)\s*\(\s*[`"']?[\s\S]{0,140}?(?:\$\{[\s\S]{0,40}?\}|["'`]\s*\+)/g,
89
+ describe: () => "raw SQL builder receives interpolated/concatenated input — injection into an ORM raw call",
90
+ fix: 'raw SQL builders must never receive user input: use the ORM\'s parameterized API: .where("id", id) instead of .whereRaw(`id = ${id}`)',
91
+ },
92
+ {
93
+ category: "sql-injection",
94
+ severity: "high",
95
+ re: /\$\s*where\s*:/g,
96
+ describe: () => "Mongoose $where query — runs JS on the DB server with injected conditions",
97
+ fix: "never use $where: it evaluates strings server-side. Use $eq/$in with validated values: { _id: { $eq: id } }",
98
+ },
99
+ {
100
+ category: "sql-injection",
101
+ severity: "high",
102
+ re: /\bqueryRawUnsafe\s*\(/g,
103
+ describe: () => "Prisma $queryRawUnsafe — explicitly marked unsafe by Prisma, intended for constant SQL only",
104
+ fix: "use Prisma's parameterized queryRaw with $1 placeholders, or better the typed findUnique/findMany API",
105
+ },
106
+ {
107
+ category: "sql-injection",
108
+ severity: "high",
109
+ re: /\b(?:cursor|conn|db)\.(?:execute|executemany)\s*\(\s*f["']|\.execute\s*\(\s*["'][^"'\n]{0,120}["']\s*%\s*[\(\[]/g,
110
+ describe: () => "SQL built with f-strings or %-formatting — Python driver-level injection",
111
+ fix: 'always pass parameters separately: cursor.execute("SELECT * FROM users WHERE id = ?", (id,)) — never f-strings',
112
+ },
113
+ /* --------------------- Command injection --------------------- */
114
+ {
115
+ category: "command-injection",
116
+ severity: "high",
117
+ re: /\b(?:exec|execSync|execFileSync)\s*\(\s*[`"']?[\s\S]{0,160}?(?:\$\{[\s\S]{0,40}?\}|["'`]\s*\+)/g,
118
+ describe: () => "shell command built with interpolation/concatenation — user input executes as code",
119
+ fix: 'never put user input in shell strings: execFile("git", ["log", commit]) with an args array and shell:false',
120
+ },
121
+ {
122
+ category: "command-injection",
123
+ severity: "high",
124
+ re: /\bos\.system\s*\(\s*f["']|subprocess\.(?:run|Popen|call|check_output)\s*\([\s\S]{0,200}?shell\s*=\s*True/g,
125
+ describe: () => "Python shell=True with a formatted command — RCE via user input",
126
+ fix: 'drop shell=True and pass an argv list: subprocess.run(["git", "log", commit]) — never a formatted string',
127
+ },
128
+ {
129
+ category: "command-injection",
130
+ severity: "medium",
131
+ re: /\bshell\s*:\s*true[\s\S]{0,60}?(?:req\.|userInput|payload|commit|targetUrl|filename|filePath)/g,
132
+ describe: () => "spawn/exec with shell:true near user-controlled data — quoting mistakes become RCE",
133
+ fix: "shell:true disables argument safety. Pass argv arrays without a shell unless you control every byte",
134
+ },
135
+ /* --------------------- Path traversal ------------------------ */
136
+ {
137
+ category: "path-traversal",
138
+ severity: "medium",
139
+ re: /\b(?:readFile|readFileSync|writeFile|writeFileSync|createReadStream|createWriteStream|sendFile|unlink|unlinkSync|rmSync|realpath|access)\w*\s*\(\s*[`"']?[\s\S]{0,120}?(?:req\.(?:query|params|body|files)|userInput|uploadedFile|pathVar)[\s\S]{0,60}?\)/g,
140
+ describe: () => "user-controlled path reaches the filesystem — ../ escapes read arbitrary files",
141
+ fix: 'confine user paths: const root = path.resolve("uploads"); const safe = path.resolve(root, name); if (!safe.startsWith(root + path.sep)) throw new Error("bad path")',
142
+ skipWhenInMatch: /path\.basename\s*\(/,
143
+ },
144
+ {
145
+ category: "path-traversal",
146
+ severity: "medium",
147
+ re: /\bpath\.(?:join|resolve)\s*\(\s*[`"']?[\s\S]{0,100}?(?:req\.(?:query|params|body)|userInput|uploadedFile|pathVar)[\s\S]{0,40}?\)/g,
148
+ describe: () => "user input joined into a path — ../ traversal unless confined to a root",
149
+ fix: "basename the user part and verify the result stays inside an allowed root: path.basename(name) + prefix check",
150
+ skipWhenInMatch: /path\.basename\s*\(/,
151
+ },
152
+ /* -------------------------- SSRF ----------------------------- */
153
+ {
154
+ category: "ssrf",
155
+ severity: "medium",
156
+ re: /\b(?:fetch|axios\.(?:get|post|put|patch|request)|got|request|superagent|http\.(?:get|post)|https\.(?:get|post))\s*\(\s*[`"']?[\s\S]{0,140}?(?:req\.(?:query|params|body|headers)|userInput|targetUrl|webhookUrl|callbackUrl|redirectUrl|userUrl|target)[\s\S]{0,40}?\)/g,
157
+ describe: () => "user-supplied URL reaches an HTTP client — SSRF lets attackers hit internal services (169.254.169.254)",
158
+ fix: "allowlist destinations: only https + your domain, or an explicit allowlist; never fetch a user-supplied URL",
159
+ },
160
+ /* --------------------------- XSS ----------------------------- */
161
+ {
162
+ category: "xss",
163
+ severity: "high",
164
+ re: /\b(?:innerHTML|outerHTML|insertAdjacentHTML|document\.write)\s*[:=]\s*[`"'(\s]*[\s\S]{0,100}?(?:req\.(?:query|params|body)|userInput|username|userTitle|userContent|userMessage|userComment|userSearch|userUrl|payload)|\bdangerouslySetInnerHTML\s*=|\bv-html\s*=/g,
165
+ describe: () => "user data flows into an HTML sink — script injection runs in every visitor's browser",
166
+ fix: 'never write user data into HTML sinks: use textContent / {expression} / v-text, or escape HTML on every path in',
167
+ },
168
+ {
169
+ category: "xss",
170
+ severity: "medium",
171
+ re: /\b(?:innerHTML|outerHTML|insertAdjacentHTML)\s*[:=]\s*(?![`"'])([A-Za-z_$][\w$.]*|\$\{)/g,
172
+ describe: (m) => `dynamic value (${m[1].slice(0, 40)}) written into an HTML sink — if it can be user-derived, this is stored/reflected XSS`,
173
+ fix: "write text with textContent; if you must write HTML, escape every interpolated value first",
174
+ },
175
+ /* --------------------- Secret management --------------------- */
176
+ {
177
+ category: "secret",
178
+ severity: "high",
179
+ re: /(?:-----BEGIN [A-Z ]*PRIVATE KEY-----|AKIA[0-9A-Z]{16}|ASIA[0-9A-Z]{16}|AIza[0-9A-Za-z\-_]{35}|ghp_[0-9A-Za-z]{36}|github_pat_[0-9A-Za-z_]{20,}|xox[baprs]-[0-9A-Za-z\-]{10,}|sk_live_[0-9A-Za-z]{16,}|sk-ant-[0-9A-Za-z\-_]{20,}|sk-[A-Za-z0-9]{20,})/g,
180
+ describe: () => "known credential format committed in source — anyone with repo access has a live key",
181
+ fix: "rotate the leaked key NOW, then move it to .env (gitignored): process.env.API_KEY — never in source",
182
+ },
183
+ {
184
+ category: "secret",
185
+ severity: "medium",
186
+ re: /\b(?:api[_-]?key|apikey|client[_-]?secret|secret[_-]?key|auth[_-]?token|access[_-]?token|refresh[_-]?token|jwt[_-]?secret|session[_-]?secret|password|passwd|pwd)\s*[:=]\s*["']([^"']{4,})["']/g,
187
+ describe: (m) => `secret-looking value "${m[1].slice(0, 12)}…" assigned inline instead of read from the environment`,
188
+ fix: "secrets live in .env (gitignored) + process.env.NAME — never as string literals in source",
189
+ },
190
+ {
191
+ category: "secret",
192
+ severity: "medium",
193
+ re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
194
+ describe: () => "a signed JWT committed in source (leaked token or test fixture — either way it must not be here)",
195
+ fix: "remove committed JWTs; issue tokens at runtime and keep the signing secret in the environment",
196
+ },
197
+ /* ---------------------- Authentication ----------------------- */
198
+ {
199
+ category: "authentication",
200
+ severity: "high",
201
+ re: /\b(?:password|passwd|pwd)\b[^;\n]{0,80}?(?:===|!==|==|!=)[^;\n]{0,80}/g,
202
+ describe: () => "password value compared directly with ==/=== — no key-derivation function in sight; cleartext compares are trivially leaked",
203
+ fix: "compare only salted hashes: await bcrypt.compare(input, user.hash) — never plaintext ===",
204
+ accept: (m) => {
205
+ const s = m[0];
206
+ if (/\b(?:bcrypt|compare|hash|argon2|scrypt)\b/.test(s))
207
+ return false;
208
+ const reqRefs = (s.match(/req\./g) ?? []).length;
209
+ // password === confirmPassword (both from the request) is a shape check, not a leak.
210
+ return reqRefs < 2;
211
+ },
212
+ },
213
+ {
214
+ category: "authentication",
215
+ severity: "medium",
216
+ re: /\b(?:INSERT|insert|UPDATE|update|create|save|set)\s*\([^)]{0,120}\bpassword\b[^)]{0,60}\)/g,
217
+ describe: () => "password written straight into a store with no hash call nearby",
218
+ fix: "store only salted hashes: await bcrypt.hash(password, 12), keep the hash, discard the plaintext",
219
+ },
220
+ {
221
+ category: "authentication",
222
+ severity: "high",
223
+ re: /\b(?:token|otp|code|nonce|sessionId|session_id|verificationCode|resetCode|authCode|csrf|secret)\w*\s*[:=]\s*[^;\n]{0,40}?\bMath\.random\(\)/g,
224
+ describe: () => "Math.random() for a token/OTP/session id — predictable output, one guess away from account takeover",
225
+ fix: 'use crypto.randomBytes(32).toString("hex") — Math.random is not cryptographic',
226
+ },
227
+ {
228
+ category: "authentication",
229
+ severity: "medium",
230
+ re: /\bjwt\.(?:sign|verify)\s*\([\s\S]{0,140}?["'][A-Za-z0-9!@#$%^&*\-_=+]{1,16}["']\s*\)/g,
231
+ describe: () => "JWT signed/verified with a short inline literal secret — brute-forceable, and it lives in source",
232
+ fix: "jwt.sign(payload, process.env.JWT_SECRET) with a long random env secret — never an inline literal",
233
+ },
234
+ /* ---------------------- Authorization ------------------------ */
235
+ {
236
+ category: "authorization",
237
+ severity: "medium",
238
+ pathFilter: new RegExp(PROTECTED_PATH),
239
+ guard: /\b(?:req\.(?:user|session|auth|headers\.authorization)|verifyToken|requireAuth|isAuthenticated|jwt\.verify|authMiddleware|passport\.authenticate|apiKey|middleware)/,
240
+ re: /\b(?:app|router)\.(?:get|post|put|delete|patch|use)\s*\(\s*["']([^"']+)["'],\s*[\s\S]{0,700}/g,
241
+ describe: (m) => `route ${m[1]} handles data with no authn/authz check visible (no req.user/req.session/JWT verify) — who is allowed in?`,
242
+ fix: 'every protected route needs a guard: router.get("/api/account", requireAuth, handler) — the guard must verify the token/session server-side, per request',
243
+ },
244
+ {
245
+ category: "authorization",
246
+ severity: "medium",
247
+ pathFilter: /(?:admin|moderator|manager|staff|dashboard)/,
248
+ guard: /\b(?:role|roles|permission|isAdmin|isStaff|requireRole|can\b|scopes?|acl|rbac)/,
249
+ re: /\b(?:app|router)\.(?:get|post|put|delete|patch|use)\s*\(\s*["']([^"']+)["'],\s*[\s\S]{0,700}/g,
250
+ describe: (m) => `privileged route ${m[1]} never references role/permission/isAdmin — broken access control: any logged-in user can reach it`,
251
+ fix: 'enforce roles server-side on every privileged route and every page: if (req.user.role !== "admin") return 403 — never rely on hiding UI',
252
+ },
253
+ /* ---------------------- Input validation --------------------- */
254
+ {
255
+ category: "input-validation",
256
+ severity: "medium",
257
+ re: /\bmulter\s*\(\s*\)|\.single\s*\(\s*["'][^"']+["']\s*\)|\.array\s*\(|\.fields\s*\(|express\.fileupload\s*\(\s*\)/g,
258
+ describe: () => "file upload with no size limit and no type filter — attacker uploads a 10 GB exe or an executable .js served by the static host",
259
+ fix: 'const upload = multer({ storage, limits: { fileSize: 1 * 1024 * 1024 }, fileFilter: (_, f, cb) => cb(null, ALLOWED_TYPES.has(f.mimetype)) })',
260
+ accept: (_m, content) => !/(?:limits\s*:\s*\{[^}]{0,120}fileSize|fileFilter)/.test(content),
261
+ },
262
+ {
263
+ category: "input-validation",
264
+ severity: "medium",
265
+ re: /\b(?:req\.(?:body|query|params)|payload)\.(?:amount|price|total|quantity|fee|qty|subtotal|priceCents)\b/g,
266
+ describe: () => "money field read straight off the request without a Number/NaN/finite/range check — strings and negatives reach arithmetic and the ledger",
267
+ fix: 'validate money as integers of the smallest unit: const v = Number(req.body.amount); if (!Number.isSafeInteger(v) || v <= 0) return 400',
268
+ accept: (m, content) => {
269
+ // Already wrapped in a sanitizer (Number/parseInt/parseFloat)? The fix
270
+ // is applied — don't re-flag the same field.
271
+ const before = content.slice(Math.max(0, m.index - 40), m.index);
272
+ if (/\b(?:Number|parseFloat|parseInt)\s*\($/.test(before))
273
+ return false;
274
+ const lineStart = content.lastIndexOf("\n", m.index);
275
+ const after = content.slice(m.index, content.indexOf("\n", m.index) === -1 ? content.length : content.indexOf("\n", m.index));
276
+ if (/\b(?:isSafeInteger|isFinite|isNaN)\b/.test(after) || /(?:\|\||&&)\s*[^;]{0,40}\b(?:return|throw)/.test(after))
277
+ return false;
278
+ return true;
279
+ },
280
+ },
281
+ /* ---------------------- Code injection ----------------------- */
282
+ {
283
+ category: "input-validation",
284
+ severity: "high",
285
+ re: /\beval\s*\([^)]{0,120}\$\{|new\s+Function\s*\([^)]{0,100}(?:\$\{|["'`]\s*\+)/g,
286
+ describe: () => "eval or dynamic Function construction with interpolated content — arbitrary code execution if any part is user-controlled",
287
+ fix: "never eval: JSON.parse for data, and precompiled functions for logic — eval(anything) is RCE by default",
288
+ },
289
+ {
290
+ category: "input-validation",
291
+ severity: "medium",
292
+ re: /\beval\s*\(/g,
293
+ describe: () => "eval() call — review that its argument can never be user-controlled; even then it is a code smell",
294
+ fix: "replace eval with JSON.parse / precompiled functions; if you truly must eval, allowlist the exact inputs",
295
+ },
296
+ /* -------------------- Prototype pollution -------------------- */
297
+ {
298
+ category: "prototype-pollution",
299
+ severity: "medium",
300
+ re: /\bObject\.assign\s*\(\s*[^)]{0,80}(?:req\.body|req\.query|req\.params|userInput|payload)[^)]{0,40}\)|\.\.\.\s*(?:req\.body|req\.query|req\.params|userInput|payload)[^)\n]{0,40}/g,
301
+ describe: () => "user input merged into objects — a crafted __proto__ key rewrites Object.prototype and breaks auth checks app-wide",
302
+ fix: 'never merge/spread raw request bodies: pick only whitelisted keys, or JSON.parse(body, (k, v) => (k === "__proto__" ? undefined : v))',
303
+ },
304
+ /* ----------------------- Config & transport ------------------ */
305
+ {
306
+ category: "cors",
307
+ severity: "high",
308
+ re: /[\s\S]{0,160}?\borigin\s*:\s*["']\*["'][\s\S]{0,160}?\bcredentials\s*:\s*true[\s\S]{0,160}?\)|Access-Control-Allow-Origin\s*:\s*\*[\s\S]{0,120}?Access-Control-Allow-Credentials\s*:\s*true/g,
309
+ describe: () => "Access-Control-Allow-Origin: * combined with credentials — any website can make authenticated requests from a victim's browser",
310
+ fix: 'allowlist the exact frontend origin(s): cors({ origin: ["https://app.example.com"], credentials: true }) — never * with cookies',
311
+ },
312
+ {
313
+ category: "transport",
314
+ severity: "medium",
315
+ re: /\b(?:fetch|axios\.(?:get|post|put|patch)|got|request|http\.(?:get|post|request))\s*\(\s*["']http:\/\//g,
316
+ describe: () => "outbound call uses cleartext http:// — credentials and data travel unencrypted, MITM-able",
317
+ fix: "use https:// for every outbound call — http only for localhost dev sandboxes",
318
+ },
319
+ {
320
+ category: "transport",
321
+ severity: "medium",
322
+ re: /\bres\.cookie\s*\([^)]{0,160}?\bsecure\s*:\s*false/g,
323
+ describe: () => "session cookie sent without the secure flag — leaked in cleartext on any http page",
324
+ fix: 'res.cookie("session", token, { httpOnly: true, secure: true, sameSite: "lax" })',
325
+ },
326
+ {
327
+ category: "transport",
328
+ severity: "low",
329
+ re: /\bws:\/\//g,
330
+ describe: () => "plain ws:// websocket — unencrypted traffic",
331
+ fix: "use wss:// (TLS) for websockets in production",
332
+ },
333
+ {
334
+ category: "transport",
335
+ severity: "low",
336
+ re: /\bres\.redirect\s*\(\s*["']http:\/\//g,
337
+ describe: () => "redirect target is hardcoded to cleartext http",
338
+ fix: "redirect to https:// URLs",
339
+ },
340
+ {
341
+ category: "logging",
342
+ severity: "medium",
343
+ re: /\bconsole\.(?:log|info|debug|warn)\s*\([\s\S]{0,120}?(?:password|passwd|pwd|api[_-]?key|secret|token|authorization)[\s\S]{0,40}?\)/g,
344
+ describe: () => "credentials reach the console/log pipeline — they end up in CI logs and support tickets",
345
+ fix: 'never log credentials: log identifiers only, or pass values through a redact({ password: "***" }) helper',
346
+ },
347
+ {
348
+ category: "logging",
349
+ severity: "medium",
350
+ re: /\b(?:res|response)\.(?:send|json|write|end)\s*\(\s*[\s\S]{0,80}?\berr\b[\s\S]{0,60}?\bstack\b|\bstack\b[\s\S]{0,40}?\b(?:send|json)\s*\(/g,
351
+ describe: () => "error.stack is shipped to the client — file paths, library versions and internals leak to attackers",
352
+ fix: 'log the stack server-side; respond with { error: "internal", id } and keep a correlation id',
353
+ },
354
+ ];
355
+ /**
356
+ * Repo-level findings that only make sense over the whole tree (e.g. "no
357
+ * rate limiter anywhere"). Cheap to compute, always honest about being
358
+ * indicated.
359
+ */
360
+ export function analyzeSecurityRepoLevel(repo) {
361
+ const issues = [];
362
+ const text = [];
363
+ for (const f of walkTextFiles(repo)) {
364
+ const content = readText(f);
365
+ if (content)
366
+ text.push(content);
367
+ }
368
+ const all = text.join("\n");
369
+ const hasAuthFlow = /\b(?:login|signup|register|signin|sign_in|createAccount|forgotPassword|resetPassword|otp)\b/i.test(all);
370
+ const hasPasswordFlow = /\b(?:password|passwd|pwd)\b/i.test(all);
371
+ if (hasAuthFlow &&
372
+ hasPasswordFlow &&
373
+ !/\b(?:bcrypt|argon2|scrypt|pbkdf2|hashSync|\.hash\s*\()/i.test(all)) {
374
+ issues.push({
375
+ type: "code",
376
+ category: "authentication",
377
+ severity: "medium",
378
+ description: "[indicated] auth + password flow present, but no bcrypt/argon2/scrypt/pbkdf2 anywhere — passwords are handled without a key-derivation function",
379
+ fix: "hash on write, compare on read: bcrypt.hash(password, 12) / bcrypt.compare(input, stored)",
380
+ });
381
+ }
382
+ if (hasAuthFlow &&
383
+ !/\b(?:rateLimit|rate-limit|express-rate-limit|limiter|throttle)\b/i.test(all)) {
384
+ issues.push({
385
+ type: "code",
386
+ category: "authentication",
387
+ severity: "low",
388
+ description: "[indicated] auth endpoints present but no rate limiter appears anywhere — credential stuffing and OTP brute-force are wide open",
389
+ fix: "express-rate-limit on every auth endpoint: 5 req/min per IP+user on /login, /otp, /reset",
390
+ });
391
+ }
392
+ const hasCookieSession = /express-session|express\.session|cookie[_-]?session|cookieSession/i.test(all);
393
+ const hasStateChanging = /\b(?:app|router)\.(?:post|put|delete|patch)\s*\(/g.test(all);
394
+ if (hasCookieSession && hasStateChanging && !/\b(?:csrf|xsrf|csrfToken|csrf-sync|csurf)\b/i.test(all)) {
395
+ issues.push({
396
+ type: "code",
397
+ category: "csrf",
398
+ severity: "medium",
399
+ description: "[indicated] cookie-based sessions with state-changing routes and no CSRF token anywhere — a victim's browser can be tricked into POSTing",
400
+ fix: "add a CSRF token to every state-changing request: csrf-sync middleware, token in a form header, verify on POST/PUT/DELETE",
401
+ });
402
+ }
403
+ if (/\bexpress\s*\(\s*\)|\bfastify\s*\(\s*\)/g.test(all) &&
404
+ !/\bhelmet\b/i.test(all)) {
405
+ issues.push({
406
+ type: "code",
407
+ category: "headers",
408
+ severity: "low",
409
+ description: "[indicated] web framework present but helmet (CSP, HSTS, X-Content-Type-Options, frameguard) is never applied",
410
+ fix: "app.use(helmet()) — one line for CSP, HSTS, X-Frame-Options, X-Content-Type-Options and more",
411
+ });
412
+ }
413
+ return issues;
414
+ }
415
+ function walkTextFiles(root) {
416
+ const out = [];
417
+ const stack = [root];
418
+ while (stack.length && out.length < MAX_FILES) {
419
+ const dir = stack.pop();
420
+ let entries;
421
+ try {
422
+ entries = fs.readdirSync(dir, { withFileTypes: true });
423
+ }
424
+ catch {
425
+ continue;
426
+ }
427
+ for (const e of entries) {
428
+ const p = path.join(dir, e.name);
429
+ if (e.isDirectory()) {
430
+ if (SKIP_DIRS.has(e.name) || e.name.startsWith("."))
431
+ continue;
432
+ stack.push(p);
433
+ }
434
+ else if (e.isFile()) {
435
+ if (e.name === ".gitignore" || e.name.startsWith(".env") || CODE_EXTS.has(path.extname(e.name)))
436
+ out.push(p);
437
+ }
438
+ }
439
+ }
440
+ return out.sort();
441
+ }
442
+ function readText(file) {
443
+ try {
444
+ const st = fs.statSync(file);
445
+ if (st.size > MAX_BYTES_PER_FILE)
446
+ return null;
447
+ const buf = fs.readFileSync(file);
448
+ if (buf.includes(0))
449
+ return null; // binary
450
+ return buf.toString("utf8");
451
+ }
452
+ catch {
453
+ return null;
454
+ }
455
+ }
456
+ function fileGitignoresEnv(root) {
457
+ const stack = [root];
458
+ while (stack.length) {
459
+ const dir = stack.pop();
460
+ let entries;
461
+ try {
462
+ entries = fs.readdirSync(dir, { withFileTypes: true });
463
+ }
464
+ catch {
465
+ continue;
466
+ }
467
+ for (const e of entries) {
468
+ if (e.isDirectory()) {
469
+ if (SKIP_DIRS.has(e.name))
470
+ continue;
471
+ stack.push(path.join(dir, e.name));
472
+ }
473
+ else if (e.isFile() && e.name === ".gitignore") {
474
+ const gi = readText(path.join(dir, e.name)) ?? "";
475
+ if (/(?:^|\n)\s*\.env(?:\$|(?:\.|\s|$))/m.test(gi))
476
+ return true;
477
+ }
478
+ }
479
+ }
480
+ return false;
481
+ }
482
+ /**
483
+ * Scan a repo for the static vulnerability classes. Deterministic: files are
484
+ * walked in sorted order, findings are ordered by (severity, file, line),
485
+ * deduped by (category, file, line), capped. All findings are labeled
486
+ * `[indicated]` — static analysis points, it does not prove.
487
+ */
488
+ export function analyzeSecurityStatic(repo) {
489
+ const issues = [];
490
+ const seen = new Set();
491
+ const push = (file, content, m, rule) => {
492
+ // De-noise: never flag documentation or rule metadata. Lines that are
493
+ // comments, quote a fix ("fix: …"), describe a rule ("describe: …"), or
494
+ // contain regex-alternation syntax ("(?:") are how scanners and docs talk
495
+ // ABOUT these bugs — not app code containing them.
496
+ const lineStart = content.lastIndexOf("\n", m.index) + 1;
497
+ const lineEnd = content.indexOf("\n", m.index);
498
+ const lineText = content.slice(lineStart, lineEnd === -1 ? undefined : lineEnd).trim();
499
+ if (/^(?:\/\/|\/\*|\*|#|<!--|"""|''')/.test(lineText))
500
+ return;
501
+ if (/\b(?:fix:|describe:|title:|re:)/.test(lineText) || /\(\?:/.test(lineText))
502
+ return;
503
+ // Multi-line metadata strings: the marker sits on the previous line
504
+ // ("describe: () =>" then the quoted text on the next line).
505
+ const prevStart = content.lastIndexOf("\n", lineStart - 2) + 1;
506
+ const prevLine = content.slice(prevStart, lineStart - 1).trim();
507
+ if (/\b(?:describe|fix)\s*:\s*\(?\s*\)?\s*=>/.test(prevLine))
508
+ return;
509
+ // Same-line adjacency false positive: `something.exec(fn) + \`…${…}\`` —
510
+ // a closing paren BEFORE the interpolated/concatenated part means the
511
+ // danger isn't the SQL/shell call on this line.
512
+ if (/\)[\s\S]{0,200}?(?:\$\{|["'`]\s*\+)/.test(m[0]))
513
+ return;
514
+ const rel = path.relative(repo, file);
515
+ const line = lineOf(content, m.index);
516
+ const key = `${rule.category}|${rel}|${line}`;
517
+ if (seen.has(key))
518
+ return;
519
+ seen.add(key);
520
+ issues.push({
521
+ type: "code",
522
+ category: rule.category,
523
+ severity: rule.severity,
524
+ file: rel,
525
+ line,
526
+ description: `[indicated] ${rule.describe(m)}`,
527
+ fix: rule.fix,
528
+ });
529
+ };
530
+ for (const file of walkTextFiles(repo)) {
531
+ const content = readText(file);
532
+ if (!content)
533
+ continue;
534
+ // .env files are where secrets belong — the dedicated .env check below
535
+ // (gitignore protection) is the rule that governs them, not the
536
+ // hardcoded-secret patterns.
537
+ if (path.basename(file).startsWith(".env"))
538
+ continue;
539
+ for (const rule of STATIC_SECURITY_RULES) {
540
+ rule.re.lastIndex = 0;
541
+ let m;
542
+ let count = 0;
543
+ while ((m = rule.re.exec(content)) !== null && count < 3) {
544
+ if (rule.skipWhenInMatch && rule.skipWhenInMatch.test(m[0])) {
545
+ count++;
546
+ continue;
547
+ }
548
+ if (rule.accept && !rule.accept(m, content)) {
549
+ count++;
550
+ continue;
551
+ }
552
+ if (rule.pathFilter) {
553
+ const pathMatch = /\(\s*["']([^"']+)["']/.exec(m[0]);
554
+ if (!pathMatch || !rule.pathFilter.test(pathMatch[1])) {
555
+ count++;
556
+ continue;
557
+ }
558
+ if (rule.guard && rule.guard.test(m[0])) {
559
+ count++;
560
+ continue;
561
+ }
562
+ }
563
+ push(file, content, m, rule);
564
+ count++;
565
+ if (m[0].length === 0)
566
+ rule.re.lastIndex++;
567
+ }
568
+ }
569
+ }
570
+ // .env committed risk (only when a .env file actually exists).
571
+ for (const file of walkTextFiles(repo)) {
572
+ if (!/^\.env(\.[\w-]+)?$/.test(path.basename(file)))
573
+ continue;
574
+ if (!fileGitignoresEnv(repo)) {
575
+ issues.push({
576
+ type: "code",
577
+ category: "secret",
578
+ severity: "medium",
579
+ file: path.relative(repo, file),
580
+ line: 1,
581
+ description: "[indicated] .env exists but nothing in .gitignore protects it — one careless commit publishes every secret",
582
+ fix: 'add ".env" and ".env.*" (keep ".env.example") to .gitignore, then confirm with git status',
583
+ });
584
+ }
585
+ break;
586
+ }
587
+ issues.push(...analyzeSecurityRepoLevel(repo));
588
+ const order = { high: 0, medium: 1, low: 2 };
589
+ return issues
590
+ .sort((a, b) => (order[a.severity] ?? 3) - (order[b.severity] ?? 3) ||
591
+ (a.file ?? "").localeCompare(b.file ?? "") ||
592
+ (a.line ?? 0) - (b.line ?? 0))
593
+ .slice(0, MAX_FINDINGS);
594
+ }
595
+ //# sourceMappingURL=securityStatic.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"securityStatic.js","sourceRoot":"","sources":["../../src/analyzers/securityStatic.ts"],"names":[],"mappings":"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"}
@@ -7,6 +7,10 @@ export interface ScanIssue {
7
7
  description: string;
8
8
  /** Stable, deterministic finding id (used by `pitstop repro`). */
9
9
  id?: string;
10
+ /** Vulnerability class (sql-injection, xss, secret, authentication, ...). */
11
+ category?: string;
12
+ /** One-line remediation — the "solve" half of identify-and-solve. */
13
+ fix?: string;
10
14
  }
11
15
  export interface DependencyGraphResult {
12
16
  status: "ok" | "skipped" | "error";