itworksbut 0.2.0 → 0.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.
package/README.md CHANGED
@@ -6,7 +6,7 @@ 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.
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`.
10
10
 
11
11
  ## Installation
12
12
 
@@ -40,6 +40,7 @@ itworksbut scan --path .
40
40
  itworksbut scan --fail-on high
41
41
  itworksbut scan --json
42
42
  itworksbut scan --sarif > itworksbut.sarif
43
+ itworksbut scan --todo
43
44
  itworksbut scan --config itworksbut.config.json
44
45
  itworksbut scan --verbose
45
46
  itworksbut --version
@@ -56,6 +57,7 @@ itworksbut scan [options]
56
57
  - `--fail-on <severity>`: Exit with code `1` when a finding at or above the severity exists. Levels: `critical`, `high`, `medium`, `low`, `info`. Default: `low`.
57
58
  - `--json`: Print machine-readable JSON only. No banner, colors, spinner, table, or extra text.
58
59
  - `--sarif`: Print SARIF JSON for GitHub Code Scanning. No banner, colors, spinner, table, or extra text.
60
+ - `--todo`: Write an AI-ready `todo.md` into the scanned project with prioritized findings, fix prompts, and acceptance criteria.
59
61
  - `--verbose`: Include scanner warnings and extra metadata in console output.
60
62
  - `--quiet`: Print only the summary.
61
63
  - `--no-color`: Disable colored output.
@@ -87,6 +89,14 @@ Console-only flags:
87
89
 
88
90
  In CI, spinners and banners are automatically disabled. With `--json` and `--sarif`, stdout contains only valid machine-readable output. The edgy tone applies only to the Console Reporter.
89
91
 
92
+ To create a fix list for a coding agent:
93
+
94
+ ```sh
95
+ itworksbut scan --todo
96
+ ```
97
+
98
+ 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
+
90
100
  ## GitHub Actions
91
101
 
92
102
  ```yaml
