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.
Files changed (53) hide show
  1. package/README.md +241 -0
  2. package/bin/itworksbut.js +63 -0
  3. package/itworksbut.config.json +5 -0
  4. package/package.json +46 -0
  5. package/src/checks/auth/idor-risk.js +45 -0
  6. package/src/checks/auth/missing-auth-on-routes.js +55 -0
  7. package/src/checks/ci/no-build-step.js +23 -0
  8. package/src/checks/ci/no-ci-config.js +20 -0
  9. package/src/checks/ci/no-test-step.js +23 -0
  10. package/src/checks/ci/npm-install-instead-of-npm-ci.js +29 -0
  11. package/src/checks/database/no-migrations.js +33 -0
  12. package/src/checks/database/raw-sql-interpolation.js +41 -0
  13. package/src/checks/dependencies/audit-script-missing.js +23 -0
  14. package/src/checks/dependencies/install-scripts-risk.js +18 -0
  15. package/src/checks/dependencies/lockfile-missing.js +21 -0
  16. package/src/checks/dependencies/multiple-lockfiles.js +21 -0
  17. package/src/checks/electron/context-isolation-disabled.js +51 -0
  18. package/src/checks/electron/node-integration-enabled.js +26 -0
  19. package/src/checks/env/env-example-missing.js +28 -0
  20. package/src/checks/env/env-file-tracked.js +20 -0
  21. package/src/checks/env/frontend-secret-exposure.js +44 -0
  22. package/src/checks/env/possible-secret-in-code.js +72 -0
  23. package/src/checks/git/gitignore-incomplete.js +47 -0
  24. package/src/checks/git/gitignore-missing.js +16 -0
  25. package/src/checks/git/ignored-files-tracked.js +38 -0
  26. package/src/checks/helpers.js +122 -0
  27. package/src/checks/index.js +63 -0
  28. package/src/checks/node/cors-wildcard.js +35 -0
  29. package/src/checks/node/express-json-limit-missing.js +30 -0
  30. package/src/checks/node/helmet-missing.js +22 -0
  31. package/src/checks/node/rate-limit-missing.js +30 -0
  32. package/src/checks/package/scripts-missing.js +30 -0
  33. package/src/checks/tauri/dangerous-allowlist-or-capabilities.js +142 -0
  34. package/src/checks/web/client-side-auth-only.js +40 -0
  35. package/src/checks/web/dangerous-inner-html.js +33 -0
  36. package/src/checks/web/missing-output-sanitization.js +34 -0
  37. package/src/cli/output.js +29 -0
  38. package/src/cli/parseArgs.js +75 -0
  39. package/src/cli/terminal.js +112 -0
  40. package/src/core/config.js +51 -0
  41. package/src/core/context.js +87 -0
  42. package/src/core/fileWalker.js +44 -0
  43. package/src/core/findings.js +39 -0
  44. package/src/core/git.js +92 -0
  45. package/src/core/scanner.js +56 -0
  46. package/src/reporters/consoleReporter.js +107 -0
  47. package/src/reporters/consoleStyle.js +155 -0
  48. package/src/reporters/jsonReporter.js +17 -0
  49. package/src/reporters/sarifReporter.js +82 -0
  50. package/src/utils/fs.js +57 -0
  51. package/src/utils/mask.js +14 -0
  52. package/src/utils/packageJson.js +31 -0
  53. 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
+ });
@@ -0,0 +1,5 @@
1
+ {
2
+ "ignore": [],
3
+ "failOn": "low",
4
+ "checks": {}
5
+ }
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
+ };