itworksbut 0.4.0 → 0.6.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/README.md CHANGED
@@ -6,7 +6,19 @@ It focuses on common "it works, but..." risks often found in AI-generated or rus
6
6
 
7
7
  For every finding, ItWorksBut gives you a copy-ready fix prompt you can paste into your coding agent. It does not just say what is wrong; it tells your AI exactly what to inspect, what to change, and what not to leak.
8
8
 
9
- It only reads files and reports findings. It does not call external APIs, does not send telemetry, and does not modify the scanned project unless you explicitly ask it to write `todo.md` with `--todo`.
9
+ It mostly reads files and reports findings. It does not send telemetry. The outdated-package check may invoke your local package manager, and the CLI only writes files when you explicitly ask for `todo.md` with `--todo` or `report.md` with `--report`.
10
+
11
+ ## Table of Contents
12
+
13
+ - [Installation](#installation)
14
+ - [Quick Start](#quick-start)
15
+ - [Options](#options)
16
+ - [Terminal Experience](#terminal-experience)
17
+ - [GitHub Actions](#github-actions)
18
+ - [Configuration](#configuration)
19
+ - [Example Output](#example-output)
20
+ - [What It Detects](#what-it-detects)
21
+ - [What It Does Not Guarantee](#what-it-does-not-guarantee)
10
22
 
11
23
  ## Installation
12
24
 
@@ -41,6 +53,7 @@ itworksbut scan --fail-on high
41
53
  itworksbut scan --json
42
54
  itworksbut scan --sarif > itworksbut.sarif
43
55
  itworksbut scan --todo
56
+ itworksbut scan --report
44
57
  itworksbut scan --config itworksbut.config.json
45
58
  itworksbut scan --verbose
46
59
  itworksbut --version
@@ -58,6 +71,7 @@ itworksbut scan [options]
58
71
  - `--json`: Print machine-readable JSON only. No banner, colors, spinner, table, or extra text.
59
72
  - `--sarif`: Print SARIF JSON for GitHub Code Scanning. No banner, colors, spinner, table, or extra text.
60
73
  - `--todo`: Write an AI-ready `todo.md` into the scanned project with prioritized findings, fix prompts, and acceptance criteria.
74
+ - `--report`: Write a Markdown `report.md` into the current working directory.
61
75
  - `--verbose`: Include scanner warnings and extra metadata in console output.
62
76
  - `--quiet`: Print only the summary.
63
77
  - `--no-color`: Disable colored output.
@@ -97,6 +111,14 @@ itworksbut scan --todo
97
111
 
98
112
  This writes `todo.md` to the scanned project. The file is ordered by severity and includes agent rules, exact fix prompts, locations, recommendations, and final verification checkboxes.
99
113
 
114
+ To create a Markdown scan report:
115
+
116
+ ```sh
117
+ itworksbut scan --report
118
+ ```
119
+
120
+ This writes `report.md` to the current working directory with check statuses, summaries, details, and recommendations.
121
+
100
122
  ## GitHub Actions
101
123
 
102
124
  ```yaml
@@ -166,38 +188,10 @@ release/**
166
188
  ## Example Output
167
189
 
168
190
  ![screenshot of an example output](/assets/medium_problems.webp)
169
- ✖ CRITICAL It works, but your .env is tracked.
170
- ✔ Check: env.env-file-tracked
171
- 📁 File: .env
172
- 🤔 Why: .env appears to be tracked by git. Secrets may be exposed.
173
- 🤖 prompt: You are a senior security engineer working in this repository. Fix the ItWorksBut finding env.env-file-tracked at .env. Treat this as a concrete finding. Problem: .env appears to be tracked by git. Secrets may be exposed. Required change: Remove tracked env files from git, add safe examples such as .env.example, and make sure any exposed credentials are treated as compromised. Do not print, log, or preserve raw secret values; use placeholders only. Keep existing behavior intact where possible, add or update focused tests when useful, and do not silence the check unless the underlying risk is actually fixed.
174
-
175
- ▲ HIGH It works, but your SQL query is one template string away from pain.
176
- ✔ Check: database.raw-sql-interpolation
177
- 📁 File: src/db.js:12
178
- 🤔 Why: Possible SQL injection risk: raw SQL appears to be built with template string interpolation.
179
- 🤖 prompt: You are a senior security engineer working in this repository. Fix the ItWorksBut finding database.raw-sql-interpolation at src/db.js:12. This finding is heuristic, so inspect the code first and only change behavior when the risk is real. Problem: Possible SQL injection risk: raw SQL appears to be built with template string interpolation. Required change: Replace SQL string interpolation or concatenation with parameterized queries, prepared statements, or a safe ORM query builder. Keep existing behavior intact where possible, add or update focused tests when useful, and do not silence the check unless the underlying risk is actually fixed.
180
-
181
- SUMMARY
182
-
183
- - ship status: DO NOT SHIP
184
- - Fix the red stuff before production.
185
- - total findings: 2
186
- - critical: 1
187
- - high: 1
188
- - medium: 0
189
- - low: 0
190
- - info: 0
191
- - fail-on: high
192
- - exit decision: 1
193
-
194
- ```
195
-
196
- Secret-like findings never print the full secret value. Findings report the file, line, and secret type where possible.
197
191
 
198
192
  ## What It Detects
199
193
 
200
- The baseline includes 40 modular checks:
194
+ The baseline includes 51 modular checks:
201
195
 
202
196
  - `git.gitignore-missing`
203
197
  - `git.gitignore-incomplete`
@@ -211,6 +205,7 @@ The baseline includes 40 modular checks:
211
205
  - `dependencies.multiple-lockfiles`
212
206
  - `dependencies.install-scripts-risk`
213
207
  - `dependencies.audit-script-missing`
208
+ - `dependencies.outdated-packages`
214
209
  - `package.scripts-missing`
215
210
  - `ci.no-ci-config`
216
211
  - `ci.npm-install-instead-of-npm-ci`
@@ -228,6 +223,10 @@ The baseline includes 40 modular checks:
228
223
  - `api.idor-risk`
229
224
  - `auth.jwt-secret-weak-or-fallback`
230
225
  - `auth.password-hashing-missing`
226
+ - `auth.missing-csrf-protection`
227
+ - `api.missing-method-guard`
228
+ - `api.mass-assignment-risk`
229
+ - `api.no-schema-validation`
231
230
  - `database.raw-sql-interpolation`
232
231
  - `database.no-migrations`
233
232
  - `cookies.insecure-session-cookie`
@@ -235,10 +234,16 @@ The baseline includes 40 modular checks:
235
234
  - `webhooks.missing-raw-body`
236
235
  - `llm.prompt-injection-risk`
237
236
  - `frontend.sourcemaps-production`
237
+ - `frontend.localstorage-token`
238
+ - `files.path-traversal-risk`
239
+ - `ssrf.user-controlled-fetch`
240
+ - `next.public-server-code-risk`
238
241
  - `config.debug-production`
239
242
  - `electron.node-integration-enabled`
240
243
  - `electron.context-isolation-disabled`
244
+ - `electron.remote-content-with-node`
241
245
  - `tauri.dangerous-allowlist-or-capabilities`
246
+ - `tauri.remote-url-permissions-risk`
242
247
 
243
248
  Each check is a plain ESM module with an `id`, metadata, and async `run(context)` function. Add new checks under `src/checks/` and register them in `src/checks/index.js`.
244
249
 
@@ -247,4 +252,3 @@ Each check is a plain ESM module with an `id`, metadata, and async `run(context)
247
252
  ItWorksBut is a static heuristic scanner, not a pentest, SAST replacement, dependency vulnerability database, or runtime security monitor. Findings intentionally use wording such as "possible", "potential", and "appears to" when a check is heuristic.
248
253
 
249
254
  Use it as a CI guardrail for common project hygiene and security mistakes. For production systems, combine it with code review, tests, dependency scanning, secrets scanning, threat modeling, and focused security assessment.
250
- ```
package/bin/itworksbut.js CHANGED
@@ -10,6 +10,7 @@ import { reportConsole } from '../src/reporters/consoleReporter.js';
10
10
  import { reportJson } from '../src/reporters/jsonReporter.js';
11
11
  import { reportSarif } from '../src/reporters/sarifReporter.js';
12
12
  import { writeTodoReport } from '../src/reporters/todoReporter.js';
13
+ import { writeMarkdownReport } from '../src/reporters/markdownReport.js';
13
14
 
14
15
  async function main() {
15
16
  const args = parseArgs(process.argv.slice(2));
@@ -59,6 +60,14 @@ async function main() {
59
60
  reportConsole(result, args);
60
61
  }
61
62
 
63
+ if (args.report) {
64
+ const report = await writeMarkdownReport(result);
65
+ if (!args.json && !args.sarif) {
66
+ const verb = report.overwritten ? 'Overwrote' : 'Wrote';
67
+ process.stdout.write(`${verb} scan report: ${report.filePath}\n`);
68
+ }
69
+ }
70
+
62
71
  return getExitCode(result.findings, result.config.failOn);
63
72
  }
64
73
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "itworksbut",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Static CI checks for common security, repo, dependency, build, and deployment risks in JavaScript vibe coding projects.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,81 @@
1
+ import { isCodeLikeFile, isEnvExampleFile, isLockfile, lineFromOffset, readNearby } from "../helpers.js";
2
+
3
+ const MASS_ASSIGNMENT_PATTERNS = [
4
+ {
5
+ regex: /\b(?:prisma\.\w+\.)?(?:create|update|upsert)\s*\(\s*{[\s\S]{0,600}?\bdata\s*:\s*(?:req\.body|body|input)\b/g,
6
+ label: "direct data object",
7
+ severity: "high"
8
+ },
9
+ {
10
+ regex: /\b(?:db|database|collection|\w+)\.(?:update|updateOne|updateMany|findOneAndUpdate)\s*\([\s\S]{0,300}?(?:req\.body|body|input)\b/g,
11
+ label: "direct update payload",
12
+ severity: "high"
13
+ },
14
+ {
15
+ regex: /\b(?:User|Account|Profile|Model|model|\w+)\.(?:create|update)\s*\(\s*(?:req\.body|body|input)\b/g,
16
+ label: "model create/update payload",
17
+ severity: "high"
18
+ },
19
+ {
20
+ regex: /\$set\s*:\s*(?:req\.body|body|input)\b/g,
21
+ label: "mongodb set payload",
22
+ severity: "high"
23
+ },
24
+ {
25
+ regex: /Object\.assign\s*\(\s*(?:user|entity|account|profile|record|model)[\w$]*\s*,\s*(?:req\.body|body|input)\b/g,
26
+ label: "object assign from request input",
27
+ severity: "medium"
28
+ },
29
+ {
30
+ regex: /\{\s*\.\.\.(?:req\.body|body)\s*}/g,
31
+ label: "spread request body",
32
+ severity: "medium",
33
+ requiresCreateOrUpdateContext: true
34
+ }
35
+ ];
36
+
37
+ const SAFE_FIELD_RE =
38
+ /\b(?:pick|omit|allowedFields|allowlist|whitelist|safeData|validatedData|schema\.parse|schema\.safeParse|safeParse|zod|Joi|joi|yup|valibot)\b/i;
39
+ const RISKY_FIELD_RE =
40
+ /\b(?:role|isAdmin|admin|plan|verified|emailVerified|ownerId|userId|tenantId|accountId|permissions|credits|balance|price|status)\b/i;
41
+ const CREATE_OR_UPDATE_RE = /\b(?:create|update|upsert|insert|save|data\s*:|\$set)\b/i;
42
+
43
+ export default {
44
+ id: "api.mass-assignment-risk",
45
+ title: "Create and update operations should not trust raw request bodies",
46
+ category: "api",
47
+ severity: "high",
48
+ tags: ["api", "database", "mass-assignment", "heuristic"],
49
+ run: async (context) => {
50
+ const findings = [];
51
+
52
+ for (const file of context.textFiles) {
53
+ if (isLockfile(file) || isEnvExampleFile(file) || !isCodeLikeFile(file)) continue;
54
+ const content = await context.readFileSafe(file);
55
+ if (!content || !/\b(?:req\.body|body|input|Object\.assign|\$set)\b/.test(content)) continue;
56
+
57
+ for (const pattern of MASS_ASSIGNMENT_PATTERNS) {
58
+ pattern.regex.lastIndex = 0;
59
+ let match;
60
+ while ((match = pattern.regex.exec(content)) !== null) {
61
+ const line = lineFromOffset(content, match.index);
62
+ const nearby = await readNearby(context, file, line, 8);
63
+ if (pattern.requiresCreateOrUpdateContext && !CREATE_OR_UPDATE_RE.test(nearby)) continue;
64
+ if (SAFE_FIELD_RE.test(nearby)) continue;
65
+
66
+ findings.push({
67
+ severity: RISKY_FIELD_RE.test(nearby) ? "high" : pattern.severity === "high" ? "high" : "medium",
68
+ message: "User-controlled input appears to be passed directly into a create or update operation.",
69
+ file,
70
+ line,
71
+ recommendation: "Whitelist allowed fields explicitly. Never pass req.body directly into database create/update calls.",
72
+ heuristic: true,
73
+ metadata: { pattern: pattern.label }
74
+ });
75
+ }
76
+ }
77
+ }
78
+
79
+ return findings.slice(0, 100);
80
+ }
81
+ };
@@ -0,0 +1,68 @@
1
+ import { isServerOrApiFile, lineFromOffset, readNearby } from "../helpers.js";
2
+
3
+ const ALL_ROUTE_RE = /\b(?:app|router|server)\.all\s*\(/g;
4
+ const NEXT_PAGES_HANDLER_RE = /\bexport\s+default\s+(?:async\s+)?function\s+\w*\s*\(\s*(?:req|request)\s*,\s*(?:res|response)\s*\)/g;
5
+ const NAMED_HANDLER_RE = /\bexport\s+(?:async\s+)?function\s+handler\s*\(\s*(?:req|request)\s*,\s*(?:res|response)\s*\)/g;
6
+ const METHOD_GUARD_RE =
7
+ /\b(?:req|request)\.method\b|\bswitch\s*\(\s*(?:req|request)\.method\s*\)|\ballowedMethods\b|\bmethodNotAllowed\b|\breturn\s+new\s+Response\s*\([^)]*405|\bstatus\s*\(\s*405\s*\)/i;
8
+ const METHOD_EXPORT_RE = /\bexport\s+(?:async\s+)?function\s+(?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*\(/;
9
+
10
+ export default {
11
+ id: "api.missing-method-guard",
12
+ title: "API handlers should restrict HTTP methods",
13
+ category: "api",
14
+ severity: "medium",
15
+ tags: ["api", "http-methods", "heuristic"],
16
+ run: async (context) => {
17
+ const findings = [];
18
+
19
+ for (const file of context.textFiles) {
20
+ if (!isApiCandidate(file)) continue;
21
+ const content = await context.readFileSafe(file);
22
+ if (!content) continue;
23
+
24
+ ALL_ROUTE_RE.lastIndex = 0;
25
+ let allMatch;
26
+ while ((allMatch = ALL_ROUTE_RE.exec(content)) !== null) {
27
+ const line = lineFromOffset(content, allMatch.index);
28
+ const nearby = await readNearby(context, file, line, 8);
29
+ if (/\b(?:allowedMethods|methodNotAllowed|405)\b/i.test(nearby)) continue;
30
+ findings.push(methodFinding(file, line, "app-router-all"));
31
+ }
32
+
33
+ if (METHOD_EXPORT_RE.test(content) || METHOD_GUARD_RE.test(content)) continue;
34
+
35
+ for (const pattern of [NEXT_PAGES_HANDLER_RE, NAMED_HANDLER_RE]) {
36
+ pattern.lastIndex = 0;
37
+ let match;
38
+ while ((match = pattern.exec(content)) !== null) {
39
+ findings.push(methodFinding(file, lineFromOffset(content, match.index), "handler-without-method-guard"));
40
+ }
41
+ }
42
+ }
43
+
44
+ return findings.slice(0, 100);
45
+ }
46
+ };
47
+
48
+ function isApiCandidate(file) {
49
+ return (
50
+ /\.[cm]?[jt]sx?$/.test(file) &&
51
+ (isServerOrApiFile(file) ||
52
+ file.startsWith("routes/") ||
53
+ file.startsWith("api/") ||
54
+ file.includes("/controllers/") ||
55
+ file.includes("/handlers/"))
56
+ );
57
+ }
58
+
59
+ function methodFinding(file, line, pattern) {
60
+ return {
61
+ message: "This API handler appears to process requests without an explicit HTTP method guard.",
62
+ file,
63
+ line,
64
+ recommendation: "Restrict API routes to the intended HTTP methods and return 405 Method Not Allowed for unsupported methods.",
65
+ heuristic: true,
66
+ metadata: { pattern }
67
+ };
68
+ }
@@ -0,0 +1,68 @@
1
+ import { isServerOrApiFile, lineFromOffset, readNearby } from "../helpers.js";
2
+
3
+ const REQUEST_INPUT_RE =
4
+ /\breq\.(?:body|query|params)\b|\brequest\.json\s*\(|\bsearchParams\.get\s*\(|\bnew\s+URL\s*\(\s*(?:req|request)\.url\s*\)\.searchParams\b|\bformData\s*\(|\bctx\.request\.body\b/g;
5
+ const VALIDATION_RE =
6
+ /\b(?:zod|Joi|joi|yup|valibot|ajv|superstruct|TypeBox|validator|validatedData)\b|\.safeParse\s*\(|\.parse\s*\(\s*(?:req\.body|body|input|payload|data)|\.validate\s*\(\s*(?:req\.body|body|input|payload|data)|\bschema\.(?:validate|parse|safeParse)\b/i;
7
+ const GLOBAL_VALIDATION_RE = /\b(?:app|router|server)\.use\s*\([^)]*(?:validate|validator|schema|zod|joi|yup|ajv|valibot)/i;
8
+
9
+ export default {
10
+ id: "api.no-schema-validation",
11
+ title: "API request input should be schema validated",
12
+ category: "api",
13
+ severity: "high",
14
+ tags: ["api", "validation", "heuristic"],
15
+ run: async (context) => {
16
+ const findings = [];
17
+ const globalValidationSeen = await hasGlobalValidationMiddleware(context);
18
+ if (globalValidationSeen) return [];
19
+
20
+ for (const file of context.textFiles) {
21
+ if (!isApiFile(file)) continue;
22
+ const content = await context.readFileSafe(file);
23
+ if (!content) continue;
24
+ if (VALIDATION_RE.test(content)) continue;
25
+
26
+ REQUEST_INPUT_RE.lastIndex = 0;
27
+ const match = REQUEST_INPUT_RE.exec(content);
28
+ if (!match) continue;
29
+
30
+ const line = lineFromOffset(content, match.index);
31
+ const nearby = await readNearby(context, file, line, 10);
32
+ if (VALIDATION_RE.test(nearby)) continue;
33
+
34
+ findings.push({
35
+ message: "This API route appears to consume request input without an obvious schema validation step.",
36
+ file,
37
+ line,
38
+ recommendation: "Validate request body, query and params with a schema library such as Zod, Joi, Valibot, AJV or equivalent.",
39
+ heuristic: true,
40
+ metadata: { pattern: "request-input-without-schema-validation" }
41
+ });
42
+ }
43
+
44
+ return findings.slice(0, 100);
45
+ }
46
+ };
47
+
48
+ function isApiFile(file) {
49
+ return (
50
+ /\.[cm]?[jt]sx?$/.test(file) &&
51
+ (isServerOrApiFile(file) ||
52
+ file.startsWith("api/") ||
53
+ file.startsWith("routes/") ||
54
+ file.startsWith("handlers/") ||
55
+ file.startsWith("controllers/") ||
56
+ file.includes("/handlers/") ||
57
+ file.includes("/controllers/"))
58
+ );
59
+ }
60
+
61
+ async function hasGlobalValidationMiddleware(context) {
62
+ for (const file of context.textFiles) {
63
+ if (!/\.[cm]?[jt]sx?$/.test(file) || !isServerOrApiFile(file)) continue;
64
+ const content = await context.readFileSafe(file);
65
+ if (content && GLOBAL_VALIDATION_RE.test(content)) return true;
66
+ }
67
+ return false;
68
+ }
@@ -0,0 +1,75 @@
1
+ import { isCodeLikeFile, isEnvExampleFile, isLockfile, lineFromOffset } from "../helpers.js";
2
+
3
+ const COOKIE_AUTH_RE =
4
+ /\b(?:res\.cookie|cookies\(\)\.set|response\.cookies\.set|setCookie|serialize)\s*\(\s*["'`](?:session|token|auth|jwt)["'`]|cookieSession\s*\(|express-session|\bsession\s*\(|credentials\s*:\s*["'`]include["'`]|withCredentials\s*:\s*true/gi;
5
+ const CSRF_PROTECTION_RE =
6
+ /\b(?:csrf|csurf|csrfToken|anti-csrf|verifyCsrf|validateCsrf|csrfProtection)\b|double\s+submit|\bsameSite\s*:\s*["'`]?(?:strict|lax)["'`]?\b/gi;
7
+ const STATE_CHANGING_ROUTE_RE =
8
+ /\b(?:app|router|server)\.(?:post|put|patch|delete)\s*\(|\bmethod\s*:\s*["'`](?:POST|PUT|PATCH|DELETE)["'`]|\breq\.method\s*={0,3}\s*["'`](?:POST|PUT|PATCH|DELETE)["'`]|\bexport\s+async\s+function\s+(?:POST|PUT|PATCH|DELETE)\s*\(/g;
9
+
10
+ export default {
11
+ id: "auth.missing-csrf-protection",
12
+ title: "Cookie-based authentication should include CSRF protection",
13
+ category: "auth",
14
+ severity: "high",
15
+ tags: ["auth", "csrf", "cookies", "heuristic"],
16
+ run: async (context) => {
17
+ const cookieMatches = [];
18
+ const stateChangingRoutes = [];
19
+ let csrfProtectionSeen = false;
20
+
21
+ for (const file of context.textFiles) {
22
+ if (isLockfile(file) || isEnvExampleFile(file) || !isCodeLikeFile(file)) continue;
23
+ const content = await context.readFileSafe(file);
24
+ if (!content) continue;
25
+
26
+ CSRF_PROTECTION_RE.lastIndex = 0;
27
+ if (CSRF_PROTECTION_RE.test(content)) {
28
+ csrfProtectionSeen = true;
29
+ break;
30
+ }
31
+
32
+ COOKIE_AUTH_RE.lastIndex = 0;
33
+ let cookieMatch;
34
+ while ((cookieMatch = COOKIE_AUTH_RE.exec(content)) !== null) {
35
+ cookieMatches.push({
36
+ file,
37
+ line: lineFromOffset(content, cookieMatch.index),
38
+ pattern: normalizePattern(cookieMatch[0])
39
+ });
40
+ if (cookieMatches.length >= 25) break;
41
+ }
42
+
43
+ STATE_CHANGING_ROUTE_RE.lastIndex = 0;
44
+ let routeMatch;
45
+ while ((routeMatch = STATE_CHANGING_ROUTE_RE.exec(content)) !== null) {
46
+ stateChangingRoutes.push({
47
+ file,
48
+ line: lineFromOffset(content, routeMatch.index),
49
+ pattern: normalizePattern(routeMatch[0])
50
+ });
51
+ if (stateChangingRoutes.length >= 25) break;
52
+ }
53
+ }
54
+
55
+ if (csrfProtectionSeen || cookieMatches.length === 0 || stateChangingRoutes.length === 0) return [];
56
+
57
+ const primaryCookie = cookieMatches[0];
58
+ return stateChangingRoutes.slice(0, 25).map((route) => ({
59
+ message: "Cookie-based authentication appears to be used without an obvious CSRF protection mechanism.",
60
+ file: route.file,
61
+ line: route.line,
62
+ recommendation: "Use SameSite cookies, CSRF tokens or another explicit CSRF mitigation for state-changing routes.",
63
+ heuristic: true,
64
+ metadata: {
65
+ pattern: "cookie-auth-with-state-changing-route",
66
+ cookieFile: primaryCookie.file,
67
+ cookieLine: primaryCookie.line
68
+ }
69
+ }));
70
+ }
71
+ };
72
+
73
+ function normalizePattern(value) {
74
+ return String(value || "").replace(/\s+/g, " ").slice(0, 80);
75
+ }