itworksbut 0.1.1 → 0.2.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
@@ -4,17 +4,20 @@ ItWorksBut is a Node.js CI tool for static checks in JavaScript, Node.js, web, T
4
4
 
5
5
  It focuses on common "it works, but..." risks often found in AI-generated or rushed prototypes: committed env files, missing lockfiles, weak CI, unsafe web APIs, broad desktop permissions, and similar issues.
6
6
 
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
+
7
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.
8
10
 
9
11
  ## Installation
10
12
 
11
13
  ```sh
12
- npx itworksbut scan
14
+ npm install --global itworksbut
15
+ itworksbut scan
13
16
  ```
14
17
 
15
18
  ### Homebrew
16
19
 
17
- After the formula is committed to the tap, install with:
20
+ With the Homebrew tap:
18
21
 
19
22
  ```sh
20
23
  brew tap oliverjessner/tap
@@ -22,85 +25,68 @@ brew install itworksbut
22
25
  itworksbut scan
23
26
  ```
24
27
 
25
- One-line install:
26
-
27
- ```sh
28
- brew install oliverjessner/tap/itworksbut
29
- ```
30
-
31
- The `itworksbut` formula belongs in the Homebrew tap repo, not in this app repo:
32
-
33
- ```text
34
- https://github.com/oliverjessner/homebrew-tap
35
- └── Formula/
36
- └── itworksbut.rb
37
- ```
38
-
39
- This repository contains a one-command release script. It runs checks, publishes the npm package, generates the Homebrew formula, commits it to the tap, and pushes the tap:
28
+ ## Quick Start
40
29
 
41
30
  ```sh
42
- npm login
43
- npm run publish
31
+ itworksbut scan
44
32
  ```
45
33
 
46
- Do not run `npm publish` directly. The package blocks direct npm publishing so the Homebrew tap cannot be forgotten.
34
+ `scan` is intentionally the strict/default path: all checks are enabled, only heavy generated folders are skipped, and the default `fail-on` threshold is `low` so more issues fail early. Use `--config` only when you deliberately want to tune or suppress checks.
47
35
 
48
- Preview everything without publishing:
36
+ Common commands:
49
37
 
50
38
  ```sh
51
- npm run publish -- --dry-run
39
+ itworksbut scan --path .
40
+ itworksbut scan --fail-on high
41
+ itworksbut scan --json
42
+ itworksbut scan --sarif > itworksbut.sarif
43
+ itworksbut scan --config itworksbut.config.json
44
+ itworksbut scan --verbose
45
+ itworksbut --version
52
46
  ```
53
47
 
54
- By default the script expects the tap checkout at `../homebrew-tap`. Override it when needed:
48
+ ## Options
55
49
 
56
- ```sh
57
- npm run publish -- --tap-path /path/to/homebrew-tap
50
+ ```text
51
+ itworksbut scan [options]
58
52
  ```
59
53
 
60
- Use `--no-push` when you want the script to commit the tap formula but leave the push to you.
54
+ - `--path <path>`: Scan a specific project directory. Defaults to the current directory.
55
+ - `--config <path>`: Use a custom config file. Defaults to `itworksbut.config.json` when present.
56
+ - `--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
+ - `--json`: Print machine-readable JSON only. No banner, colors, spinner, table, or extra text.
58
+ - `--sarif`: Print SARIF JSON for GitHub Code Scanning. No banner, colors, spinner, table, or extra text.
59
+ - `--verbose`: Include scanner warnings and extra metadata in console output.
60
+ - `--quiet`: Print only the summary.
61
+ - `--no-color`: Disable colored output.
62
+ - `--no-banner`: Disable the ASCII intro banner.
63
+ - `--version`, `-v`: Print the installed ItWorksBut version.
61
64
 
62
- ## Local Usage
65
+ Exit codes:
63
66
 
64
- ```sh
65
- node ./bin/itworksbut.js scan
66
- node ./bin/itworksbut.js scan --json
67
- node ./bin/itworksbut.js scan --sarif
68
- node ./bin/itworksbut.js scan --fail-on high
69
- node ./bin/itworksbut.js scan --config itworksbut.config.json
70
- node ./bin/itworksbut.js scan --path .
71
- node ./bin/itworksbut.js scan --verbose
72
- ```
67
+ - `0`: no findings at or above the configured `fail-on` severity
68
+ - `1`: at least one finding at or above the configured `fail-on` severity
69
+ - `2`: tool/runtime error
73
70
 
74
- `scan` is intentionally the strict/default path: all checks are enabled, only heavy generated folders are skipped, and the default `fail-on` threshold is `low` so more issues fail early. Use `--config` only when you deliberately want to tune or suppress checks.
71
+ Severity levels are `critical`, `high`, `medium`, `low`, and `info`.
75
72
 
76
73
  ## Terminal Experience
77
74
 
78
75
  Normal console output is intentionally more opinionated than the machine-readable reporters:
79
76
 
80
77
  ```sh
81
- node ./bin/itworksbut.js scan --theme toxic
78
+ itworksbut scan
82
79
  ```
83
80
 
84
81
  Console-only flags:
85
82
 
86
83
  - `--no-color`
87
84
  - `--no-banner`
88
- - `--no-spinner`
89
- - `--compact`
90
85
  - `--quiet`
91
86
  - `--verbose`
92
- - `--theme default|toxic|mono`
93
87
 
94
88
  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.
95
89
 
96
- Exit codes:
97
-
98
- - `0`: no findings at or above the configured `fail-on` severity
99
- - `1`: at least one finding at or above the configured `fail-on` severity
100
- - `2`: tool/runtime error
101
-
102
- Severity levels are `critical`, `high`, `medium`, `low`, and `info`.
103
-
104
90
  ## GitHub Actions
105
91
 
106
92
  ```yaml
@@ -121,13 +107,13 @@ jobs:
121
107
  node-version: 20
122
108
  cache: npm
123
109
  - run: npm ci
124
- - run: node ./bin/itworksbut.js scan --fail-on high
110
+ - run: npx itworksbut scan --fail-on high
125
111
  ```
126
112
 
127
113
  For GitHub Code Scanning-style output:
128
114
 
129
115
  ```sh
130
- node ./bin/itworksbut.js scan --sarif > itworksbut.sarif
116
+ itworksbut scan --sarif > itworksbut.sarif
131
117
  ```
132
118
 
133
119
  ## Configuration
