itworksbut 0.1.1
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 +241 -0
- package/bin/itworksbut.js +63 -0
- package/itworksbut.config.json +5 -0
- package/package.json +46 -0
- package/src/checks/auth/idor-risk.js +45 -0
- package/src/checks/auth/missing-auth-on-routes.js +55 -0
- package/src/checks/ci/no-build-step.js +23 -0
- package/src/checks/ci/no-ci-config.js +20 -0
- package/src/checks/ci/no-test-step.js +23 -0
- package/src/checks/ci/npm-install-instead-of-npm-ci.js +29 -0
- package/src/checks/database/no-migrations.js +33 -0
- package/src/checks/database/raw-sql-interpolation.js +41 -0
- package/src/checks/dependencies/audit-script-missing.js +23 -0
- package/src/checks/dependencies/install-scripts-risk.js +18 -0
- package/src/checks/dependencies/lockfile-missing.js +21 -0
- package/src/checks/dependencies/multiple-lockfiles.js +21 -0
- package/src/checks/electron/context-isolation-disabled.js +51 -0
- package/src/checks/electron/node-integration-enabled.js +26 -0
- package/src/checks/env/env-example-missing.js +28 -0
- package/src/checks/env/env-file-tracked.js +20 -0
- package/src/checks/env/frontend-secret-exposure.js +44 -0
- package/src/checks/env/possible-secret-in-code.js +72 -0
- package/src/checks/git/gitignore-incomplete.js +47 -0
- package/src/checks/git/gitignore-missing.js +16 -0
- package/src/checks/git/ignored-files-tracked.js +38 -0
- package/src/checks/helpers.js +122 -0
- package/src/checks/index.js +63 -0
- package/src/checks/node/cors-wildcard.js +35 -0
- package/src/checks/node/express-json-limit-missing.js +30 -0
- package/src/checks/node/helmet-missing.js +22 -0
- package/src/checks/node/rate-limit-missing.js +30 -0
- package/src/checks/package/scripts-missing.js +30 -0
- package/src/checks/tauri/dangerous-allowlist-or-capabilities.js +142 -0
- package/src/checks/web/client-side-auth-only.js +40 -0
- package/src/checks/web/dangerous-inner-html.js +33 -0
- package/src/checks/web/missing-output-sanitization.js +34 -0
- package/src/cli/output.js +29 -0
- package/src/cli/parseArgs.js +75 -0
- package/src/cli/terminal.js +112 -0
- package/src/core/config.js +51 -0
- package/src/core/context.js +87 -0
- package/src/core/fileWalker.js +44 -0
- package/src/core/findings.js +39 -0
- package/src/core/git.js +92 -0
- package/src/core/scanner.js +56 -0
- package/src/reporters/consoleReporter.js +107 -0
- package/src/reporters/consoleStyle.js +155 -0
- package/src/reporters/jsonReporter.js +17 -0
- package/src/reporters/sarifReporter.js +82 -0
- package/src/utils/fs.js +57 -0
- package/src/utils/mask.js +14 -0
- package/src/utils/packageJson.js +31 -0
- package/src/utils/path.js +71 -0
package/README.md
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# ItWorksBut
|
|
2
|
+
|
|
3
|
+
ItWorksBut is a Node.js CI tool for static checks in JavaScript, Node.js, web, Tauri, and Electron vibe coding projects.
|
|
4
|
+
|
|
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
|
+
|
|
7
|
+
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
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npx itworksbut scan
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### Homebrew
|
|
16
|
+
|
|
17
|
+
After the formula is committed to the tap, install with:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
brew tap oliverjessner/tap
|
|
21
|
+
brew install itworksbut
|
|
22
|
+
itworksbut scan
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
One-line install:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
brew install oliverjessner/tap/itworksbut
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The `itworksbut` formula belongs in the Homebrew tap repo, not in this app repo:
|
|
32
|
+
|
|
33
|
+
```text
|
|
34
|
+
https://github.com/oliverjessner/homebrew-tap
|
|
35
|
+
└── Formula/
|
|
36
|
+
└── itworksbut.rb
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
This repository contains a one-command release script. It runs checks, publishes the npm package, generates the Homebrew formula, commits it to the tap, and pushes the tap:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
npm login
|
|
43
|
+
npm run publish
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Do not run `npm publish` directly. The package blocks direct npm publishing so the Homebrew tap cannot be forgotten.
|
|
47
|
+
|
|
48
|
+
Preview everything without publishing:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
npm run publish -- --dry-run
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
By default the script expects the tap checkout at `../homebrew-tap`. Override it when needed:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
npm run publish -- --tap-path /path/to/homebrew-tap
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Use `--no-push` when you want the script to commit the tap formula but leave the push to you.
|
|
61
|
+
|
|
62
|
+
## Local Usage
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
node ./bin/itworksbut.js scan
|
|
66
|
+
node ./bin/itworksbut.js scan --json
|
|
67
|
+
node ./bin/itworksbut.js scan --sarif
|
|
68
|
+
node ./bin/itworksbut.js scan --fail-on high
|
|
69
|
+
node ./bin/itworksbut.js scan --config itworksbut.config.json
|
|
70
|
+
node ./bin/itworksbut.js scan --path .
|
|
71
|
+
node ./bin/itworksbut.js scan --verbose
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`scan` is intentionally the strict/default path: all checks are enabled, only heavy generated folders are skipped, and the default `fail-on` threshold is `low` so more issues fail early. Use `--config` only when you deliberately want to tune or suppress checks.
|
|
75
|
+
|
|
76
|
+
## Terminal Experience
|
|
77
|
+
|
|
78
|
+
Normal console output is intentionally more opinionated than the machine-readable reporters:
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
node ./bin/itworksbut.js scan --theme toxic
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Console-only flags:
|
|
85
|
+
|
|
86
|
+
- `--no-color`
|
|
87
|
+
- `--no-banner`
|
|
88
|
+
- `--no-spinner`
|
|
89
|
+
- `--compact`
|
|
90
|
+
- `--quiet`
|
|
91
|
+
- `--verbose`
|
|
92
|
+
- `--theme default|toxic|mono`
|
|
93
|
+
|
|
94
|
+
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
|
+
|
|
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
|
+
## GitHub Actions
|
|
105
|
+
|
|
106
|
+
```yaml
|
|
107
|
+
name: ItWorksBut
|
|
108
|
+
|
|
109
|
+
on:
|
|
110
|
+
pull_request:
|
|
111
|
+
push:
|
|
112
|
+
branches: [main]
|
|
113
|
+
|
|
114
|
+
jobs:
|
|
115
|
+
scan:
|
|
116
|
+
runs-on: ubuntu-latest
|
|
117
|
+
steps:
|
|
118
|
+
- uses: actions/checkout@v4
|
|
119
|
+
- uses: actions/setup-node@v4
|
|
120
|
+
with:
|
|
121
|
+
node-version: 20
|
|
122
|
+
cache: npm
|
|
123
|
+
- run: npm ci
|
|
124
|
+
- run: node ./bin/itworksbut.js scan --fail-on high
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
For GitHub Code Scanning-style output:
|
|
128
|
+
|
|
129
|
+
```sh
|
|
130
|
+
node ./bin/itworksbut.js scan --sarif > itworksbut.sarif
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Configuration
|
|
134
|
+
|
|
135
|
+
Optional `itworksbut.config.json`:
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"ignore": ["dist/**", "build/**", "node_modules/**"],
|
|
140
|
+
"failOn": "low",
|
|
141
|
+
"checks": {
|
|
142
|
+
"env.env-file-tracked": true,
|
|
143
|
+
"dependencies.lockfile-missing": true,
|
|
144
|
+
"node.rate-limit-missing": false
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Checks are enabled by default. Set a check id to `false` to disable it.
|
|
150
|
+
|
|
151
|
+
This repository also has `itworksbut.self.config.json` for its own CI run. It ignores intentional test fixtures and scanner regex files. Do not use that profile if you want the highest finding rate.
|
|
152
|
+
|
|
153
|
+
Default ignored paths:
|
|
154
|
+
|
|
155
|
+
```text
|
|
156
|
+
node_modules/**
|
|
157
|
+
dist/**
|
|
158
|
+
build/**
|
|
159
|
+
.next/**
|
|
160
|
+
.nuxt/**
|
|
161
|
+
coverage/**
|
|
162
|
+
.git/**
|
|
163
|
+
target/**
|
|
164
|
+
src-tauri/target/**
|
|
165
|
+
out/**
|
|
166
|
+
release/**
|
|
167
|
+
.vite/**
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Example Output
|
|
171
|
+
|
|
172
|
+
```text
|
|
173
|
+
✖ CRITICAL It works, but your .env is tracked.
|
|
174
|
+
Check: env.env-file-tracked
|
|
175
|
+
File: .env
|
|
176
|
+
Why: .env appears to be tracked by git. Secrets may be exposed.
|
|
177
|
+
Fix: Remove it from git index, rotate secrets, and commit .env.example.
|
|
178
|
+
|
|
179
|
+
▲ HIGH It works, but your SQL query is one template string away from pain.
|
|
180
|
+
Check: database.raw-sql-interpolation
|
|
181
|
+
File: src/db.js:12
|
|
182
|
+
Why: Possible SQL injection risk: raw SQL appears to be built with template string interpolation.
|
|
183
|
+
Fix: Use parameterized queries, prepared statements, or ORM query builders instead of interpolating values into SQL strings.
|
|
184
|
+
|
|
185
|
+
SUMMARY
|
|
186
|
+
- ship status: DO NOT SHIP
|
|
187
|
+
- Fix the red stuff before production.
|
|
188
|
+
- total findings: 2
|
|
189
|
+
- critical: 1
|
|
190
|
+
- high: 1
|
|
191
|
+
- medium: 0
|
|
192
|
+
- low: 0
|
|
193
|
+
- info: 0
|
|
194
|
+
- fail-on: high
|
|
195
|
+
- exit decision: 1
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Secret-like findings never print the full secret value. Findings report the file, line, and secret type where possible.
|
|
199
|
+
|
|
200
|
+
## What It Detects
|
|
201
|
+
|
|
202
|
+
The baseline includes 30 modular checks:
|
|
203
|
+
|
|
204
|
+
- `git.gitignore-missing`
|
|
205
|
+
- `git.gitignore-incomplete`
|
|
206
|
+
- `git.ignored-files-tracked`
|
|
207
|
+
- `env.env-file-tracked`
|
|
208
|
+
- `env.env-example-missing`
|
|
209
|
+
- `env.possible-secret-in-code`
|
|
210
|
+
- `env.frontend-secret-exposure`
|
|
211
|
+
- `dependencies.lockfile-missing`
|
|
212
|
+
- `dependencies.multiple-lockfiles`
|
|
213
|
+
- `dependencies.install-scripts-risk`
|
|
214
|
+
- `dependencies.audit-script-missing`
|
|
215
|
+
- `package.scripts-missing`
|
|
216
|
+
- `ci.no-ci-config`
|
|
217
|
+
- `ci.npm-install-instead-of-npm-ci`
|
|
218
|
+
- `ci.no-build-step`
|
|
219
|
+
- `ci.no-test-step`
|
|
220
|
+
- `node.express-json-limit-missing`
|
|
221
|
+
- `node.rate-limit-missing`
|
|
222
|
+
- `node.helmet-missing`
|
|
223
|
+
- `node.cors-wildcard`
|
|
224
|
+
- `web.client-side-auth-only`
|
|
225
|
+
- `web.dangerous-inner-html`
|
|
226
|
+
- `web.missing-output-sanitization`
|
|
227
|
+
- `api.missing-auth-on-routes`
|
|
228
|
+
- `api.idor-risk`
|
|
229
|
+
- `database.raw-sql-interpolation`
|
|
230
|
+
- `database.no-migrations`
|
|
231
|
+
- `electron.node-integration-enabled`
|
|
232
|
+
- `electron.context-isolation-disabled`
|
|
233
|
+
- `tauri.dangerous-allowlist-or-capabilities`
|
|
234
|
+
|
|
235
|
+
Each check is a plain ESM module with an `id`, metadata, and async `run(context)` function. Add new checks under `src/checks/` and register them in `src/checks/index.js`.
|
|
236
|
+
|
|
237
|
+
## What It Does Not Guarantee
|
|
238
|
+
|
|
239
|
+
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.
|
|
240
|
+
|
|
241
|
+
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.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { parseArgs } from "../src/cli/parseArgs.js";
|
|
4
|
+
import { printUsage, printRuntimeError } from "../src/cli/output.js";
|
|
5
|
+
import { createScanSpinner, normalizeTheme, printIntro } from "../src/cli/terminal.js";
|
|
6
|
+
import { scanProject } from "../src/core/scanner.js";
|
|
7
|
+
import { getExitCode } from "../src/core/findings.js";
|
|
8
|
+
import { reportConsole } from "../src/reporters/consoleReporter.js";
|
|
9
|
+
import { reportJson } from "../src/reporters/jsonReporter.js";
|
|
10
|
+
import { reportSarif } from "../src/reporters/sarifReporter.js";
|
|
11
|
+
|
|
12
|
+
async function main() {
|
|
13
|
+
const args = parseArgs(process.argv.slice(2));
|
|
14
|
+
args.theme = normalizeTheme(args.theme);
|
|
15
|
+
|
|
16
|
+
if (args.help) {
|
|
17
|
+
printUsage();
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (args.command !== "scan") {
|
|
22
|
+
printUsage();
|
|
23
|
+
return 2;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
printIntro(args);
|
|
27
|
+
|
|
28
|
+
const spinner = createScanSpinner(args);
|
|
29
|
+
if (spinner) spinner.start();
|
|
30
|
+
|
|
31
|
+
let result;
|
|
32
|
+
try {
|
|
33
|
+
result = await scanProject({
|
|
34
|
+
rootPath: args.path,
|
|
35
|
+
configPath: args.config,
|
|
36
|
+
failOn: args.failOn,
|
|
37
|
+
verbose: args.verbose
|
|
38
|
+
});
|
|
39
|
+
if (spinner) spinner.succeed("Scan complete. Now the receipts.");
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (spinner) spinner.fail("Scan stopped before the receipts were printed.");
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (args.sarif) {
|
|
46
|
+
process.stdout.write(`${JSON.stringify(reportSarif(result), null, 2)}\n`);
|
|
47
|
+
} else if (args.json) {
|
|
48
|
+
process.stdout.write(`${JSON.stringify(reportJson(result), null, 2)}\n`);
|
|
49
|
+
} else {
|
|
50
|
+
reportConsole(result, args);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return getExitCode(result.findings, result.config.failOn);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
main()
|
|
57
|
+
.then((code) => {
|
|
58
|
+
process.exitCode = code;
|
|
59
|
+
})
|
|
60
|
+
.catch((error) => {
|
|
61
|
+
printRuntimeError(error);
|
|
62
|
+
process.exitCode = 2;
|
|
63
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "itworksbut",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Static CI checks for common security, repo, dependency, build, and deployment risks in JavaScript vibe coding projects.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"itworksbut": "bin/itworksbut.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"README.md",
|
|
13
|
+
"itworksbut.config.json"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "node --test",
|
|
20
|
+
"build": "node --check ./bin/itworksbut.js",
|
|
21
|
+
"lint": "node --check ./bin/itworksbut.js",
|
|
22
|
+
"check": "npm test",
|
|
23
|
+
"audit": "npm audit",
|
|
24
|
+
"dev": "node ./bin/itworksbut.js scan --verbose",
|
|
25
|
+
"publish": "npm login --auth-type=web && node ./scripts/publish.js",
|
|
26
|
+
"brew:formula": "node ./scripts/publish-brew.js",
|
|
27
|
+
"prepublishOnly": "node ./scripts/guard-npm-publish.js"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"security",
|
|
31
|
+
"ci",
|
|
32
|
+
"static-analysis",
|
|
33
|
+
"node",
|
|
34
|
+
"electron",
|
|
35
|
+
"tauri"
|
|
36
|
+
],
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"boxen": "^8.0.1",
|
|
40
|
+
"chalk": "^5.6.2",
|
|
41
|
+
"cli-table3": "^0.6.5",
|
|
42
|
+
"figlet": "^1.11.0",
|
|
43
|
+
"gradient-string": "^3.0.0",
|
|
44
|
+
"ora": "^9.4.0"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { hasOwnerKeyword, lineFromOffset, readNearby } from "../helpers.js";
|
|
2
|
+
|
|
3
|
+
const IDOR_PATTERNS = [
|
|
4
|
+
{ regex: /findUnique\s*\(\s*{[\s\S]{0,220}?where\s*:\s*{[\s\S]{0,120}?\bid\b/g, label: "findUnique by id" },
|
|
5
|
+
{ regex: /\bSELECT\b[\s\S]{0,180}?\bWHERE\b[\s\S]{0,80}?\bid\s*=\s*(?:\?|[$:]\w+|\$\{)/gi, label: "SQL lookup by id" },
|
|
6
|
+
{ regex: /["'`]\/api\/[^"'`]*\/:id["'`]/g, label: "API route with :id parameter" }
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
id: "api.idor-risk",
|
|
11
|
+
title: "Object lookup by id should be scoped to the authenticated owner",
|
|
12
|
+
category: "auth",
|
|
13
|
+
severity: "high",
|
|
14
|
+
tags: ["api", "auth", "idor", "heuristic"],
|
|
15
|
+
run: async (context) => {
|
|
16
|
+
const findings = [];
|
|
17
|
+
|
|
18
|
+
for (const file of context.textFiles) {
|
|
19
|
+
if (!/\.[cm]?[jt]sx?$/.test(file)) continue;
|
|
20
|
+
const content = await context.readFileSafe(file);
|
|
21
|
+
if (!content) continue;
|
|
22
|
+
|
|
23
|
+
for (const pattern of IDOR_PATTERNS) {
|
|
24
|
+
pattern.regex.lastIndex = 0;
|
|
25
|
+
let match;
|
|
26
|
+
while ((match = pattern.regex.exec(content)) !== null) {
|
|
27
|
+
const line = lineFromOffset(content, match.index);
|
|
28
|
+
const nearby = await readNearby(context, file, line, 8);
|
|
29
|
+
if (hasOwnerKeyword(nearby)) continue;
|
|
30
|
+
|
|
31
|
+
findings.push({
|
|
32
|
+
message: `Potential IDOR risk: ${pattern.label} appears without a nearby owner, tenant, or user scope check.`,
|
|
33
|
+
file,
|
|
34
|
+
line,
|
|
35
|
+
recommendation: "Scope object reads and writes by authenticated user, owner, account, tenant, or organization, not by id alone.",
|
|
36
|
+
heuristic: true,
|
|
37
|
+
metadata: { pattern: pattern.label }
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return findings.slice(0, 100);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { hasAuthKeyword, isServerOrApiFile, readNearby } from '../helpers.js';
|
|
2
|
+
|
|
3
|
+
const ROUTE_RE = /\b(?:app|router|fastify)\.(get|post|put|patch|delete)\s*\(\s*["'`]([^"'`]+)["'`]/g;
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
id: 'api.missing-auth-on-routes',
|
|
7
|
+
title: 'API routes should have explicit authentication',
|
|
8
|
+
category: 'auth',
|
|
9
|
+
severity: 'high',
|
|
10
|
+
tags: ['api', 'auth', 'heuristic'],
|
|
11
|
+
run: async context => {
|
|
12
|
+
const findings = [];
|
|
13
|
+
|
|
14
|
+
for (const file of context.textFiles) {
|
|
15
|
+
if (!/\.[cm]?[jt]s$/.test(file) && !isServerOrApiFile(file)) continue;
|
|
16
|
+
const content = await context.readFileSafe(file);
|
|
17
|
+
if (!content) continue;
|
|
18
|
+
|
|
19
|
+
if ((file.startsWith('pages/api/') || file.startsWith('app/api/')) && !hasAuthKeyword(content)) {
|
|
20
|
+
findings.push({
|
|
21
|
+
message: 'This API route file does not appear to contain an authentication check.',
|
|
22
|
+
file,
|
|
23
|
+
recommendation:
|
|
24
|
+
'Require authentication and authorization in API route handlers. Public routes should be documented explicitly.',
|
|
25
|
+
heuristic: true,
|
|
26
|
+
});
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const lines = content.split(/\r?\n/);
|
|
31
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
32
|
+
ROUTE_RE.lastIndex = 0;
|
|
33
|
+
const match = ROUTE_RE.exec(lines[index]);
|
|
34
|
+
if (!match) continue;
|
|
35
|
+
const routePath = match[2];
|
|
36
|
+
if (!routePath.startsWith('/api') && !isServerOrApiFile(file)) continue;
|
|
37
|
+
|
|
38
|
+
const nearby = await readNearby(context, file, index + 1, 6);
|
|
39
|
+
if (hasAuthKeyword(nearby)) continue;
|
|
40
|
+
|
|
41
|
+
findings.push({
|
|
42
|
+
message: `Possible unauthenticated API route ${routePath} appears without nearby auth middleware or checks.`,
|
|
43
|
+
file,
|
|
44
|
+
line: index + 1,
|
|
45
|
+
recommendation:
|
|
46
|
+
'Add explicit authentication middleware/checks near the route, or document why the route is intentionally public.',
|
|
47
|
+
heuristic: true,
|
|
48
|
+
metadata: { routePath },
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return findings.slice(0, 100);
|
|
54
|
+
},
|
|
55
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { collectCiFiles } from "../helpers.js";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
id: "ci.no-build-step",
|
|
5
|
+
title: "CI should run a build step",
|
|
6
|
+
category: "ci",
|
|
7
|
+
severity: "medium",
|
|
8
|
+
tags: ["ci", "build"],
|
|
9
|
+
run: async (context) => {
|
|
10
|
+
const ciFiles = await collectCiFiles(context);
|
|
11
|
+
if (ciFiles.length === 0) return [];
|
|
12
|
+
|
|
13
|
+
const combined = (await Promise.all(ciFiles.map((file) => context.readFileSafe(file)))).filter(Boolean).join("\n");
|
|
14
|
+
if (/\b(npm|pnpm|yarn)\s+(run\s+)?build\b|bun\s+run\s+build/i.test(combined)) return [];
|
|
15
|
+
|
|
16
|
+
return [
|
|
17
|
+
{
|
|
18
|
+
message: "CI configuration exists, but no build step was detected.",
|
|
19
|
+
recommendation: "Run the project build in CI, for example npm run build, before deployment or merge."
|
|
20
|
+
}
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { collectCiFiles } from "../helpers.js";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
id: "ci.no-ci-config",
|
|
5
|
+
title: "CI configuration should exist",
|
|
6
|
+
category: "ci",
|
|
7
|
+
severity: "medium",
|
|
8
|
+
tags: ["ci", "deployment"],
|
|
9
|
+
run: async (context) => {
|
|
10
|
+
const ciFiles = await collectCiFiles(context);
|
|
11
|
+
if (ciFiles.length > 0) return [];
|
|
12
|
+
|
|
13
|
+
return [
|
|
14
|
+
{
|
|
15
|
+
message: "No common CI configuration was found.",
|
|
16
|
+
recommendation: "Add a CI workflow that installs dependencies from the lockfile and runs tests, linting, and builds."
|
|
17
|
+
}
|
|
18
|
+
];
|
|
19
|
+
}
|
|
20
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { collectCiFiles } from "../helpers.js";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
id: "ci.no-test-step",
|
|
5
|
+
title: "CI should run tests",
|
|
6
|
+
category: "ci",
|
|
7
|
+
severity: "medium",
|
|
8
|
+
tags: ["ci", "tests"],
|
|
9
|
+
run: async (context) => {
|
|
10
|
+
const ciFiles = await collectCiFiles(context);
|
|
11
|
+
if (ciFiles.length === 0) return [];
|
|
12
|
+
|
|
13
|
+
const combined = (await Promise.all(ciFiles.map((file) => context.readFileSafe(file)))).filter(Boolean).join("\n");
|
|
14
|
+
if (/\b(npm|pnpm|yarn)\s+(run\s+)?test\b|bun\s+test|vitest|jest|playwright\s+test/i.test(combined)) return [];
|
|
15
|
+
|
|
16
|
+
return [
|
|
17
|
+
{
|
|
18
|
+
message: "CI configuration exists, but no test step was detected.",
|
|
19
|
+
recommendation: "Run automated tests in CI, for example npm test, before deployment or merge."
|
|
20
|
+
}
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { collectCiFiles } from "../helpers.js";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
id: "ci.npm-install-instead-of-npm-ci",
|
|
5
|
+
title: "CI should prefer npm ci over npm install",
|
|
6
|
+
category: "ci",
|
|
7
|
+
severity: "medium",
|
|
8
|
+
tags: ["ci", "dependencies", "reproducibility"],
|
|
9
|
+
run: async (context) => {
|
|
10
|
+
const findings = [];
|
|
11
|
+
for (const file of await collectCiFiles(context)) {
|
|
12
|
+
const content = await context.readFileSafe(file);
|
|
13
|
+
if (!content) continue;
|
|
14
|
+
const lines = content.split(/\r?\n/);
|
|
15
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
16
|
+
const line = lines[index];
|
|
17
|
+
if (/\bnpm\s+install\b/.test(line) && !/\bnpm\s+install\s+(-g|--global)\b/.test(line)) {
|
|
18
|
+
findings.push({
|
|
19
|
+
message: "CI appears to use npm install instead of npm ci.",
|
|
20
|
+
file,
|
|
21
|
+
line: index + 1,
|
|
22
|
+
recommendation: "Use npm ci in CI so installs are clean and lockfile-driven."
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return findings;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const DB_PACKAGES = ["prisma", "@prisma/client", "drizzle-orm", "sequelize", "knex", "sqlite", "sqlite3", "better-sqlite3", "pg", "mysql2"];
|
|
2
|
+
const MIGRATION_PATTERNS = [
|
|
3
|
+
"prisma/migrations/**",
|
|
4
|
+
"migrations/**",
|
|
5
|
+
"db/migrations/**",
|
|
6
|
+
"src/db/migrations/**",
|
|
7
|
+
"drizzle/**"
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
id: "database.no-migrations",
|
|
12
|
+
title: "Database projects should include migrations",
|
|
13
|
+
category: "database",
|
|
14
|
+
severity: "medium",
|
|
15
|
+
tags: ["database", "deployment"],
|
|
16
|
+
run: async (context) => {
|
|
17
|
+
const dbDetected =
|
|
18
|
+
DB_PACKAGES.some((name) => context.hasDependency(name) || context.hasDevDependency(name)) ||
|
|
19
|
+
context.allFiles.some((file) => file === "prisma/schema.prisma" || file.endsWith(".sql"));
|
|
20
|
+
|
|
21
|
+
if (!dbDetected) return [];
|
|
22
|
+
const hasMigrations = MIGRATION_PATTERNS.some((pattern) => context.findFiles(pattern).length > 0);
|
|
23
|
+
if (hasMigrations) return [];
|
|
24
|
+
|
|
25
|
+
return [
|
|
26
|
+
{
|
|
27
|
+
message: "Database or ORM usage appears to exist, but no migrations directory was found.",
|
|
28
|
+
recommendation: "Add versioned database migrations and run them through a controlled deployment process.",
|
|
29
|
+
heuristic: true
|
|
30
|
+
}
|
|
31
|
+
];
|
|
32
|
+
}
|
|
33
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { lineFromOffset } from "../helpers.js";
|
|
2
|
+
|
|
3
|
+
const SQL_TEMPLATE_RE = /`[^`]*\b(SELECT|INSERT|UPDATE|DELETE|WITH)\b[^`]*\$\{[^`]*`/gi;
|
|
4
|
+
const SQL_CONCAT_RE = /\b(SELECT|INSERT|UPDATE|DELETE|WITH)\b[^;\n]*(?:["'`]\s*\+|\+\s*["'`])[^;\n]*/gi;
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
id: "database.raw-sql-interpolation",
|
|
8
|
+
title: "Raw SQL should not be built with string interpolation",
|
|
9
|
+
category: "database",
|
|
10
|
+
severity: "high",
|
|
11
|
+
tags: ["database", "sql-injection"],
|
|
12
|
+
run: async (context) => {
|
|
13
|
+
const findings = [];
|
|
14
|
+
|
|
15
|
+
for (const file of context.textFiles) {
|
|
16
|
+
if (!/\.[cm]?[jt]sx?$/.test(file)) continue;
|
|
17
|
+
const content = await context.readFileSafe(file);
|
|
18
|
+
if (!content) continue;
|
|
19
|
+
|
|
20
|
+
for (const pattern of [
|
|
21
|
+
{ regex: SQL_TEMPLATE_RE, label: "template string interpolation" },
|
|
22
|
+
{ regex: SQL_CONCAT_RE, label: "string concatenation" }
|
|
23
|
+
]) {
|
|
24
|
+
pattern.regex.lastIndex = 0;
|
|
25
|
+
let match;
|
|
26
|
+
while ((match = pattern.regex.exec(content)) !== null) {
|
|
27
|
+
findings.push({
|
|
28
|
+
message: `Possible SQL injection risk: raw SQL appears to be built with ${pattern.label}.`,
|
|
29
|
+
file,
|
|
30
|
+
line: lineFromOffset(content, match.index),
|
|
31
|
+
recommendation: "Use parameterized queries, prepared statements, or ORM query builders instead of interpolating values into SQL strings.",
|
|
32
|
+
heuristic: true,
|
|
33
|
+
metadata: { pattern: pattern.label }
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return findings.slice(0, 100);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
id: "dependencies.audit-script-missing",
|
|
3
|
+
title: "Dependency security audit should be available",
|
|
4
|
+
category: "dependencies",
|
|
5
|
+
severity: "low",
|
|
6
|
+
tags: ["dependencies", "supply-chain", "ci"],
|
|
7
|
+
run: async (context) => {
|
|
8
|
+
if (!context.packageJson) return [];
|
|
9
|
+
const scripts = context.packageJson.scripts || {};
|
|
10
|
+
const hasAudit = Object.entries(scripts).some(([name, command]) => {
|
|
11
|
+
return /\b(audit|security|sca|snyk|semgrep)\b/i.test(name) || /\b(npm|pnpm|yarn)\s+audit\b|snyk|semgrep/i.test(String(command));
|
|
12
|
+
});
|
|
13
|
+
if (hasAudit) return [];
|
|
14
|
+
|
|
15
|
+
return [
|
|
16
|
+
{
|
|
17
|
+
message: "package.json does not appear to define an audit or security script.",
|
|
18
|
+
file: "package.json",
|
|
19
|
+
recommendation: "Add an npm audit or equivalent SCA/security script and run it in CI."
|
|
20
|
+
}
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const INSTALL_SCRIPTS = ["preinstall", "install", "postinstall"];
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
id: "dependencies.install-scripts-risk",
|
|
5
|
+
title: "Install lifecycle scripts should be reviewed",
|
|
6
|
+
category: "dependencies",
|
|
7
|
+
severity: "medium",
|
|
8
|
+
tags: ["dependencies", "supply-chain", "npm"],
|
|
9
|
+
run: async (context) => {
|
|
10
|
+
const scripts = context.packageJson?.scripts || {};
|
|
11
|
+
return INSTALL_SCRIPTS.filter((scriptName) => scripts[scriptName]).map((scriptName) => ({
|
|
12
|
+
message: `package.json defines an npm ${scriptName} lifecycle script. This can run during dependency installation.`,
|
|
13
|
+
file: "package.json",
|
|
14
|
+
recommendation: "Review whether the install-time script is necessary. In CI, prefer npm ci and consider --ignore-scripts where appropriate.",
|
|
15
|
+
metadata: { scriptName }
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
18
|
+
};
|