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 +45 -59
- package/bin/itworksbut.js +61 -56
- package/package.json +1 -1
- package/src/checks/node/express-json-limit-missing.js +2 -2
- package/src/cli/output.js +11 -3
- package/src/cli/parseArgs.js +5 -9
- package/src/cli/terminal.js +7 -25
- package/src/core/packageInfo.js +11 -0
- package/src/core/scanner.js +2 -1
- package/src/reporters/consoleReporter.js +91 -86
- package/src/reporters/consoleStyle.js +226 -121
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
|
-
|
|
14
|
+
npm install --global itworksbut
|
|
15
|
+
itworksbut scan
|
|
13
16
|
```
|
|
14
17
|
|
|
15
18
|
### Homebrew
|
|
16
19
|
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43
|
-
npm run publish
|
|
31
|
+
itworksbut scan
|
|
44
32
|
```
|
|
45
33
|
|
|
46
|
-
|
|
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
|
-
|
|
36
|
+
Common commands:
|
|
49
37
|
|
|
50
38
|
```sh
|
|
51
|
-
|
|
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
|
-
|
|
48
|
+
## Options
|
|
55
49
|
|
|
56
|
-
```
|
|
57
|
-
|
|
50
|
+
```text
|
|
51
|
+
itworksbut scan [options]
|
|
58
52
|
```
|
|
59
53
|
|
|
60
|
-
|
|
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
|
-
|
|
65
|
+
Exit codes:
|
|
63
66
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
4
|
-
import { printUsage, printRuntimeError } from
|
|
5
|
-
import { createScanSpinner,
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
.then(code => {
|
|
63
|
+
process.exitCode = code;
|
|
64
|
+
})
|
|
65
|
+
.catch(error => {
|
|
66
|
+
printRuntimeError(error);
|
|
67
|
+
process.exitCode = 2;
|
|
68
|
+
});
|
package/package.json
CHANGED
|
@@ -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: "
|
|
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
|
|
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`);
|
package/src/cli/parseArgs.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
const FLAG_WITH_VALUE = new Set(["--fail-on", "--config", "--path"
|
|
2
|
-
const BOOLEAN_FLAGS = new Set(["--json", "--sarif", "--verbose", "--help", "-h", "--
|
|
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
|
-
|
|
19
|
-
|
|
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
|
}
|
package/src/cli/terminal.js
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
|
76
|
-
const
|
|
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:
|
|
71
|
+
borderColor: options.noColor ? undefined : "cyan"
|
|
89
72
|
})}\n`
|
|
90
73
|
);
|
|
91
74
|
}
|
|
92
75
|
|
|
93
|
-
function renderTitle(
|
|
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 (
|
|
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
|
+
};
|
package/src/core/scanner.js
CHANGED
|
@@ -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:
|
|
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
|
|
2
|
-
import { countBySeverity, getExitCode } from
|
|
3
|
-
import { isFancyOutputEnabled, getChalk } from
|
|
4
|
-
import {
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
39
|
-
|
|
47
|
+
const exitCode = getExitCode(findings, config.failOn);
|
|
48
|
+
writeSummary({ counts, total: findings.length, failOn: config.failOn, exitCode }, options);
|
|
40
49
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
80
|
+
process.stdout.write('\n');
|
|
76
81
|
}
|
|
77
82
|
|
|
78
83
|
function writeSummary({ counts, total, failOn, exitCode }, options) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
2
|
-
import Table from
|
|
3
|
-
import { SEVERITIES } from
|
|
4
|
-
import { getChalk
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
155
|
+
const style = stylers[severity] || stylers.info;
|
|
52
156
|
return {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
221
|
+
const table = new Table({
|
|
222
|
+
head: ['Severity', 'Count'],
|
|
223
|
+
style: {
|
|
224
|
+
head: [],
|
|
225
|
+
border: [],
|
|
226
|
+
},
|
|
227
|
+
});
|
|
135
228
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
229
|
+
for (const severity of SEVERITIES) {
|
|
230
|
+
const formatted = formatSeverity(severity, options);
|
|
231
|
+
table.push([formatted.shortText, counts[severity]]);
|
|
232
|
+
}
|
|
140
233
|
|
|
141
|
-
|
|
234
|
+
return table.toString();
|
|
142
235
|
}
|
|
143
236
|
|
|
144
237
|
function severityColor(value, severity, colors) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
}
|