@@ -171,16 +157,16 @@ release/**
171
157
 
172
158
  ```text
173
159
  ✖ CRITICAL It works, but your .env is tracked.
174
- Check: env.env-file-tracked
175
- File: .env
176
- Why: .env appears to be tracked by git. Secrets may be exposed.
177
- Fix: Remove it from git index, rotate secrets, and commit .env.example.
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.
178
164
 
179
165
  ▲ HIGH It works, but your SQL query is one template string away from pain.
180
- Check: database.raw-sql-interpolation
181
- File: src/db.js:12
182
- Why: Possible SQL injection risk: raw SQL appears to be built with template string interpolation.
183
- Fix: Use parameterized queries, prepared statements, or ORM query builders instead of interpolating values into SQL strings.
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.
184
170
 
185
171
  SUMMARY
186
172
  - ship status: DO NOT SHIP
package/bin/itworksbut.js CHANGED
@@ -1,63 +1,68 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { parseArgs } from "../src/cli/parseArgs.js";
4
- import { printUsage, printRuntimeError } from "../src/cli/output.js";
5
- import { createScanSpinner, normalizeTheme, printIntro } from "../src/cli/terminal.js";
6
- import { scanProject } from "../src/core/scanner.js";
7
- import { getExitCode } from "../src/core/findings.js";
8
- import { reportConsole } from "../src/reporters/consoleReporter.js";
9
- import { reportJson } from "../src/reporters/jsonReporter.js";
10
- import { reportSarif } from "../src/reporters/sarifReporter.js";
3
+ import { parseArgs } from '../src/cli/parseArgs.js';
4
+ import { printUsage, printRuntimeError, printVersion } from '../src/cli/output.js';
5
+ import { createScanSpinner, printIntro } from '../src/cli/terminal.js';
6
+ import { packageInfo } from '../src/core/packageInfo.js';
7
+ import { scanProject } from '../src/core/scanner.js';
8
+ import { getExitCode } from '../src/core/findings.js';
9
+ import { reportConsole } from '../src/reporters/consoleReporter.js';
10
+ import { reportJson } from '../src/reporters/jsonReporter.js';
11
+ import { reportSarif } from '../src/reporters/sarifReporter.js';
11
12
 
12
13
  async function main() {
13
- const args = parseArgs(process.argv.slice(2));
14
- args.theme = normalizeTheme(args.theme);
15
-
16
- if (args.help) {
17
- printUsage();
18
- return 0;
19
- }
20
-
21
- if (args.command !== "scan") {
22
- printUsage();
23
- return 2;
24
- }
25
-
26
- printIntro(args);
27
-
28
- const spinner = createScanSpinner(args);
29
- if (spinner) spinner.start();
30
-
31
- let result;
32
- try {
33
- result = await scanProject({
34
- rootPath: args.path,
35
- configPath: args.config,
36
- failOn: args.failOn,
37
- verbose: args.verbose
38
- });
39
- if (spinner) spinner.succeed("Scan complete. Now the receipts.");
40
- } catch (error) {
41
- if (spinner) spinner.fail("Scan stopped before the receipts were printed.");
42
- throw error;
43
- }
44
-
45
- if (args.sarif) {
46
- process.stdout.write(`${JSON.stringify(reportSarif(result), null, 2)}\n`);
47
- } else if (args.json) {
48
- process.stdout.write(`${JSON.stringify(reportJson(result), null, 2)}\n`);
49
- } else {
50
- reportConsole(result, args);
51
- }
52
-
53
- return getExitCode(result.findings, result.config.failOn);
14
+ const args = parseArgs(process.argv.slice(2));
15
+
16
+ if (args.help) {
17
+ printUsage();
18
+ return 0;
19
+ }
20
+
21
+ if (args.version) {
22
+ printVersion(`It Works But… version ${packageInfo.version}`);
23
+ return 0;
24
+ }
25
+
26
+ if (args.command !== 'scan') {
27
+ printUsage();
28
+ return 2;
29
+ }
30
+
31
+ printIntro(args);
32
+
33
+ const spinner = createScanSpinner(args);
34
+ if (spinner) spinner.start();
35
+
36
+ let result;
37
+ try {
38
+ result = await scanProject({
39
+ rootPath: args.path,
40
+ configPath: args.config,
41
+ failOn: args.failOn,
42
+ verbose: args.verbose,
43
+ });
44
+ if (spinner) spinner.succeed('Scan complete. Now the receipts.');
45
+ } catch (error) {
46
+ if (spinner) spinner.fail('Scan stopped before the receipts were printed.');
47
+ throw error;
48
+ }
49
+
50
+ if (args.sarif) {
51
+ process.stdout.write(`${JSON.stringify(reportSarif(result), null, 2)}\n`);
52
+ } else if (args.json) {
53
+ process.stdout.write(`${JSON.stringify(reportJson(result), null, 2)}\n`);
54
+ } else {
55
+ reportConsole(result, args);
56
+ }
57
+
58
+ return getExitCode(result.findings, result.config.failOn);
54
59
  }
55
60
 
56
61
  main()
57
- .then((code) => {
58
- process.exitCode = code;
59
- })
60
- .catch((error) => {
61
- printRuntimeError(error);
62
- process.exitCode = 2;
63
- });
62
+ .then(code => {
63
+ process.exitCode = code;
64
+ })
65
+ .catch(error => {
66
+ printRuntimeError(error);
67
+ process.exitCode = 2;
68
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "itworksbut",
3
- "version": "0.1.1",
3
+ "version": "0.2.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": {
@@ -18,11 +18,11 @@ export default {
18
18
  const args = match.match[1] || "";
19
19
  if (/\blimit\s*:/.test(args)) continue;
20
20
  findings.push({
21
- message: "express.json() appears to be used without an explicit request body size limit.",
21
+ message: "Express JSON body parsing appears to be used without an explicit request body size limit.",
22
22
  file: match.file,
23
23
  line: match.line,
24
24
  column: match.column,
25
- recommendation: "Set a conservative limit, for example express.json({ limit: '100kb' }), and tune it per route when needed."
25
+ recommendation: "Set a conservative JSON body parser limit such as 100kb, and tune it per route when needed."
26
26
  });
27
27
  }
28
28
  return findings;
package/src/cli/output.js CHANGED
@@ -1,3 +1,5 @@
1
+ import gradient from 'gradient-string';
2
+
1
3
  export function printUsage() {
2
4
  process.stdout.write(`ItWorksBut
3
5
 
@@ -14,15 +16,21 @@ Options:
14
16
  --sarif Print SARIF for GitHub Code Scanning.
15
17
  --no-color Disable color styling.
16
18
  --no-banner Disable the intro banner.
17
- --no-spinner Disable scan spinner.
18
- --compact Print one-line findings.
19
19
  --quiet Print only the summary.
20
- --theme <theme> Console theme: default, toxic, mono.
21
20
  --verbose Include scanner warnings and extra metadata.
21
+ --version, -v Print the ItWorksBut version.
22
22
  --help Show this help.
23
23
  `);
24
24
  }
25
25
 
26
+ export function printVersion(version) {
27
+ try {
28
+ process.stdout.write(`${gradient.rainbow(version)}\n`);
29
+ } catch {
30
+ process.stdout.write(`${version}\n`);
31
+ }
32
+ }
33
+
26
34
  export function printRuntimeError(error) {
27
35
  const message = error instanceof Error ? error.message : String(error);
28
36
  process.stderr.write(`ItWorksBut runtime error: ${message}\n`);
@@ -1,5 +1,5 @@
1
- const FLAG_WITH_VALUE = new Set(["--fail-on", "--config", "--path", "--theme"]);
2
- const BOOLEAN_FLAGS = new Set(["--json", "--sarif", "--verbose", "--help", "-h", "--no-color", "--no-banner", "--no-spinner", "--compact", "--quiet"]);
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"]);
3
3
 
4
4
  export function parseArgs(argv) {
5
5
  const args = {
@@ -12,11 +12,9 @@ export function parseArgs(argv) {
12
12
  verbose: false,
13
13
  noColor: false,
14
14
  noBanner: false,
15
- noSpinner: false,
16
- compact: false,
17
15
  quiet: false,
18
- theme: "default",
19
- help: false
16
+ help: false,
17
+ version: false
20
18
  };
21
19
 
22
20
  const tokens = [...argv];
@@ -45,13 +43,12 @@ export function parseArgs(argv) {
45
43
 
46
44
  if (BOOLEAN_FLAGS.has(token)) {
47
45
  if (token === "--help" || token === "-h") args.help = true;
46
+ if (token === "--version" || token === "-v") args.version = true;
48
47
  if (token === "--json") args.json = true;
49
48
  if (token === "--sarif") args.sarif = true;
50
49
  if (token === "--verbose") args.verbose = true;
51
50
  if (token === "--no-color") args.noColor = true;
52
51
  if (token === "--no-banner") args.noBanner = true;
53
- if (token === "--no-spinner") args.noSpinner = true;
54
- if (token === "--compact") args.compact = true;
55
52
  if (token === "--quiet") args.quiet = true;
56
53
  continue;
57
54
  }
@@ -70,6 +67,5 @@ function assignValue(args, flag, value) {
70
67
  if (flag === "--fail-on") args.failOn = value;
71
68
  else if (flag === "--config") args.config = value;
72
69
  else if (flag === "--path") args.path = value;
73
- else if (flag === "--theme") args.theme = value;
74
70
  else throw new Error(`Unknown argument: ${flag}`);
75
71
  }
@@ -4,8 +4,6 @@ import figlet from "figlet";
4
4
  import gradient from "gradient-string";
5
5
  import ora from "ora";
6
6
 
7
- const THEMES = new Set(["default", "toxic", "mono"]);
8
-
9
7
  const SPINNER_TEXT = {
10
8
  git: "Checking git hygiene",
11
9
  env: "Sniffing for secrets",
@@ -20,21 +18,12 @@ const SPINNER_TEXT = {
20
18
  default: "Looking for things that work but should not ship"
21
19
  };
22
20
 
23
- export function normalizeTheme(theme) {
24
- const normalized = String(theme || "default").toLowerCase();
25
- if (!THEMES.has(normalized)) {
26
- throw new Error(`Invalid theme "${theme}". Expected one of: default, toxic, mono`);
27
- }
28
- return normalized;
29
- }
30
-
31
21
  export function isFancyOutputEnabled(options = {}, env = process.env, stdout = process.stdout) {
32
- return Boolean(stdout.isTTY) && !env.CI && !options.json && !options.sarif && !options.noColor && normalizeTheme(options.theme) !== "mono";
22
+ return Boolean(stdout.isTTY) && !env.CI && !options.json && !options.sarif && !options.noColor;
33
23
  }
34
24
 
35
25
  export function isColorEnabled(options = {}, env = process.env, stdout = process.stdout) {
36
26
  if (options.noColor || options.json || options.sarif) return false;
37
- if (normalizeTheme(options.theme) === "mono") return false;
38
27
  if (env.FORCE_COLOR && env.FORCE_COLOR !== "0") return true;
39
28
  if (env.CI) return false;
40
29
  return Boolean(stdout.isTTY);
@@ -51,7 +40,6 @@ export function shouldUseSpinner(options = {}, env = process.env, stdout = proce
51
40
  !env.CI &&
52
41
  !options.json &&
53
42
  !options.sarif &&
54
- !options.noSpinner &&
55
43
  !options.quiet
56
44
  );
57
45
  }
@@ -61,7 +49,7 @@ export function createScanSpinner(options = {}) {
61
49
  return ora({
62
50
  text: SPINNER_TEXT.default,
63
51
  stream: process.stderr,
64
- color: normalizeTheme(options.theme) === "toxic" ? "green" : "cyan"
52
+ color: "cyan"
65
53
  });
66
54
  }
67
55
 
@@ -70,14 +58,9 @@ export function printIntro(options = {}) {
70
58
  return;
71
59
  }
72
60
 
73
- const theme = normalizeTheme(options.theme);
74
61
  const colors = getChalk(options);
75
- const renderTheme = options.noColor ? "mono" : theme;
76
- const title = renderTitle(renderTheme);
77
- const claim =
78
- theme === "toxic"
79
- ? `${colors.bold("Green builds. Red flags.")}\n${colors.green("Let's see what breaks before production.")}`
80
- : `${colors.bold("AI-built? Nice.")}\n${colors.yellow("Now let's see what breaks before production.")}`;
62
+ const title = renderTitle(options.noColor);
63
+ const claim = `${colors.bold("AI-built? Nice.")}\n${colors.yellow("Now let's see what breaks before production.")}`;
81
64
 
82
65
  process.stdout.write(`${title}\n`);
83
66
  process.stdout.write(
@@ -85,12 +68,12 @@ export function printIntro(options = {}) {
85
68
  padding: 1,
86
69
  margin: 1,
87
70
  borderStyle: "round",
88
- borderColor: renderTheme === "mono" ? undefined : renderTheme === "toxic" ? "green" : "cyan"
71
+ borderColor: options.noColor ? undefined : "cyan"
89
72
  })}\n`
90
73
  );
91
74
  }
92
75
 
93
- function renderTitle(theme) {
76
+ function renderTitle(noColor) {
94
77
  let title = "ItWorksBut";
95
78
  try {
96
79
  title = figlet.textSync("ItWorksBut", {
@@ -103,8 +86,7 @@ function renderTitle(theme) {
103
86
  }
104
87
 
105
88
  try {
106
- if (theme === "mono") return title;
107
- if (theme === "toxic") return gradient(["#faff00", "#39ff14", "#00f5ff"])(title);
89
+ if (noColor) return title;
108
90
  return gradient.rainbow(title);
109
91
  } catch {
110
92
  return title;
@@ -0,0 +1,11 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
6
+ const packageJson = JSON.parse(fs.readFileSync(path.join(repoRoot, "package.json"), "utf8"));
7
+
8
+ export const packageInfo = {
9
+ name: packageJson.name,
10
+ version: packageJson.version
11
+ };
@@ -1,6 +1,7 @@
1
1
  import checks from "../checks/index.js";
2
2
  import { createContext } from "./context.js";
3
3
  import { normalizeFinding, severityRank } from "./findings.js";
4
+ import { packageInfo } from "./packageInfo.js";
4
5
 
5
6
  export async function scanProject(options = {}) {
6
7
  const startedAt = new Date();
@@ -43,7 +44,7 @@ export async function scanProject(options = {}) {
43
44
  config: context.config,
44
45
  meta: {
45
46
  tool: "ItWorksBut",
46
- version: "0.1.0",
47
+ version: packageInfo.version,
47
48
  rootPath: context.rootPath,
48
49
  packageManager: context.packageManager,
49
50
  gitAvailable: context.gitAvailable,
@@ -1,107 +1,112 @@
1
- import { SEVERITIES } from "../core/config.js";
2
- import { countBySeverity, getExitCode } from "../core/findings.js";
3
- import { isFancyOutputEnabled, getChalk } from "../cli/terminal.js";
4
- import { formatSeverity, getConsoleFindingTitle, getShipStatus, renderSummaryBox, renderSummaryTable } from "./consoleStyle.js";
1
+ import { SEVERITIES } from '../core/config.js';
2
+ import { countBySeverity, getExitCode } from '../core/findings.js';
3
+ import { isFancyOutputEnabled, getChalk } from '../cli/terminal.js';
4
+ import {
5
+ formatSeverity,
6
+ getConsoleFindingTitle,
7
+ getFixPrompt,
8
+ getShipStatus,
9
+ renderSummaryBox,
10
+ renderSummaryTable,
11
+ } from './consoleStyle.js';
5
12
 
6
13
  export function reportConsole(result, options = {}) {
7
- const { findings, warnings, config, meta } = result;
8
- const counts = countBySeverity(findings);
9
- const colors = getChalk(options);
10
- const rich = isFancyOutputEnabled(options);
14
+ const { findings, warnings, config, meta } = result;
15
+ const counts = countBySeverity(findings);
16
+ const colors = getChalk(options);
17
+ const rich = isFancyOutputEnabled(options);
11
18
 
12
- if (!options.quiet && !rich) {
13
- process.stdout.write(`${colors.bold("ItWorksBut receipts")}\n\n`);
14
- }
15
-
16
- if (!options.quiet && findings.length === 0) {
17
- process.stdout.write(`${colors.green ? colors.green("Suspiciously clean. No findings.") : "Suspiciously clean. No findings."}\n\n`);
18
- } else if (!options.quiet) {
19
- for (const severity of SEVERITIES) {
20
- const group = findings.filter((finding) => finding.severity === severity);
21
- if (group.length === 0) continue;
19
+ if (!options.quiet && !rich) {
20
+ process.stdout.write(`${colors.bold('ItWorksBut receipts')}\n\n`);
21
+ }
22
22
 
23
- for (const finding of group) {
24
- writeFinding(finding, options);
25
- }
26
- process.stdout.write("\n");
23
+ if (!options.quiet && findings.length === 0) {
24
+ process.stdout.write(
25
+ `${colors.green ? colors.green('Suspiciously clean. No findings.') : 'Suspiciously clean. No findings.'}\n\n`,
26
+ );
27
+ } else if (!options.quiet) {
28
+ for (const severity of SEVERITIES) {
29
+ const group = findings.filter(finding => finding.severity === severity);
30
+ if (group.length === 0) continue;
31
+
32
+ for (const finding of group) {
33
+ writeFinding(finding, options);
34
+ }
35
+ process.stdout.write('\n');
36
+ }
27
37
  }
28
- }
29
38
 
30
- if (options.verbose && warnings.length > 0) {
31
- process.stdout.write("WARNINGS\n");
32
- for (const warning of warnings) {
33
- process.stdout.write(`- [${warning.checkId}] ${warning.message}\n`);
39
+ if (options.verbose && warnings.length > 0) {
40
+ process.stdout.write('WARNINGS\n');
41
+ for (const warning of warnings) {
42
+ process.stdout.write(`- [${warning.checkId}] ${warning.message}\n`);
43
+ }
44
+ process.stdout.write('\n');
34
45
  }
35
- process.stdout.write("\n");
36
- }
37
46
 
38
- const exitCode = getExitCode(findings, config.failOn);
39
- writeSummary({ counts, total: findings.length, failOn: config.failOn, exitCode }, options);
47
+ const exitCode = getExitCode(findings, config.failOn);
48
+ writeSummary({ counts, total: findings.length, failOn: config.failOn, exitCode }, options);
40
49
 
41
- if (options.verbose) {
42
- process.stdout.write(`- files scanned: ${meta.filesScanned}\n`);
43
- process.stdout.write(`- text files scanned: ${meta.textFilesScanned}\n`);
44
- process.stdout.write(`- git available: ${meta.gitAvailable}\n`);
45
- process.stdout.write(`- warnings: ${warnings.length}\n`);
46
- }
50
+ if (options.verbose) {
51
+ process.stdout.write(`- files scanned: ${meta.filesScanned}\n`);
52
+ process.stdout.write(`- text files scanned: ${meta.textFilesScanned}\n`);
53
+ process.stdout.write(`- git available: ${meta.gitAvailable}\n`);
54
+ process.stdout.write(`- warnings: ${warnings.length}\n`);
55
+ }
47
56
  }
48
57
 
49
58
  function writeFinding(finding, options) {
50
- const colors = getChalk(options);
51
- const severity = formatSeverity(finding.severity, options);
52
- const title = getConsoleFindingTitle(finding);
53
- const location = finding.file ? (finding.line ? `${finding.file}:${finding.line}` : finding.file) : "";
54
-
55
- if (options.compact) {
56
- const where = location ? `${location} - ` : "";
57
- process.stdout.write(`${severity.compactText} ${finding.checkId} ${where}${title}\n`);
58
- return;
59
- }
60
-
61
- process.stdout.write(`${severity.text} ${colors.bold(title)}${finding.heuristic ? colors.gray(" (heuristic)") : ""}\n`);
62
- process.stdout.write(` Check: ${finding.checkId}\n`);
63
- if (location) process.stdout.write(` File: ${location}\n`);
64
- process.stdout.write(` Why: ${finding.message}\n`);
65
- if (finding.recommendation) process.stdout.write(` Fix: ${finding.recommendation}\n`);
66
-
67
- if (options.verbose) {
68
- process.stdout.write(` Category: ${finding.category || "unknown"}\n`);
69
- if (finding.tags?.length) process.stdout.write(` Tags: ${finding.tags.join(", ")}\n`);
70
- if (finding.line) process.stdout.write(` Line: ${finding.line}\n`);
71
- const evidence = safeEvidence(finding);
72
- if (evidence) process.stdout.write(` Evidence: ${evidence}\n`);
73
- }
59
+ const colors = getChalk(options);
60
+ const severity = formatSeverity(finding.severity, options);
61
+ const title = getConsoleFindingTitle(finding);
62
+ const location = finding.file ? (finding.line ? `${finding.file}:${finding.line}` : finding.file) : '';
63
+
64
+ process.stdout.write(
65
+ `${severity.text} ${colors.bold(title)}${finding.heuristic ? colors.gray(' (heuristic)') : ''}\n`,
66
+ );
67
+ process.stdout.write(` ✔ Check: ${finding.checkId}\n`);
68
+ if (location) process.stdout.write(` 📁 File: ${location}\n`);
69
+ process.stdout.write(` 🤔 Why: ${finding.message}\n`);
70
+ process.stdout.write(` 🤖 Prompt: ${getFixPrompt(finding)}\n`);
71
+
72
+ if (options.verbose) {
73
+ process.stdout.write(` Category: ${finding.category || 'unknown'}\n`);
74
+ if (finding.tags?.length) process.stdout.write(` Tags: ${finding.tags.join(', ')}\n`);
75
+ if (finding.line) process.stdout.write(` Line: ${finding.line}\n`);
76
+ const evidence = safeEvidence(finding);
77
+ if (evidence) process.stdout.write(` Evidence: ${evidence}\n`);
78
+ }
74
79
 
75
- process.stdout.write("\n");
80
+ process.stdout.write('\n');
76
81
  }
77
82
 
78
83
  function writeSummary({ counts, total, failOn, exitCode }, options) {
79
- const colors = getChalk(options);
80
- const ship = getShipStatus(counts);
81
-
82
- if (isFancyOutputEnabled(options)) {
83
- process.stdout.write(`${renderSummaryBox(counts, options)}\n`);
84
- process.stdout.write(`${renderSummaryTable(counts, options)}\n`);
85
- process.stdout.write(`\nFail-on: ${failOn} | Exit decision: ${exitCode}\n`);
86
- return;
87
- }
84
+ const colors = getChalk(options);
85
+ const ship = getShipStatus(counts);
86
+
87
+ if (isFancyOutputEnabled(options)) {
88
+ process.stdout.write(`${renderSummaryBox(counts, options)}\n`);
89
+ process.stdout.write(`${renderSummaryTable(counts, options)}\n`);
90
+ process.stdout.write(`\nFail-on: ${failOn} | Exit decision: ${exitCode}\n`);
91
+ return;
92
+ }
88
93
 
89
- process.stdout.write("SUMMARY\n");
90
- process.stdout.write(`- ship status: ${colors.bold(ship.status)}\n`);
91
- process.stdout.write(`- ${ship.tone}\n`);
92
- process.stdout.write(`- total findings: ${total}\n`);
93
- for (const severity of SEVERITIES) {
94
- process.stdout.write(`- ${severity}: ${counts[severity]}\n`);
95
- }
96
- process.stdout.write(`- fail-on: ${failOn}\n`);
97
- process.stdout.write(`- exit decision: ${exitCode}\n`);
94
+ process.stdout.write('SUMMARY\n');
95
+ process.stdout.write(`- ship status: ${colors.bold(ship.status)}\n`);
96
+ process.stdout.write(`- ${ship.tone}\n`);
97
+ process.stdout.write(`- total findings: ${total}\n`);
98
+ for (const severity of SEVERITIES) {
99
+ process.stdout.write(`- ${severity}: ${counts[severity]}\n`);
100
+ }
101
+ process.stdout.write(`- fail-on: ${failOn}\n`);
102
+ process.stdout.write(`- exit decision: ${exitCode}\n`);
98
103
  }
99
104
 
100
105
  function safeEvidence(finding) {
101
- const metadata = finding.metadata || {};
102
- if (metadata.secretType) return `secret type: ${metadata.secretType}; value redacted`;
103
- if (metadata.pattern) return `pattern: ${metadata.pattern}`;
104
- if (metadata.routePath) return `route: ${metadata.routePath}`;
105
- if (metadata.envName) return `environment variable name: ${metadata.envName}`;
106
- return "";
106
+ const metadata = finding.metadata || {};
107
+ if (metadata.secretType) return `secret type: ${metadata.secretType}; value redacted`;
108
+ if (metadata.pattern) return `pattern: ${metadata.pattern}`;
109
+ if (metadata.routePath) return `route: ${metadata.routePath}`;
110
+ if (metadata.envName) return `environment variable name: ${metadata.envName}`;
111
+ return '';
107
112
  }
@@ -1,155 +1,260 @@
1
- import boxen from "boxen";
2
- import Table from "cli-table3";
3
- import { SEVERITIES } from "../core/config.js";
4
- import { getChalk, normalizeTheme } from "../cli/terminal.js";
1
+ import boxen from 'boxen';
2
+ import Table from 'cli-table3';
3
+ import { SEVERITIES } from '../core/config.js';
4
+ import { getChalk } from '../cli/terminal.js';
5
5
 
6
6
  const EDGY_TITLES = {
7
- "env.env-file-tracked": "It works, but your .env is tracked.",
8
- "env.possible-secret-in-code": "It works, but your repo may be leaking secrets.",
9
- "env.frontend-secret-exposure": "It works, but your frontend env variable smells like a backend secret.",
10
- "git.gitignore-missing": "It works, but your repo forgot what not to commit.",
11
- "git.gitignore-incomplete": "It works, but your .gitignore has holes.",
12
- "git.ignored-files-tracked": "It works, but Git is already tracking files you meant to ignore.",
13
- "dependencies.lockfile-missing": "It works on your machine, but your dependency tree is not locked.",
14
- "dependencies.multiple-lockfiles": "It works, but your package managers are fighting.",
15
- "ci.no-ci-config": "It works, but nobody checks it before it ships.",
16
- "ci.npm-install-instead-of-npm-ci": "It works, but your CI is installing instead of reproducing.",
17
- "ci.no-test-step": "It works, but your CI is basically decorative.",
18
- "node.express-json-limit-missing": "It works, but your API accepts oversized bodies.",
19
- "node.rate-limit-missing": "It works, but your endpoints have no brakes.",
20
- "node.helmet-missing": "It works, but your HTTP headers are underdressed.",
21
- "node.cors-wildcard": "It works, but CORS is holding the door open.",
22
- "web.dangerous-inner-html": "It works, but your frontend is injecting HTML with sharp edges.",
23
- "api.missing-auth-on-routes": "It works, but this API route appears to trust strangers.",
24
- "api.idor-risk": "It works, but this ID lookup may belong to someone else.",
25
- "database.raw-sql-interpolation": "It works, but your SQL query is one template string away from pain.",
26
- "database.no-migrations": "It works, but your database schema has no paper trail.",
27
- "electron.node-integration-enabled": "It works, but Electron is holding the Node.js door open.",
28
- "electron.context-isolation-disabled": "It works, but your renderer and backend are sharing a room.",
29
- "tauri.dangerous-allowlist-or-capabilities": "It works, but your Tauri permissions look too generous."
7
+ 'env.env-file-tracked': 'It works, but your .env is tracked.',
8
+ 'env.possible-secret-in-code': 'It works, but your repo may be leaking secrets.',
9
+ 'env.frontend-secret-exposure': 'It works, but your frontend env variable smells like a backend secret.',
10
+ 'git.gitignore-missing': 'It works, but your repo forgot what not to commit.',
11
+ 'git.gitignore-incomplete': 'It works, but your .gitignore has holes.',
12
+ 'git.ignored-files-tracked': 'It works, but Git is already tracking files you meant to ignore.',
13
+ 'dependencies.lockfile-missing': 'It works on your machine, but your dependency tree is not locked.',
14
+ 'dependencies.multiple-lockfiles': 'It works, but your package managers are fighting.',
15
+ 'ci.no-ci-config': 'It works, but nobody checks it before it ships.',
16
+ 'ci.npm-install-instead-of-npm-ci': 'It works, but your CI is installing instead of reproducing.',
17
+ 'ci.no-test-step': 'It works, but your CI is basically decorative.',
18
+ 'node.express-json-limit-missing': 'It works, but your API accepts oversized bodies.',
19
+ 'node.rate-limit-missing': 'It works, but your endpoints have no brakes.',
20
+ 'node.helmet-missing': 'It works, but your HTTP headers are underdressed.',
21
+ 'node.cors-wildcard': 'It works, but CORS is holding the door open.',
22
+ 'web.dangerous-inner-html': 'It works, but your frontend is injecting HTML with sharp edges.',
23
+ 'api.missing-auth-on-routes': 'It works, but this API route appears to trust strangers.',
24
+ 'api.idor-risk': 'It works, but this ID lookup may belong to someone else.',
25
+ 'database.raw-sql-interpolation': 'It works, but your SQL query is one template string away from pain.',
26
+ 'database.no-migrations': 'It works, but your database schema has no paper trail.',
27
+ 'electron.node-integration-enabled': 'It works, but Electron is holding the Node.js door open.',
28
+ 'electron.context-isolation-disabled': 'It works, but your renderer and backend are sharing a room.',
29
+ 'tauri.dangerous-allowlist-or-capabilities': 'It works, but your Tauri permissions look too generous.',
30
30
  };
31
31
 
32
32
  const SEVERITY_META = {
33
- critical: { symbol: "", label: "CRITICAL" },
34
- high: { symbol: "", label: "HIGH" },
35
- medium: { symbol: "", label: "MEDIUM" },
36
- low: { symbol: "", label: "LOW" },
37
- info: { symbol: "i", label: "INFO" }
33
+ critical: { symbol: '', label: 'CRITICAL' },
34
+ high: { symbol: '', label: 'HIGH' },
35
+ medium: { symbol: '', label: 'MEDIUM' },
36
+ low: { symbol: '', label: 'LOW' },
37
+ info: { symbol: 'i', label: 'INFO' },
38
+ };
39
+
40
+ const FIX_PROMPT_ACTIONS = {
41
+ 'env.env-file-tracked':
42
+ 'Remove tracked env files from git, add safe examples such as .env.example, and make sure any exposed credentials are treated as compromised.',
43
+ 'env.possible-secret-in-code':
44
+ 'Move hardcoded secret material into a runtime secret store or CI secret, replace committed values with placeholders, and avoid printing secret values anywhere.',
45
+ 'env.frontend-secret-exposure':
46
+ 'Move secret-like frontend environment variables to server-side code and keep only intentionally public values behind public prefixes.',
47
+ 'git.gitignore-missing':
48
+ 'Add a project-appropriate .gitignore for dependencies, local env files, build output, logs, databases, OS files, and coverage artifacts.',
49
+ 'git.gitignore-incomplete':
50
+ 'Update .gitignore with the missing high-risk patterns without removing existing project-specific ignores.',
51
+ 'git.ignored-files-tracked':
52
+ 'Stop tracking generated or local-only files that match ignore rules and preserve the files locally when appropriate.',
53
+ 'dependencies.lockfile-missing':
54
+ 'Generate and commit exactly one package-manager lockfile for the package manager used by the project.',
55
+ 'dependencies.multiple-lockfiles':
56
+ 'Keep the lockfile for the package manager the project actually uses and remove competing lockfiles.',
57
+ 'dependencies.install-scripts-risk':
58
+ 'Review install lifecycle scripts, remove them if unnecessary, or document and constrain them so CI installs stay predictable.',
59
+ 'dependencies.audit-script-missing':
60
+ 'Add a dependency audit or security script and wire it into CI without breaking existing scripts.',
61
+ 'package.scripts-missing':
62
+ 'Add the missing standard package scripts using the existing tooling and naming conventions in this project.',
63
+ 'ci.no-ci-config': 'Add a CI workflow that installs from the lockfile and runs checks, tests, and build steps.',
64
+ 'ci.npm-install-instead-of-npm-ci':
65
+ 'Replace npm install with npm ci in CI jobs unless the command is intentionally global installation.',
66
+ 'ci.no-build-step': "Add a build step to CI using the project's existing package scripts.",
67
+ 'ci.no-test-step': "Add a test step to CI using the project's existing test command.",
68
+ 'node.express-json-limit-missing':
69
+ 'Add explicit body size limits to express.json middleware and keep route behavior intact.',
70
+ 'node.rate-limit-missing':
71
+ 'Add appropriate rate limiting for API routes, especially authentication and write endpoints.',
72
+ 'node.helmet-missing':
73
+ 'Install and apply Helmet or equivalent security headers early in the Express middleware stack.',
74
+ 'node.cors-wildcard':
75
+ 'Restrict CORS origins to trusted application origins and avoid wildcard or credentials-unsafe configurations.',
76
+ 'web.client-side-auth-only':
77
+ 'Move authorization enforcement to server-side API or route handlers and keep frontend checks as UI-only hints.',
78
+ 'web.dangerous-inner-html':
79
+ 'Remove direct HTML injection or add proven sanitization at the trust boundary before rendering.',
80
+ 'web.missing-output-sanitization': 'Escape or sanitize user-controlled output before it reaches HTML responses.',
81
+ 'api.missing-auth-on-routes':
82
+ 'Add explicit authentication and authorization to the route, or document why the route is intentionally public.',
83
+ 'api.idor-risk':
84
+ 'Scope object access by authenticated user, owner, tenant, account, or organization in addition to object id.',
85
+ 'database.raw-sql-interpolation':
86
+ 'Replace SQL string interpolation or concatenation with parameterized queries, prepared statements, or a safe ORM query builder.',
87
+ 'database.no-migrations': 'Add versioned database migrations that match the detected ORM or database stack.',
88
+ 'electron.node-integration-enabled':
89
+ 'Set nodeIntegration to false and expose only narrowly scoped APIs through preload.',
90
+ 'electron.context-isolation-disabled':
91
+ 'Enable contextIsolation and review preload boundaries for renderer-to-main communication.',
92
+ 'tauri.dangerous-allowlist-or-capabilities':
93
+ 'Tighten Tauri allowlists, capabilities, scopes, shell access, filesystem access, remote URLs, and CSP.',
38
94
  };
39
95
 
40
96
  export function getConsoleFindingTitle(finding) {
41
- if (EDGY_TITLES[finding.checkId]) return EDGY_TITLES[finding.checkId];
42
- if (finding.heuristic) return `It works, but this pattern may be risky: ${finding.title || finding.checkId}.`;
43
- return `It works, but ${lowercaseFirst(finding.title || finding.message || finding.checkId)}.`;
97
+ if (EDGY_TITLES[finding.checkId]) return EDGY_TITLES[finding.checkId];
98
+ if (finding.heuristic) return `It works, but this pattern may be risky: ${finding.title || finding.checkId}.`;
99
+ return `It works, but ${lowercaseFirst(finding.title || finding.message || finding.checkId)}.`;
100
+ }
101
+
102
+ export function getFixPrompt(finding) {
103
+ const location = finding.file
104
+ ? `${finding.file}${finding.line ? `:${finding.line}` : ''}`
105
+ : 'the affected project files';
106
+ const action =
107
+ FIX_PROMPT_ACTIONS[finding.checkId] ||
108
+ finding.recommendation ||
109
+ 'Fix the underlying issue without suppressing the scanner.';
110
+ const heuristic = finding.heuristic
111
+ ? 'This finding is heuristic, so inspect the code first and only change behavior when the risk is real.'
112
+ : 'Treat this as a concrete finding.';
113
+ const secretSafety = isSecretFinding(finding)
114
+ ? 'Do not print, log, or preserve raw secret values; use placeholders only.'
115
+ : '';
116
+ const recommendation = finding.recommendation ? `Existing recommendation: ${finding.recommendation}` : '';
117
+
118
+ return collapseWhitespace(
119
+ [
120
+ 'You are a senior security engineer working in this repository.',
121
+ `Fix the ItWorksBut finding ${finding.checkId} at ${location}.`,
122
+ heuristic,
123
+ `Problem: ${finding.message}`,
124
+ `Required change: ${action}`,
125
+ recommendation,
126
+ secretSafety,
127
+ '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.',
128
+ ]
129
+ .filter(Boolean)
130
+ .join(' '),
131
+ );
44
132
  }
45
133
 
46
134
  export function formatSeverity(severity, options = {}) {
47
- const colors = getChalk(options);
48
- const meta = SEVERITY_META[severity] || SEVERITY_META.info;
49
- const raw = `${meta.symbol} ${meta.label}`;
135
+ const colors = getChalk(options);
136
+ const meta = SEVERITY_META[severity] || SEVERITY_META.info;
137
+ const raw = `${meta.symbol} ${meta.label}`;
138
+
139
+ if (options.noColor) {
140
+ return {
141
+ ...meta,
142
+ text: colors.bold(raw),
143
+ shortText: colors.bold(`${meta.symbol} ${meta.label}`),
144
+ };
145
+ }
146
+
147
+ const stylers = {
148
+ critical: value => colors.bgRed.white.bold(value),
149
+ high: value => colors.red.bold(value),
150
+ medium: value => colors.yellow.bold(value),
151
+ low: value => colors.blue(value),
152
+ info: value => colors.gray(value),
153
+ };
50
154
 
51
- if (normalizeTheme(options.theme) === "mono") {
155
+ const style = stylers[severity] || stylers.info;
52
156
  return {
53
- ...meta,
54
- text: colors.bold(raw),
55
- compactText: colors.bold(`${meta.symbol} ${meta.label}`)
157
+ ...meta,
158
+ text: style(raw),
159
+ shortText: style(`${meta.symbol} ${meta.label}`),
56
160
  };
57
- }
58
-
59
- const stylers = {
60
- critical: (value) => colors.bgRed.white.bold(value),
61
- high: (value) => colors.red.bold(value),
62
- medium: (value) => colors.yellow.bold(value),
63
- low: (value) => colors.blue(value),
64
- info: (value) => colors.gray(value)
65
- };
66
-
67
- const style = stylers[severity] || stylers.info;
68
- return {
69
- ...meta,
70
- text: style(raw),
71
- compactText: style(`${meta.symbol} ${meta.label}`)
72
- };
73
161
  }
74
162
 
75
163
  export function getShipStatus(counts) {
76
- if (counts.critical > 0) {
77
- return {
78
- status: "DO NOT SHIP",
79
- tone: "Fix the red stuff before production.",
80
- severity: "critical"
81
- };
82
- }
83
- if (counts.high > 0) {
84
- return {
85
- status: "FIX BEFORE SHIP",
86
- tone: "Close the obvious holes before shipping.",
87
- severity: "high"
88
- };
89
- }
90
- if (counts.medium > 0) {
164
+ if (counts.critical > 0) {
165
+ return {
166
+ status: 'DO NOT SHIP',
167
+ tone: 'Fix the red stuff before production.',
168
+ severity: 'critical',
169
+ };
170
+ }
171
+ if (counts.high > 0) {
172
+ return {
173
+ status: 'FIX BEFORE SHIP',
174
+ tone: "Just copy the text from '🤖 Prompt:' and shove it into your AI.",
175
+ severity: 'high',
176
+ };
177
+ }
178
+ if (counts.medium > 0) {
179
+ return {
180
+ status: 'SHIP WITH CAUTION',
181
+ tone: 'You can ship, but future-you will ask questions.',
182
+ severity: 'medium',
183
+ };
184
+ }
91
185
  return {
92
- status: "SHIP WITH CAUTION",
93
- tone: "You can ship, but future-you will ask questions.",
94
- severity: "medium"
186
+ status: 'SHIP IT, BUT STAY PARANOID',
187
+ tone: 'Suspiciously clean. Ship it, but stay paranoid.',
188
+ severity: 'info',
95
189
  };
96
- }
97
- return {
98
- status: "SHIP IT, BUT STAY PARANOID",
99
- tone: "Suspiciously clean. Ship it, but stay paranoid.",
100
- severity: "info"
101
- };
102
190
  }
103
191
 
104
192
  export function renderSummaryBox(counts, options = {}) {
105
- const colors = getChalk(options);
106
- const ship = getShipStatus(counts);
107
- const severity = formatSeverity(ship.severity, options);
108
- const content = [
109
- colors.bold("It works, but..."),
110
- "",
111
- `Ship status: ${severity.label === "INFO" ? colors.bold(ship.status) : severityColor(ship.status, ship.severity, colors)}`,
112
- `Critical: ${counts.critical}`,
113
- `High: ${counts.high}`,
114
- `Medium: ${counts.medium}`,
115
- "",
116
- ship.tone
117
- ].join("\n");
118
-
119
- return boxen(content, {
120
- padding: 1,
121
- margin: 1,
122
- borderStyle: "round",
123
- borderColor: ship.severity === "critical" || ship.severity === "high" ? "red" : ship.severity === "medium" ? "yellow" : "green"
124
- });
193
+ const colors = getChalk(options);
194
+ const ship = getShipStatus(counts);
195
+ const severity = formatSeverity(ship.severity, options);
196
+ const content = [
197
+ colors.bold('It works, but...'),
198
+ '',
199
+ `Ship status: ${severity.label === 'INFO' ? colors.bold(ship.status) : severityColor(ship.status, ship.severity, colors)}`,
200
+ `Critical: ${counts.critical}`,
201
+ `High: ${counts.high}`,
202
+ `Medium: ${counts.medium}`,
203
+ '',
204
+ ship.tone,
205
+ ].join('\n');
206
+
207
+ return boxen(content, {
208
+ padding: 1,
209
+ margin: 1,
210
+ borderStyle: 'round',
211
+ borderColor:
212
+ ship.severity === 'critical' || ship.severity === 'high'
213
+ ? 'red'
214
+ : ship.severity === 'medium'
215
+ ? 'yellow'
216
+ : 'green',
217
+ });
125
218
  }
126
219
 
127
220
  export function renderSummaryTable(counts, options = {}) {
128
- const table = new Table({
129
- head: ["Severity", "Count"],
130
- style: {
131
- head: [],
132
- border: []
133
- }
134
- });
221
+ const table = new Table({
222
+ head: ['Severity', 'Count'],
223
+ style: {
224
+ head: [],
225
+ border: [],
226
+ },
227
+ });
135
228
 
136
- for (const severity of SEVERITIES) {
137
- const formatted = formatSeverity(severity, options);
138
- table.push([formatted.compactText, counts[severity]]);
139
- }
229
+ for (const severity of SEVERITIES) {
230
+ const formatted = formatSeverity(severity, options);
231
+ table.push([formatted.shortText, counts[severity]]);
232
+ }
140
233
 
141
- return table.toString();
234
+ return table.toString();
142
235
  }
143
236
 
144
237
  function severityColor(value, severity, colors) {
145
- if (severity === "critical") return colors.bgRed.white.bold(value);
146
- if (severity === "high") return colors.red.bold(value);
147
- if (severity === "medium") return colors.yellow.bold(value);
148
- return colors.bold(value);
238
+ if (severity === 'critical') return colors.bgRed.white.bold(value);
239
+ if (severity === 'high') return colors.red.bold(value);
240
+ if (severity === 'medium') return colors.yellow.bold(value);
241
+ return colors.bold(value);
149
242
  }
150
243
 
151
244
  function lowercaseFirst(value) {
152
- if (!value) return value;
153
- const normalized = String(value).replace(/\.$/, "");
154
- return `${normalized.charAt(0).toLowerCase()}${normalized.slice(1)}`;
245
+ if (!value) return value;
246
+ const normalized = String(value).replace(/\.$/, '');
247
+ return `${normalized.charAt(0).toLowerCase()}${normalized.slice(1)}`;
248
+ }
249
+
250
+ function isSecretFinding(finding) {
251
+ return (
252
+ finding.category === 'env' ||
253
+ finding.tags?.some(tag => /secret|token|credential/i.test(tag)) ||
254
+ Boolean(finding.metadata?.secretType)
255
+ );
256
+ }
257
+
258
+ function collapseWhitespace(value) {
259
+ return String(value).replace(/\s+/g, ' ').trim();
155
260
  }