itworksbut 0.1.1 → 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.
@@ -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
  }