itworksbut 0.5.0 → 0.7.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 +46 -31
- package/bin/itworksbut.js +16 -1
- package/package.json +2 -1
- package/src/checks/dependencies/outdated-packages.js +297 -0
- package/src/checks/index.js +2 -0
- package/src/cli/output.js +8 -0
- package/src/cli/parseArgs.js +14 -2
- package/src/commands/stress.js +114 -0
- package/src/core/checkResults.js +53 -0
- package/src/core/scanner.js +42 -4
- package/src/reporters/consoleReporter.js +42 -1
- package/src/reporters/jsonReporter.js +3 -0
- package/src/reporters/markdownReport.js +203 -0
- package/src/reporters/stressMarkdownReport.js +157 -0
- package/src/stress/artilleryRunner.js +83 -0
- package/src/stress/discoverEndpoints.js +200 -0
- package/src/stress/stressConfig.js +31 -0
- package/src/stress/stressRenderer.js +80 -0
- package/src/stress/stressResultParser.js +155 -0
- package/src/utils/packageManager.js +28 -0
- package/src/utils/targetSafety.js +90 -0
package/README.md
CHANGED
|
@@ -6,7 +6,19 @@ It focuses on common "it works, but..." risks often found in AI-generated or rus
|
|
|
6
6
|
|
|
7
7
|
For every finding, ItWorksBut gives you a copy-ready fix prompt you can paste into your coding agent. It does not just say what is wrong; it tells your AI exactly what to inspect, what to change, and what not to leak.
|
|
8
8
|
|
|
9
|
-
It
|
|
9
|
+
It mostly reads files and reports findings. It does not send telemetry. The outdated-package check may invoke your local package manager, and the CLI only writes files when you explicitly ask for `todo.md` with `--todo` or `report.md` with `--report`.
|
|
10
|
+
|
|
11
|
+
## Table of Contents
|
|
12
|
+
|
|
13
|
+
- [Installation](#installation)
|
|
14
|
+
- [Quick Start](#quick-start)
|
|
15
|
+
- [Options](#options)
|
|
16
|
+
- [Terminal Experience](#terminal-experience)
|
|
17
|
+
- [GitHub Actions](#github-actions)
|
|
18
|
+
- [Configuration](#configuration)
|
|
19
|
+
- [Example Output](#example-output)
|
|
20
|
+
- [What It Detects](#what-it-detects)
|
|
21
|
+
- [What It Does Not Guarantee](#what-it-does-not-guarantee)
|
|
10
22
|
|
|
11
23
|
## Installation
|
|
12
24
|
|
|
@@ -37,10 +49,13 @@ Common commands:
|
|
|
37
49
|
|
|
38
50
|
```sh
|
|
39
51
|
itworksbut scan --path .
|
|
52
|
+
itworksbut deps
|
|
53
|
+
itworksbut stress
|
|
40
54
|
itworksbut scan --fail-on high
|
|
41
55
|
itworksbut scan --json
|
|
42
56
|
itworksbut scan --sarif > itworksbut.sarif
|
|
43
57
|
itworksbut scan --todo
|
|
58
|
+
itworksbut scan --report
|
|
44
59
|
itworksbut scan --config itworksbut.config.json
|
|
45
60
|
itworksbut scan --verbose
|
|
46
61
|
itworksbut --version
|
|
@@ -50,14 +65,24 @@ itworksbut --version
|
|
|
50
65
|
|
|
51
66
|
```text
|
|
52
67
|
itworksbut scan [options]
|
|
68
|
+
itworksbut deps [options]
|
|
69
|
+
itworksbut stress [options]
|
|
53
70
|
```
|
|
54
71
|
|
|
72
|
+
- `deps`: Run only dependency checks, including lockfile hygiene, install-script risk, audit script availability, and outdated packages.
|
|
73
|
+
- `stress`: Discover API endpoints and run a controlled Artillery load test against local or explicitly authorized targets.
|
|
55
74
|
- `--path <path>`: Scan a specific project directory. Defaults to the current directory.
|
|
56
75
|
- `--config <path>`: Use a custom config file. Defaults to `itworksbut.config.json` when present.
|
|
57
76
|
- `--fail-on <severity>`: Exit with code `1` when a finding at or above the severity exists. Levels: `critical`, `high`, `medium`, `low`, `info`. Default: `low`.
|
|
77
|
+
- `--target <url>`: Stress-test target. Defaults to `http://localhost:3000`. External targets require `--i-own-this`.
|
|
78
|
+
- `--duration <seconds>`: Stress-test duration. Default `30`, maximum `300`.
|
|
79
|
+
- `--arrival-rate <number>`: Arrival rate in requests per second. Default `5`, maximum `50`.
|
|
80
|
+
- `--max-vusers <number>`: Virtual user cap. Default `50`, maximum `100`.
|
|
81
|
+
- `--i-own-this`: Required for non-local stress-test targets.
|
|
58
82
|
- `--json`: Print machine-readable JSON only. No banner, colors, spinner, table, or extra text.
|
|
59
83
|
- `--sarif`: Print SARIF JSON for GitHub Code Scanning. No banner, colors, spinner, table, or extra text.
|
|
60
84
|
- `--todo`: Write an AI-ready `todo.md` into the scanned project with prioritized findings, fix prompts, and acceptance criteria.
|
|
85
|
+
- `--report`: Write a Markdown `report.md` into the current working directory.
|
|
61
86
|
- `--verbose`: Include scanner warnings and extra metadata in console output.
|
|
62
87
|
- `--quiet`: Print only the summary.
|
|
63
88
|
- `--no-color`: Disable colored output.
|
|
@@ -97,6 +122,24 @@ itworksbut scan --todo
|
|
|
97
122
|
|
|
98
123
|
This writes `todo.md` to the scanned project. The file is ordered by severity and includes agent rules, exact fix prompts, locations, recommendations, and final verification checkboxes.
|
|
99
124
|
|
|
125
|
+
To create a Markdown scan report:
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
itworksbut scan --report
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
This writes `report.md` to the current working directory with check statuses, summaries, details, and recommendations.
|
|
132
|
+
|
|
133
|
+
To run a controlled API stress test:
|
|
134
|
+
|
|
135
|
+
```sh
|
|
136
|
+
itworksbut stress
|
|
137
|
+
itworksbut stress --target https://my-own-api.example --i-own-this
|
|
138
|
+
itworksbut stress --report
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`stress` only tests local targets by default. For external hosts, pass `--i-own-this` to confirm that you own the target or have explicit authorization. Mutating endpoints such as `POST`, `PUT`, `PATCH`, and `DELETE` are discovered but skipped automatically.
|
|
142
|
+
|
|
100
143
|
## GitHub Actions
|
|
101
144
|
|
|
102
145
|
```yaml
|
|
@@ -166,38 +209,10 @@ release/**
|
|
|
166
209
|
## Example Output
|
|
167
210
|
|
|
168
211
|

|
|
169
|
-
✖ CRITICAL It works, but your .env is tracked.
|
|
170
|
-
✔ Check: env.env-file-tracked
|
|
171
|
-
📁 File: .env
|
|
172
|
-
🤔 Why: .env appears to be tracked by git. Secrets may be exposed.
|
|
173
|
-
🤖 prompt: You are a senior security engineer working in this repository. Fix the ItWorksBut finding env.env-file-tracked at .env. Treat this as a concrete finding. Problem: .env appears to be tracked by git. Secrets may be exposed. Required change: Remove tracked env files from git, add safe examples such as .env.example, and make sure any exposed credentials are treated as compromised. Do not print, log, or preserve raw secret values; use placeholders only. Keep existing behavior intact where possible, add or update focused tests when useful, and do not silence the check unless the underlying risk is actually fixed.
|
|
174
|
-
|
|
175
|
-
▲ HIGH It works, but your SQL query is one template string away from pain.
|
|
176
|
-
✔ Check: database.raw-sql-interpolation
|
|
177
|
-
📁 File: src/db.js:12
|
|
178
|
-
🤔 Why: Possible SQL injection risk: raw SQL appears to be built with template string interpolation.
|
|
179
|
-
🤖 prompt: You are a senior security engineer working in this repository. Fix the ItWorksBut finding database.raw-sql-interpolation at src/db.js:12. This finding is heuristic, so inspect the code first and only change behavior when the risk is real. Problem: Possible SQL injection risk: raw SQL appears to be built with template string interpolation. Required change: Replace SQL string interpolation or concatenation with parameterized queries, prepared statements, or a safe ORM query builder. Keep existing behavior intact where possible, add or update focused tests when useful, and do not silence the check unless the underlying risk is actually fixed.
|
|
180
|
-
|
|
181
|
-
SUMMARY
|
|
182
|
-
|
|
183
|
-
- ship status: DO NOT SHIP
|
|
184
|
-
- Fix the red stuff before production.
|
|
185
|
-
- total findings: 2
|
|
186
|
-
- critical: 1
|
|
187
|
-
- high: 1
|
|
188
|
-
- medium: 0
|
|
189
|
-
- low: 0
|
|
190
|
-
- info: 0
|
|
191
|
-
- fail-on: high
|
|
192
|
-
- exit decision: 1
|
|
193
|
-
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
Secret-like findings never print the full secret value. Findings report the file, line, and secret type where possible.
|
|
197
212
|
|
|
198
213
|
## What It Detects
|
|
199
214
|
|
|
200
|
-
The baseline includes
|
|
215
|
+
The baseline includes 51 modular checks:
|
|
201
216
|
|
|
202
217
|
- `git.gitignore-missing`
|
|
203
218
|
- `git.gitignore-incomplete`
|
|
@@ -211,6 +226,7 @@ The baseline includes 50 modular checks:
|
|
|
211
226
|
- `dependencies.multiple-lockfiles`
|
|
212
227
|
- `dependencies.install-scripts-risk`
|
|
213
228
|
- `dependencies.audit-script-missing`
|
|
229
|
+
- `dependencies.outdated-packages`
|
|
214
230
|
- `package.scripts-missing`
|
|
215
231
|
- `ci.no-ci-config`
|
|
216
232
|
- `ci.npm-install-instead-of-npm-ci`
|
|
@@ -257,4 +273,3 @@ Each check is a plain ESM module with an `id`, metadata, and async `run(context)
|
|
|
257
273
|
ItWorksBut is a static heuristic scanner, not a pentest, SAST replacement, dependency vulnerability database, or runtime security monitor. Findings intentionally use wording such as "possible", "potential", and "appears to" when a check is heuristic.
|
|
258
274
|
|
|
259
275
|
Use it as a CI guardrail for common project hygiene and security mistakes. For production systems, combine it with code review, tests, dependency scanning, secrets scanning, threat modeling, and focused security assessment.
|
|
260
|
-
```
|
package/bin/itworksbut.js
CHANGED
|
@@ -10,6 +10,8 @@ import { reportConsole } from '../src/reporters/consoleReporter.js';
|
|
|
10
10
|
import { reportJson } from '../src/reporters/jsonReporter.js';
|
|
11
11
|
import { reportSarif } from '../src/reporters/sarifReporter.js';
|
|
12
12
|
import { writeTodoReport } from '../src/reporters/todoReporter.js';
|
|
13
|
+
import { writeMarkdownReport } from '../src/reporters/markdownReport.js';
|
|
14
|
+
import { runStressCommand } from '../src/commands/stress.js';
|
|
13
15
|
|
|
14
16
|
async function main() {
|
|
15
17
|
const args = parseArgs(process.argv.slice(2));
|
|
@@ -24,7 +26,11 @@ async function main() {
|
|
|
24
26
|
return 0;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
if (args.command
|
|
29
|
+
if (args.command === 'stress') {
|
|
30
|
+
return await runStressCommand(args);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!['scan', 'deps'].includes(args.command)) {
|
|
28
34
|
printUsage();
|
|
29
35
|
return 2;
|
|
30
36
|
}
|
|
@@ -41,6 +47,7 @@ async function main() {
|
|
|
41
47
|
configPath: args.config,
|
|
42
48
|
failOn: args.failOn,
|
|
43
49
|
verbose: args.verbose,
|
|
50
|
+
categories: args.command === 'deps' ? ['dependencies'] : undefined,
|
|
44
51
|
});
|
|
45
52
|
if (spinner) spinner.succeed('Scan complete. Now the receipts.');
|
|
46
53
|
} catch (error) {
|
|
@@ -59,6 +66,14 @@ async function main() {
|
|
|
59
66
|
reportConsole(result, args);
|
|
60
67
|
}
|
|
61
68
|
|
|
69
|
+
if (args.report) {
|
|
70
|
+
const report = await writeMarkdownReport(result);
|
|
71
|
+
if (!args.json && !args.sarif) {
|
|
72
|
+
const verb = report.overwritten ? 'Overwrote' : 'Wrote';
|
|
73
|
+
process.stdout.write(`${verb} scan report: ${report.filePath}\n`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
62
77
|
return getExitCode(result.findings, result.config.failOn);
|
|
63
78
|
}
|
|
64
79
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "itworksbut",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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": {
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
],
|
|
37
37
|
"license": "MIT",
|
|
38
38
|
"dependencies": {
|
|
39
|
+
"artillery": "^2.0.30",
|
|
39
40
|
"boxen": "^8.0.1",
|
|
40
41
|
"chalk": "^5.6.2",
|
|
41
42
|
"cli-table3": "^0.6.5",
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { detectOutdatedPackageManager, getOutdatedCommand } from "../../utils/packageManager.js";
|
|
4
|
+
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
const CHECK_ID = "dependencies.outdated-packages";
|
|
7
|
+
const CHECK_TITLE = "Outdated packages";
|
|
8
|
+
const COMMAND_TIMEOUT_MS = 30_000;
|
|
9
|
+
const COMMAND_MAX_BUFFER = 10 * 1024 * 1024;
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
id: CHECK_ID,
|
|
13
|
+
title: CHECK_TITLE,
|
|
14
|
+
category: "dependencies",
|
|
15
|
+
severity: "info",
|
|
16
|
+
tags: ["dependencies", "maintenance"],
|
|
17
|
+
run: async (context) => {
|
|
18
|
+
const result = await runOutdatedPackagesCheck(context);
|
|
19
|
+
return { findings: [], result };
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export async function runOutdatedPackagesCheck(context, options = {}) {
|
|
24
|
+
const detected = detectOutdatedPackageManager({
|
|
25
|
+
packageJson: context.packageJson,
|
|
26
|
+
files: context.allFiles
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (detected.status === "skip") {
|
|
30
|
+
return checkResult({
|
|
31
|
+
status: "skip",
|
|
32
|
+
summary: detected.summary,
|
|
33
|
+
metadata: { reason: "missing-package-json" }
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const commandResult = await runOutdatedCommand(detected.manager, context.rootPath, options);
|
|
38
|
+
if (isMissingCommand(commandResult)) {
|
|
39
|
+
return checkResult({
|
|
40
|
+
status: "fail",
|
|
41
|
+
summary: `${detected.manager} is not installed or could not be found.`,
|
|
42
|
+
details: [
|
|
43
|
+
{
|
|
44
|
+
message: `${detected.manager} is not installed or could not be found.`,
|
|
45
|
+
command: renderCommand(commandResult),
|
|
46
|
+
exitCode: commandResult.exitCode
|
|
47
|
+
}
|
|
48
|
+
],
|
|
49
|
+
metadata: { packageManager: detected.manager }
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const parsed = parseOutdatedOutput(detected.manager, commandResult.stdout, context.packageJson);
|
|
54
|
+
if (!parsed.ok) {
|
|
55
|
+
if (!hasCommandFailure(commandResult) && isEmptyOutput(commandResult.stdout)) {
|
|
56
|
+
return checkResult({
|
|
57
|
+
status: "pass",
|
|
58
|
+
summary: "all dependencies are up to date",
|
|
59
|
+
details: [],
|
|
60
|
+
metadata: { packageManager: detected.manager }
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return checkResult({
|
|
65
|
+
status: "fail",
|
|
66
|
+
summary: `${detected.manager} outdated returned output that could not be parsed.`,
|
|
67
|
+
details: [
|
|
68
|
+
{
|
|
69
|
+
message: parsed.error,
|
|
70
|
+
command: renderCommand(commandResult),
|
|
71
|
+
exitCode: commandResult.exitCode,
|
|
72
|
+
stderr: trimText(commandResult.stderr)
|
|
73
|
+
}
|
|
74
|
+
],
|
|
75
|
+
metadata: { packageManager: detected.manager }
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if ((hasCommandFailure(commandResult) || hasEmptyExitOneFailure(commandResult)) && parsed.packages.length === 0) {
|
|
80
|
+
return checkResult({
|
|
81
|
+
status: "fail",
|
|
82
|
+
summary: `${detected.manager} outdated failed.`,
|
|
83
|
+
details: [
|
|
84
|
+
{
|
|
85
|
+
message: trimText(commandResult.stderr) || "Command exited with code " + commandResult.exitCode + ".",
|
|
86
|
+
command: renderCommand(commandResult),
|
|
87
|
+
exitCode: commandResult.exitCode
|
|
88
|
+
}
|
|
89
|
+
],
|
|
90
|
+
metadata: { packageManager: detected.manager }
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (parsed.packages.length === 0) {
|
|
95
|
+
return checkResult({
|
|
96
|
+
status: "pass",
|
|
97
|
+
summary: "all dependencies are up to date",
|
|
98
|
+
details: [],
|
|
99
|
+
metadata: { packageManager: detected.manager }
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return checkResult({
|
|
104
|
+
status: "warn",
|
|
105
|
+
summary: `${parsed.packages.length} ${parsed.packages.length === 1 ? "package" : "packages"} outdated`,
|
|
106
|
+
details: parsed.packages,
|
|
107
|
+
metadata: { packageManager: detected.manager }
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function runOutdatedCommand(manager, rootPath, options = {}) {
|
|
112
|
+
const runCommand = options.runCommand || execCommand;
|
|
113
|
+
const { command, args } = getOutdatedCommand(manager);
|
|
114
|
+
return await runCommand(command, args, { cwd: rootPath });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function parseOutdatedOutput(manager, stdout, packageJson = {}) {
|
|
118
|
+
const output = String(stdout || "").trim();
|
|
119
|
+
if (!output) return { ok: true, packages: [] };
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
if (manager === "yarn") {
|
|
123
|
+
return { ok: true, packages: parseYarnOutput(output, packageJson) };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const parsed = JSON.parse(output);
|
|
127
|
+
return { ok: true, packages: normalizeOutdatedData(parsed, packageJson) };
|
|
128
|
+
} catch (error) {
|
|
129
|
+
return {
|
|
130
|
+
ok: false,
|
|
131
|
+
packages: [],
|
|
132
|
+
error: error instanceof Error ? error.message : String(error)
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function normalizeOutdatedData(data, packageJson = {}) {
|
|
138
|
+
if (!data || typeof data !== "object") return [];
|
|
139
|
+
|
|
140
|
+
if (Array.isArray(data)) {
|
|
141
|
+
return data
|
|
142
|
+
.map((entry) => normalizePackageEntry(entry.name || entry.package, entry, packageJson))
|
|
143
|
+
.filter(Boolean);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return Object.entries(data)
|
|
147
|
+
.map(([name, entry]) => normalizePackageEntry(name, entry, packageJson))
|
|
148
|
+
.filter(Boolean);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function execCommand(command, args, options) {
|
|
152
|
+
try {
|
|
153
|
+
const result = await execFileAsync(command, args, {
|
|
154
|
+
cwd: options.cwd,
|
|
155
|
+
timeout: COMMAND_TIMEOUT_MS,
|
|
156
|
+
maxBuffer: COMMAND_MAX_BUFFER
|
|
157
|
+
});
|
|
158
|
+
return {
|
|
159
|
+
command,
|
|
160
|
+
args,
|
|
161
|
+
stdout: result.stdout || "",
|
|
162
|
+
stderr: result.stderr || "",
|
|
163
|
+
exitCode: 0
|
|
164
|
+
};
|
|
165
|
+
} catch (error) {
|
|
166
|
+
return {
|
|
167
|
+
command,
|
|
168
|
+
args,
|
|
169
|
+
stdout: error?.stdout || "",
|
|
170
|
+
stderr: error?.stderr || error?.message || "",
|
|
171
|
+
exitCode: error?.code ?? null,
|
|
172
|
+
signal: error?.signal,
|
|
173
|
+
error
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function parseYarnOutput(output, packageJson) {
|
|
179
|
+
const directJson = tryJson(output);
|
|
180
|
+
if (directJson.ok) {
|
|
181
|
+
if (directJson.value?.type === "table") return parseYarnTable(directJson.value.data, packageJson);
|
|
182
|
+
return normalizeOutdatedData(directJson.value, packageJson);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const records = output
|
|
186
|
+
.split(/\r?\n/)
|
|
187
|
+
.map((line) => line.trim())
|
|
188
|
+
.filter(Boolean)
|
|
189
|
+
.map((line) => tryJson(line))
|
|
190
|
+
.filter((record) => record.ok)
|
|
191
|
+
.map((record) => record.value);
|
|
192
|
+
|
|
193
|
+
const tableRecord = records.find((record) => record?.type === "table" && record.data);
|
|
194
|
+
if (tableRecord) return parseYarnTable(tableRecord.data, packageJson);
|
|
195
|
+
|
|
196
|
+
const packageRecords = records
|
|
197
|
+
.filter((record) => record?.data && typeof record.data === "object")
|
|
198
|
+
.map((record) => record.data);
|
|
199
|
+
if (packageRecords.length > 0) return normalizeOutdatedData(packageRecords, packageJson);
|
|
200
|
+
|
|
201
|
+
throw new Error("No parseable Yarn outdated JSON records were found.");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function parseYarnTable(data, packageJson) {
|
|
205
|
+
const head = Array.isArray(data?.head) ? data.head.map((value) => String(value).toLowerCase()) : [];
|
|
206
|
+
const body = Array.isArray(data?.body) ? data.body : [];
|
|
207
|
+
|
|
208
|
+
return body.map((row) => {
|
|
209
|
+
const entry = {
|
|
210
|
+
name: row[indexOf(head, "package", 0)],
|
|
211
|
+
current: row[indexOf(head, "current", 1)],
|
|
212
|
+
wanted: row[indexOf(head, "wanted", 2)],
|
|
213
|
+
latest: row[indexOf(head, "latest", 3)],
|
|
214
|
+
type: row[indexOf(head, "package type", 4)]
|
|
215
|
+
};
|
|
216
|
+
return normalizePackageEntry(entry.name, entry, packageJson);
|
|
217
|
+
}).filter(Boolean);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function normalizePackageEntry(name, entry, packageJson) {
|
|
221
|
+
if (!name || !entry || typeof entry !== "object") return null;
|
|
222
|
+
|
|
223
|
+
const current = stringValue(entry.current ?? entry.installed ?? entry.version);
|
|
224
|
+
const wanted = stringValue(entry.wanted ?? entry.latest);
|
|
225
|
+
const latest = stringValue(entry.latest ?? entry.wanted);
|
|
226
|
+
|
|
227
|
+
if (!current && !wanted && !latest) return null;
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
name: String(name),
|
|
231
|
+
current: current || "unknown",
|
|
232
|
+
wanted: wanted || "unknown",
|
|
233
|
+
latest: latest || "unknown",
|
|
234
|
+
type: stringValue(entry.type ?? entry.dependencyType ?? entry.packageType) || inferDependencyType(name, packageJson)
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function inferDependencyType(name, packageJson = {}) {
|
|
239
|
+
if (Object.hasOwn(packageJson.dependencies || {}, name)) return "dependencies";
|
|
240
|
+
if (Object.hasOwn(packageJson.devDependencies || {}, name)) return "devDependencies";
|
|
241
|
+
if (Object.hasOwn(packageJson.peerDependencies || {}, name)) return "peerDependencies";
|
|
242
|
+
if (Object.hasOwn(packageJson.optionalDependencies || {}, name)) return "optionalDependencies";
|
|
243
|
+
return "dependencies";
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function checkResult(result) {
|
|
247
|
+
return {
|
|
248
|
+
id: CHECK_ID,
|
|
249
|
+
title: CHECK_TITLE,
|
|
250
|
+
category: "dependencies",
|
|
251
|
+
details: [],
|
|
252
|
+
...result
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function hasCommandFailure(result) {
|
|
257
|
+
return result.exitCode !== 0 && result.exitCode !== 1;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function hasEmptyExitOneFailure(result) {
|
|
261
|
+
return result.exitCode === 1 && isEmptyOutput(result.stdout) && !isEmptyOutput(result.stderr);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function isMissingCommand(result) {
|
|
265
|
+
return result.exitCode === "ENOENT" || result.error?.code === "ENOENT";
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function isEmptyOutput(value) {
|
|
269
|
+
return String(value || "").trim() === "";
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function renderCommand(result) {
|
|
273
|
+
return [result.command, ...(result.args || [])].filter(Boolean).join(" ");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function trimText(value) {
|
|
277
|
+
const normalized = String(value || "").trim();
|
|
278
|
+
return normalized.length > 1000 ? `${normalized.slice(0, 1000)}...` : normalized;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function tryJson(value) {
|
|
282
|
+
try {
|
|
283
|
+
return { ok: true, value: JSON.parse(value) };
|
|
284
|
+
} catch {
|
|
285
|
+
return { ok: false, value: null };
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function indexOf(head, name, fallback) {
|
|
290
|
+
const index = head.indexOf(name);
|
|
291
|
+
return index === -1 ? fallback : index;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function stringValue(value) {
|
|
295
|
+
if (value === undefined || value === null || value === "") return "";
|
|
296
|
+
return String(value);
|
|
297
|
+
}
|
package/src/checks/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import lockfileMissing from "./dependencies/lockfile-missing.js";
|
|
|
10
10
|
import multipleLockfiles from "./dependencies/multiple-lockfiles.js";
|
|
11
11
|
import installScriptsRisk from "./dependencies/install-scripts-risk.js";
|
|
12
12
|
import auditScriptMissing from "./dependencies/audit-script-missing.js";
|
|
13
|
+
import outdatedPackages from "./dependencies/outdated-packages.js";
|
|
13
14
|
import packageScriptsMissing from "./package/scripts-missing.js";
|
|
14
15
|
import noCiConfig from "./ci/no-ci-config.js";
|
|
15
16
|
import npmInstallInsteadOfNpmCi from "./ci/npm-install-instead-of-npm-ci.js";
|
|
@@ -62,6 +63,7 @@ export default [
|
|
|
62
63
|
multipleLockfiles,
|
|
63
64
|
installScriptsRisk,
|
|
64
65
|
auditScriptMissing,
|
|
66
|
+
outdatedPackages,
|
|
65
67
|
packageScriptsMissing,
|
|
66
68
|
noCiConfig,
|
|
67
69
|
npmInstallInsteadOfNpmCi,
|
package/src/cli/output.js
CHANGED
|
@@ -5,15 +5,23 @@ export function printUsage() {
|
|
|
5
5
|
|
|
6
6
|
Usage:
|
|
7
7
|
itworksbut scan [options]
|
|
8
|
+
itworksbut deps [options]
|
|
9
|
+
itworksbut stress [options]
|
|
8
10
|
|
|
9
11
|
Options:
|
|
10
12
|
--path <path> Project path to scan. Defaults to current directory.
|
|
11
13
|
--config <path> Optional itworksbut.config.json path.
|
|
12
14
|
--fail-on <level> Exit 1 when findings meet or exceed this severity.
|
|
13
15
|
Levels: critical, high, medium, low, info. Default: low.
|
|
16
|
+
--target <url> Stress-test target. Defaults to http://localhost:3000.
|
|
17
|
+
--duration <sec> Stress-test duration. Default: 30, max: 300.
|
|
18
|
+
--arrival-rate <n> Stress-test arrival rate. Default: 5, max: 50.
|
|
19
|
+
--max-vusers <n> Stress-test virtual user cap. Default: 50, max: 100.
|
|
20
|
+
--i-own-this Required to stress-test non-local targets.
|
|
14
21
|
--json Print machine-readable JSON.
|
|
15
22
|
--sarif Print SARIF for GitHub Code Scanning.
|
|
16
23
|
--todo Write an AI-ready todo.md to the scanned project.
|
|
24
|
+
--report Write a Markdown scan report.md to the current directory.
|
|
17
25
|
--no-color Disable color styling.
|
|
18
26
|
--no-banner Disable the intro banner.
|
|
19
27
|
--quiet Print only the summary.
|
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", "--todo", "--verbose", "--help", "-h", "--version", "-v", "--no-color", "--no-banner", "--quiet"]);
|
|
1
|
+
const FLAG_WITH_VALUE = new Set(["--fail-on", "--config", "--path", "--target", "--duration", "--arrival-rate", "--max-vusers"]);
|
|
2
|
+
const BOOLEAN_FLAGS = new Set(["--json", "--sarif", "--todo", "--report", "--i-own-this", "--verbose", "--help", "-h", "--version", "-v", "--no-color", "--no-banner", "--quiet"]);
|
|
3
3
|
|
|
4
4
|
export function parseArgs(argv) {
|
|
5
5
|
const args = {
|
|
@@ -10,6 +10,12 @@ export function parseArgs(argv) {
|
|
|
10
10
|
json: false,
|
|
11
11
|
sarif: false,
|
|
12
12
|
todo: false,
|
|
13
|
+
report: false,
|
|
14
|
+
target: undefined,
|
|
15
|
+
duration: undefined,
|
|
16
|
+
arrivalRate: undefined,
|
|
17
|
+
maxVusers: undefined,
|
|
18
|
+
iOwnThis: false,
|
|
13
19
|
verbose: false,
|
|
14
20
|
noColor: false,
|
|
15
21
|
noBanner: false,
|
|
@@ -48,6 +54,8 @@ export function parseArgs(argv) {
|
|
|
48
54
|
if (token === "--json") args.json = true;
|
|
49
55
|
if (token === "--sarif") args.sarif = true;
|
|
50
56
|
if (token === "--todo") args.todo = true;
|
|
57
|
+
if (token === "--report") args.report = true;
|
|
58
|
+
if (token === "--i-own-this") args.iOwnThis = true;
|
|
51
59
|
if (token === "--verbose") args.verbose = true;
|
|
52
60
|
if (token === "--no-color") args.noColor = true;
|
|
53
61
|
if (token === "--no-banner") args.noBanner = true;
|
|
@@ -69,5 +77,9 @@ function assignValue(args, flag, value) {
|
|
|
69
77
|
if (flag === "--fail-on") args.failOn = value;
|
|
70
78
|
else if (flag === "--config") args.config = value;
|
|
71
79
|
else if (flag === "--path") args.path = value;
|
|
80
|
+
else if (flag === "--target") args.target = value;
|
|
81
|
+
else if (flag === "--duration") args.duration = value;
|
|
82
|
+
else if (flag === "--arrival-rate") args.arrivalRate = value;
|
|
83
|
+
else if (flag === "--max-vusers") args.maxVusers = value;
|
|
72
84
|
else throw new Error(`Unknown argument: ${flag}`);
|
|
73
85
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { validateStressOptions } from "../utils/targetSafety.js";
|
|
3
|
+
import { discoverEndpoints } from "../stress/discoverEndpoints.js";
|
|
4
|
+
import { createArtilleryConfig } from "../stress/stressConfig.js";
|
|
5
|
+
import { runArtillery } from "../stress/artilleryRunner.js";
|
|
6
|
+
import { parseArtilleryResult } from "../stress/stressResultParser.js";
|
|
7
|
+
import { reportStressConsole } from "../stress/stressRenderer.js";
|
|
8
|
+
import { writeStressMarkdownReport } from "../reporters/stressMarkdownReport.js";
|
|
9
|
+
|
|
10
|
+
export async function runStressCommand(args, options = {}) {
|
|
11
|
+
if (args.todo || args.sarif) {
|
|
12
|
+
throw new Error("The stress command supports console output, --json, and --report.");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const result = await runStress({
|
|
16
|
+
rootPath: path.resolve(args.path || "."),
|
|
17
|
+
...validateStressOptions(args)
|
|
18
|
+
}, options);
|
|
19
|
+
|
|
20
|
+
if (args.json) {
|
|
21
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
22
|
+
} else {
|
|
23
|
+
reportStressConsole(result, args);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (args.report) {
|
|
27
|
+
const report = await writeStressMarkdownReport(result);
|
|
28
|
+
if (!args.json && !args.quiet) {
|
|
29
|
+
const verb = report.overwritten ? "Overwrote" : "Wrote";
|
|
30
|
+
process.stdout.write(`${verb} stress report: ${report.filePath}\n`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return result.status === "fail" ? 1 : 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function runStress(options, runnerOptions = {}) {
|
|
38
|
+
const startedAt = new Date();
|
|
39
|
+
const discovery = await discoverEndpoints(options.rootPath);
|
|
40
|
+
const baseDetails = {
|
|
41
|
+
target: options.target,
|
|
42
|
+
duration: options.duration,
|
|
43
|
+
arrivalRate: options.arrivalRate,
|
|
44
|
+
maxVusers: options.maxVusers,
|
|
45
|
+
endpointsFound: discovery.endpoints.length,
|
|
46
|
+
safeEndpoints: discovery.safeEndpoints.length,
|
|
47
|
+
skippedEndpoints: discovery.skippedEndpoints,
|
|
48
|
+
startedAt: startedAt.toISOString()
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
if (discovery.endpoints.length === 0) {
|
|
52
|
+
return stressResult({
|
|
53
|
+
status: "skip",
|
|
54
|
+
summary: "No API endpoints found.",
|
|
55
|
+
details: {
|
|
56
|
+
...baseDetails,
|
|
57
|
+
testedEndpoints: [],
|
|
58
|
+
warnings: 0,
|
|
59
|
+
failed: 0,
|
|
60
|
+
completedAt: new Date().toISOString()
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (discovery.safeEndpoints.length === 0) {
|
|
66
|
+
return stressResult({
|
|
67
|
+
status: "fail",
|
|
68
|
+
summary: "No safe GET/HEAD endpoints can be tested automatically.",
|
|
69
|
+
details: {
|
|
70
|
+
...baseDetails,
|
|
71
|
+
testedEndpoints: [],
|
|
72
|
+
warnings: 0,
|
|
73
|
+
failed: 1,
|
|
74
|
+
completedAt: new Date().toISOString()
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const config = createArtilleryConfig({
|
|
80
|
+
target: options.target,
|
|
81
|
+
duration: options.duration,
|
|
82
|
+
arrivalRate: options.arrivalRate,
|
|
83
|
+
maxVusers: options.maxVusers,
|
|
84
|
+
endpoints: discovery.safeEndpoints
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const artillery = await runArtillery(config, {
|
|
88
|
+
rootPath: options.rootPath,
|
|
89
|
+
duration: options.duration,
|
|
90
|
+
...runnerOptions
|
|
91
|
+
});
|
|
92
|
+
const parsed = parseArtilleryResult(artillery, discovery.safeEndpoints);
|
|
93
|
+
|
|
94
|
+
return stressResult({
|
|
95
|
+
status: parsed.status,
|
|
96
|
+
summary: parsed.summary,
|
|
97
|
+
details: {
|
|
98
|
+
...baseDetails,
|
|
99
|
+
testedEndpoints: parsed.testedEndpoints,
|
|
100
|
+
warnings: parsed.warnings,
|
|
101
|
+
failed: parsed.failed,
|
|
102
|
+
artilleryError: parsed.error,
|
|
103
|
+
completedAt: new Date().toISOString()
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function stressResult(result) {
|
|
109
|
+
return {
|
|
110
|
+
id: "stress-test",
|
|
111
|
+
title: "API stress test",
|
|
112
|
+
...result
|
|
113
|
+
};
|
|
114
|
+
}
|