@@ -155,20 +165,21 @@ release/**
155
165
 
156
166
  ## Example Output
157
167
 
158
- ```text
159
- CRITICAL It works, but your .env is tracked.
160
- ✔ Check: env.env-file-tracked
161
- 📁 File: .env
162
- 🤔 Why: .env appears to be tracked by git. Secrets may be exposed.
163
- 🤖 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.
164
-
165
- HIGH It works, but your SQL query is one template string away from pain.
166
- ✔ Check: database.raw-sql-interpolation
167
- 📁 File: src/db.js:12
168
- 🤔 Why: Possible SQL injection risk: raw SQL appears to be built with template string interpolation.
169
- 🤖 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.
168
+ ![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.
170
180
 
171
181
  SUMMARY
182
+
172
183
  - ship status: DO NOT SHIP
173
184
  - Fix the red stuff before production.
174
185
  - total findings: 2
@@ -179,6 +190,7 @@ SUMMARY
179
190
  - info: 0
180
191
  - fail-on: high
181
192
  - exit decision: 1
193
+
182
194
  ```
183
195
 
184
196
  Secret-like findings never print the full secret value. Findings report the file, line, and secret type where possible.
@@ -225,3 +237,4 @@ Each check is a plain ESM module with an `id`, metadata, and async `run(context)
225
237
  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.
226
238
 
227
239
  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.
240
+ ```
package/bin/itworksbut.js CHANGED
@@ -9,6 +9,7 @@ import { getExitCode } from '../src/core/findings.js';
9
9
  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
+ import { writeTodoReport } from '../src/reporters/todoReporter.js';
12
13
 
13
14
  async function main() {
14
15
  const args = parseArgs(process.argv.slice(2));
@@ -51,6 +52,9 @@ async function main() {
51
52
  process.stdout.write(`${JSON.stringify(reportSarif(result), null, 2)}\n`);
52
53
  } else if (args.json) {
53
54
  process.stdout.write(`${JSON.stringify(reportJson(result), null, 2)}\n`);
55
+ } else if (args.todo) {
56
+ const filePath = await writeTodoReport(result);
57
+ if (!args.quiet) process.stdout.write(`Wrote AI todo file: ${filePath}\n`);
54
58
  } else {
55
59
  reportConsole(result, args);
56
60
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "itworksbut",
3
- "version": "0.2.0",
3
+ "version": "0.3.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": {
package/src/cli/output.js CHANGED
@@ -14,6 +14,7 @@ Options:
14
14
  Levels: critical, high, medium, low, info. Default: low.
15
15
  --json Print machine-readable JSON.
16
16
  --sarif Print SARIF for GitHub Code Scanning.
17
+ --todo Write an AI-ready todo.md to the scanned project.
17
18
  --no-color Disable color styling.
18
19
  --no-banner Disable the intro banner.
19
20
  --quiet Print only the summary.
@@ -1,5 +1,5 @@
1
1
  const FLAG_WITH_VALUE = new Set(["--fail-on", "--config", "--path"]);
2
- const BOOLEAN_FLAGS = new Set(["--json", "--sarif", "--verbose", "--help", "-h", "--version", "-v", "--no-color", "--no-banner", "--quiet"]);
2
+ const BOOLEAN_FLAGS = new Set(["--json", "--sarif", "--todo", "--verbose", "--help", "-h", "--version", "-v", "--no-color", "--no-banner", "--quiet"]);
3
3
 
4
4
  export function parseArgs(argv) {
5
5
  const args = {
@@ -9,6 +9,7 @@ export function parseArgs(argv) {
9
9
  failOn: undefined,
10
10
  json: false,
11
11
  sarif: false,
12
+ todo: false,
12
13
  verbose: false,
13
14
  noColor: false,
14
15
  noBanner: false,
@@ -46,6 +47,7 @@ export function parseArgs(argv) {
46
47
  if (token === "--version" || token === "-v") args.version = true;
47
48
  if (token === "--json") args.json = true;
48
49
  if (token === "--sarif") args.sarif = true;
50
+ if (token === "--todo") args.todo = true;
49
51
  if (token === "--verbose") args.verbose = true;
50
52
  if (token === "--no-color") args.noColor = true;
51
53
  if (token === "--no-banner") args.noBanner = true;
@@ -56,8 +58,8 @@ export function parseArgs(argv) {
56
58
  throw new Error(`Unknown argument: ${token}`);
57
59
  }
58
60
 
59
- if (args.json && args.sarif) {
60
- throw new Error("Use only one output format: --json or --sarif");
61
+ if ([args.json, args.sarif, args.todo].filter(Boolean).length > 1) {
62
+ throw new Error("Use only one output format: --json, --sarif, or --todo");
61
63
  }
62
64
 
63
65
  return args;
@@ -19,11 +19,11 @@ const SPINNER_TEXT = {
19
19
  };
20
20
 
21
21
  export function isFancyOutputEnabled(options = {}, env = process.env, stdout = process.stdout) {
22
- return Boolean(stdout.isTTY) && !env.CI && !options.json && !options.sarif && !options.noColor;
22
+ return Boolean(stdout.isTTY) && !env.CI && !options.json && !options.sarif && !options.todo && !options.noColor;
23
23
  }
24
24
 
25
25
  export function isColorEnabled(options = {}, env = process.env, stdout = process.stdout) {
26
- if (options.noColor || options.json || options.sarif) return false;
26
+ if (options.noColor || options.json || options.sarif || options.todo) return false;
27
27
  if (env.FORCE_COLOR && env.FORCE_COLOR !== "0") return true;
28
28
  if (env.CI) return false;
29
29
  return Boolean(stdout.isTTY);
@@ -40,6 +40,7 @@ export function shouldUseSpinner(options = {}, env = process.env, stdout = proce
40
40
  !env.CI &&
41
41
  !options.json &&
42
42
  !options.sarif &&
43
+ !options.todo &&
43
44
  !options.quiet
44
45
  );
45
46
  }
@@ -54,7 +55,7 @@ export function createScanSpinner(options = {}) {
54
55
  }
55
56
 
56
57
  export function printIntro(options = {}) {
57
- if (options.json || options.sarif || options.noBanner || options.quiet || process.env.CI || !process.stdout.isTTY) {
58
+ if (options.json || options.sarif || options.todo || options.noBanner || options.quiet || process.env.CI || !process.stdout.isTTY) {
58
59
  return;
59
60
  }
60
61
 
@@ -0,0 +1,133 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { SEVERITIES } from "../core/config.js";
4
+ import { countBySeverity, getExitCode } from "../core/findings.js";
5
+ import { getConsoleFindingTitle, getFixPrompt } from "./consoleStyle.js";
6
+
7
+ export async function writeTodoReport(result, options = {}) {
8
+ const filePath = options.filePath || path.join(result.meta.rootPath, "todo.md");
9
+ await fs.writeFile(filePath, reportTodo(result), "utf8");
10
+ return filePath;
11
+ }
12
+
13
+ export function reportTodo(result) {
14
+ const counts = countBySeverity(result.findings);
15
+ const exitCode = getExitCode(result.findings, result.config.failOn);
16
+ const findingsBySeverity = new Map(
17
+ SEVERITIES.map((severity) => [severity, result.findings.filter((finding) => finding.severity === severity)])
18
+ );
19
+
20
+ return `${[
21
+ "# AI Fix Todo",
22
+ "",
23
+ "This file was generated by ItWorksBut. It is optimized for coding agents: work top to bottom, keep changes focused, and mark tasks complete only after verification.",
24
+ "",
25
+ "## Agent Rules",
26
+ "",
27
+ "- Fix critical and high findings before lower-priority cleanup.",
28
+ "- Inspect the referenced code before editing, especially for heuristic findings.",
29
+ "- Preserve existing behavior unless the finding explicitly requires changing it.",
30
+ "- Add or update focused tests when the change has behavioral risk.",
31
+ "- Do not silence ItWorksBut checks unless the underlying issue is actually fixed.",
32
+ "- Never print, log, or preserve raw secret values. Use placeholders only.",
33
+ "",
34
+ "## Scan Summary",
35
+ "",
36
+ `- Tool: ${result.meta.tool || "ItWorksBut"} ${result.meta.version || ""}`.trim(),
37
+ `- Project: ${result.meta.rootPath || "unknown"}`,
38
+ `- Completed: ${result.meta.completedAt || "unknown"}`,
39
+ `- Files scanned: ${result.meta.filesScanned ?? "unknown"}`,
40
+ `- Text files scanned: ${result.meta.textFilesScanned ?? "unknown"}`,
41
+ `- Fail-on: ${result.config.failOn}`,
42
+ `- Exit decision: ${exitCode}`,
43
+ `- Total findings: ${result.findings.length}`,
44
+ ...SEVERITIES.map((severity) => `- ${capitalize(severity)}: ${counts[severity]}`),
45
+ "",
46
+ renderFindingSections(findingsBySeverity),
47
+ renderWarnings(result.warnings),
48
+ "## Final Verification",
49
+ "",
50
+ "- [ ] Run the relevant test suite.",
51
+ "- [ ] Run `itworksbut scan` again.",
52
+ "- [ ] Confirm there are no remaining findings at or above the configured fail-on severity.",
53
+ "",
54
+ ].join("\n")}\n`;
55
+ }
56
+
57
+ function renderFindingSections(findingsBySeverity) {
58
+ const sections = [];
59
+
60
+ for (const severity of SEVERITIES) {
61
+ const findings = findingsBySeverity.get(severity) || [];
62
+ if (findings.length === 0) continue;
63
+
64
+ sections.push(`## ${capitalize(severity)} Findings`);
65
+ sections.push("");
66
+
67
+ findings.forEach((finding, index) => {
68
+ sections.push(renderFinding(finding, index + 1));
69
+ sections.push("");
70
+ });
71
+ }
72
+
73
+ if (sections.length === 0) {
74
+ return [
75
+ "## Findings",
76
+ "",
77
+ "No findings were detected. Keep this file as a record or delete it when no longer useful.",
78
+ "",
79
+ ].join("\n");
80
+ }
81
+
82
+ return sections.join("\n");
83
+ }
84
+
85
+ function renderFinding(finding, number) {
86
+ const location = finding.file
87
+ ? `${finding.file}${finding.line ? `:${finding.line}` : ""}${finding.column ? `:${finding.column}` : ""}`
88
+ : "project-wide";
89
+ const heuristic = finding.heuristic ? "yes" : "no";
90
+ const tags = finding.tags?.length ? finding.tags.join(", ") : "none";
91
+
92
+ return [
93
+ `### ${number}. ${getConsoleFindingTitle(finding)}`,
94
+ "",
95
+ "- [ ] Fix this finding.",
96
+ `- Check ID: \`${finding.checkId}\``,
97
+ `- Severity: \`${finding.severity}\``,
98
+ `- Category: \`${finding.category || "unknown"}\``,
99
+ `- Location: \`${location}\``,
100
+ `- Heuristic: \`${heuristic}\``,
101
+ `- Tags: ${tags}`,
102
+ `- Problem: ${finding.message}`,
103
+ `- Recommendation: ${finding.recommendation || "Fix the underlying issue without suppressing the scanner."}`,
104
+ "",
105
+ "#### Agent Prompt",
106
+ "",
107
+ "```text",
108
+ getFixPrompt(finding),
109
+ "```",
110
+ "",
111
+ "#### Acceptance Criteria",
112
+ "",
113
+ `- [ ] The issue behind \`${finding.checkId}\` is fixed at the source.`,
114
+ "- [ ] Existing behavior is preserved or intentional behavior changes are covered by tests.",
115
+ "- [ ] No raw secrets, tokens, credentials, or private values were added to code, logs, tests, or this todo.",
116
+ "- [ ] The relevant scanner finding no longer appears after rerunning ItWorksBut.",
117
+ ].join("\n");
118
+ }
119
+
120
+ function renderWarnings(warnings = []) {
121
+ if (!warnings.length) return "";
122
+
123
+ return [
124
+ "## Scanner Warnings",
125
+ "",
126
+ ...warnings.map((warning) => `- \`${warning.checkId}\`: ${warning.message}`),
127
+ "",
128
+ ].join("\n");
129
+ }
130
+
131
+ function capitalize(value) {
132
+ return `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
133
+ }