codevet-cli 1.1.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/.gitleaks.toml ADDED
@@ -0,0 +1,91 @@
1
+ title = "codevet-gitleaks-config"
2
+
3
+ [extend]
4
+ useDefault = true
5
+
6
+ # --no-git mode (used so CodeVet works even outside a git repo, and catches
7
+ # uncommitted files) scans the raw filesystem and does NOT respect
8
+ # .gitignore. Without this, build caches, dependency folders, and other
9
+ # directories that can never actually leak via git get scanned anyway,
10
+ # producing pure noise. Confirmed in real testing: a Next.js .next/ build
11
+ # cache produced 6 false "Generic API Key" findings before this was added.
12
+ [allowlist]
13
+ paths = [
14
+ '''(^|[/\\])node_modules([/\\]|$)''',
15
+ '''(^|[/\\])\.next([/\\]|$)''',
16
+ '''(^|[/\\])\.nuxt([/\\]|$)''',
17
+ '''(^|[/\\])dist([/\\]|$)''',
18
+ '''(^|[/\\])build([/\\]|$)''',
19
+ '''(^|[/\\])out([/\\]|$)''',
20
+ '''(^|[/\\])coverage([/\\]|$)''',
21
+ '''(^|[/\\])\.turbo([/\\]|$)''',
22
+ '''(^|[/\\])\.git([/\\]|$)''',
23
+ '''(^|[/\\])target([/\\]|$)''',
24
+ '''(^|[/\\])vendor([/\\]|$)''',
25
+ '''(^|[/\\])__pycache__([/\\]|$)''',
26
+ '''(^|[/\\])\.venv([/\\]|$)''',
27
+ '''(^|[/\\])venv([/\\]|$)''',
28
+ '''(^|[/\\])\.cache([/\\]|$)''',
29
+ # Confirmed as a real false-positive source in testing: a single
30
+ # Angular project's .angular/cache/ produced ~90 "Generic API Key"
31
+ # false positives from webpack .pack binary cache blobs — their
32
+ # compressed/hashed content trips generic entropy-based secret
33
+ # detection, but they're compiler cache artifacts, never application
34
+ # source. Angular commits nothing sensitive here by design.
35
+ '''(^|[/\\])\.angular([/\\]|$)''',
36
+ # Yarn 2+ (Berry) commits vendored CLI binaries under .yarn/releases/
37
+ # and .yarn/cache/ by design (the "zero-installs" pattern) — these are
38
+ # third-party tool code, not application secrets, and the same
39
+ # generic-entropy false-positive risk applies as with webpack packs.
40
+ '''(^|[/\\])\.yarn([/\\]|$)''',
41
+ # Webpack/build-tool cache ".pack" files are binary blobs regardless of
42
+ # which directory they end up in — matched by extension as a safety net
43
+ # beyond the .angular directory rule above, since other bundlers may use
44
+ # differently-named cache folders we haven't seen a false positive from
45
+ # yet. Real secrets essentially never live in a compiled binary cache
46
+ # file, so this trade-off favors eliminating noise.
47
+ '''\.pack$''',
48
+ ]
49
+
50
+ [[rules]]
51
+ id = "generic-connection-string-credentials"
52
+ description = "Hardcoded username/password embedded in a database or service connection string"
53
+ regex = '''(?i)(postgres(ql)?|mysql|mongodb(\+srv)?|redis|amqp|rabbitmq):\/\/[^:\/\s"']+:[^@\/\s"']+@[^\/\s"']+'''
54
+ tags = ["credentials", "connection-string"]
55
+
56
+ # Real leaked credentials live in application source/config files, not prose
57
+ # documentation — confirmed in real testing: 15 false positives came from
58
+ # *.md reference/skill docs containing illustrative example connection
59
+ # strings, not actual secrets. Markdown docs are excluded from this rule
60
+ # specifically (not globally) since a real secret pasted into a README is
61
+ # still worth catching by the default rules.
62
+ [[rules.allowlists]]
63
+ paths = [
64
+ '''\.md$''',
65
+ '''(^|[/\\])references([/\\]|$)''',
66
+ '''(^|[/\\])docs?([/\\]|$)''',
67
+ '''(^|[/\\])examples?([/\\]|$)''',
68
+ ]
69
+ regexes = [
70
+ '''://(user|username|admin|test|example|localhost|127\.0\.0\.1):(password|pass|test|example|changeme)@''',
71
+ ]
72
+
73
+ # Supabase's service role key bypasses Row Level Security entirely — it
74
+ # must never reach client code. Two real, statically-detectable patterns:
75
+ # (1) the key itself assigned to a SERVICE_ROLE-named variable, and
76
+ # (2) the naming mistake alone (NEXT_PUBLIC_/VITE_ prefix on a
77
+ # SERVICE_ROLE var), which guarantees the bundler ships it to the browser
78
+ # even before any real value is filled in.
79
+ [[rules]]
80
+ id = "supabase-service-role-key-assignment"
81
+ description = "Supabase service_role key assigned in code — this key bypasses Row Level Security and must never reach client-side code"
82
+ regex = '''(?i)(SUPABASE[_-]?SERVICE[_-]?ROLE[_-]?KEY|service_role_key)\s*[:=]\s*['"]eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+['"]'''
83
+ tags = ["credentials", "supabase"]
84
+
85
+ [[rules]]
86
+ id = "supabase-service-role-key-exposed-to-client"
87
+ description = "A SUPABASE service_role env var is prefixed for client-side exposure (NEXT_PUBLIC_/VITE_/REACT_APP_) — this ships the key straight into the browser bundle regardless of its actual value"
88
+ regex = '''(?i)(NEXT_PUBLIC_|VITE_|REACT_APP_)[A-Z_]*SERVICE[_-]?ROLE[_-]?KEY'''
89
+ tags = ["credentials", "supabase", "misconfiguration"]
90
+
91
+
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CodeVet Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to
7
+ deal in the Software without restriction, including without limitation the
8
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
9
+ sell copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
21
+ DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,171 @@
1
+ # CodeVet
2
+
3
+ **Vet your code before it ships.** A free, open-source security co-pilot for
4
+ developers who aren't security experts — students, solo/indie developers,
5
+ and small teams who can't justify an enterprise AppSec platform yet.
6
+
7
+ CodeVet doesn't invent its own detection logic. It orchestrates trusted,
8
+ independently-maintained tools (`gitleaks`, `npm audit`, `pip-audit`,
9
+ `bearer`) and translates their output into plain language: what's wrong,
10
+ how severe it is, why it matters, and — where possible — the exact working
11
+ fix code, not just a description.
12
+
13
+ ```
14
+ $ npx codevet scan
15
+
16
+ CodeVet — scanning .
17
+
18
+ Detected: node (package.json)
19
+
20
+ Checking for exposed secrets...
21
+ ✖ 1 exposed secret(s) found: .env:12 — not gitignored
22
+
23
+ Checking dependencies for known vulnerabilities...
24
+ ✖ axios — HIGH — MITM via proxy config prototype pollution — fix: axios@1.19.0
25
+
26
+ Checking for missing security middleware...
27
+ CRITICAL Table 'payments' may be missing Row Level Security
28
+ ✖ HIGH No rate limiting found
29
+ ○ MODERATE No security headers configured
30
+ Suggested fix — security/helmet.config.js: [real, ready-to-paste code]
31
+
32
+ Checking personal data flow (via bearer)...
33
+ ✔ No personal-data-flow risks found.
34
+ ```
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ npm install -g codevet-cli # global — run `codevet` from any project
40
+ # or
41
+ npm install --save-dev codevet-cli # per-project — run via `npx codevet`
42
+ ```
43
+
44
+ ## Use
45
+
46
+ ```bash
47
+ codevet scan # scan the current directory
48
+ codevet scan ./some/folder # scan a specific local folder
49
+ codevet scan https://github.com/user/repo # review a repo before you trust it
50
+ codevet fix # safely upgrade flagged dependencies
51
+ codevet fix --force # allow major-version upgrades
52
+ codevet remove-dependency <name> # explicitly uninstall a flagged package
53
+ codevet clean ./some-repo-you-kept # remove a repo CodeVet cloned earlier
54
+ codevet config status # see which checks are enabled
55
+ codevet config disable <check> # turn a check off for this project
56
+ ```
57
+
58
+ Full command reference: [`docs/HELP.md`](./docs/HELP.md).
59
+
60
+ ## What it checks today
61
+
62
+ | Check | Tool | Severity model |
63
+ |---|---|---|
64
+ | Leaked secrets — API keys, credentials, connection strings, Supabase service-role-key exposure | `gitleaks` (extended ruleset) | Every finding is real and confirmed |
65
+ | Dependency vulnerabilities (Node) | `npm audit` | CRITICAL/HIGH/MODERATE/LOW from the advisory database. Skipped (not failed) on pnpm-managed projects — a confirmed bug in npm itself, run `pnpm audit` directly for those |
66
+ | Dependency vulnerabilities (Python) | `pip-audit` | Unranked — PyPA's database has no severity field; prioritize by whether a fix exists |
67
+ | Missing security middleware — no helmet, no rate limiting, wide-open CORS, error responses leaking internals, missing Supabase Row Level Security | CodeVet's own heuristic scanner | CRITICAL/HIGH/MODERATE, each with a `Verify:` note on how the check could be wrong |
68
+ | Personal data flow — logging or transmitting PII, secrets, etc. | `bearer` | CRITICAL/HIGH/MODERATE/LOW. **No native Windows build** — unavailable there, everything else still works |
69
+
70
+ **CRITICAL** means fix it before anyone else touches the code. **HIGH**
71
+ means fix before launch. **MODERATE** is real but rarely the sole cause of
72
+ an incident. See [`docs/SECURITY-CHECKLIST.md`](./docs/SECURITY-CHECKLIST.md)
73
+ for the full severity-annotated checklist this is built around.
74
+
75
+ ## Reviewing an unfamiliar repo before you trust it
76
+
77
+ ```bash
78
+ codevet scan https://github.com/someone/some-repo
79
+ ```
80
+
81
+ Clones to a temp folder, scans it, and asks before keeping anything if
82
+ issues are found. Decline and nothing is left on disk. Accept and it's
83
+ moved next to where you ran the command, with a provenance marker so
84
+ `codevet clean` can safely identify and remove it later.
85
+
86
+ ## Running automatically on every PR
87
+
88
+ ```yaml
89
+ # .github/workflows/codevet.yml
90
+ name: CodeVet
91
+ on: [pull_request]
92
+ jobs:
93
+ scan:
94
+ runs-on: ubuntu-latest
95
+ steps:
96
+ - uses: actions/checkout@v4
97
+ - uses: i-akb25/codevet@v1
98
+ ```
99
+
100
+ Posts results as a PR comment. Fails the check on critical/high findings
101
+ by default — see [`docs/HELP.md`](./docs/HELP.md) to make it advisory-only.
102
+
103
+ ## Works inside your AI coding CLI
104
+
105
+ CodeVet ships as an Agent Skill — ask Claude Code, Cursor, or OpenCode to
106
+ "scan this project with CodeVet" and it runs directly in conversation. See
107
+ [`AGENTS.md`](./AGENTS.md).
108
+
109
+ ## What it doesn't do (see [`TERMS.md`](./TERMS.md) for the full scope)
110
+
111
+ Not a malware/antivirus scanner. Not a replacement for a professional
112
+ security audit on anything handling real user data or payments at scale.
113
+ A clean scan means the checks CodeVet currently runs found nothing — not a
114
+ certification.
115
+
116
+ ## About the Socket.dev supply-chain score
117
+
118
+ CodeVet's automated Socket.dev score flags Install scripts, Network access,
119
+ Shell access, and Filesystem access as "supply chain risk." These are all
120
+ expected and necessary — CodeVet downloads vetted, official scanner
121
+ binaries (gitleaks, bearer) at install time or on first use, invokes them
122
+ via the shell to run a scan, and reads your project's files to scan them.
123
+ That's the whole job. Automated scoring can't distinguish that from
124
+ something malicious without deeper context — see
125
+ [`PRIVACY.md`](./PRIVACY.md) for exactly what each of these does and
126
+ where any data goes.
127
+
128
+ ## Why this exists
129
+
130
+ Every existing free security tool — Semgrep, Trivy, GitGuardian — is built
131
+ for teams that already have security expertise. Nobody was serving the
132
+ developer who's never heard of a CVE and just wants to know if it's safe
133
+ to ship. That gap is what CodeVet is for. More in
134
+ [`docs/ABOUT.md`](./docs/ABOUT.md).
135
+
136
+ ## Repository structure
137
+
138
+ See [`INFO.md`](./INFO.md) for a complete, file-by-file breakdown of this
139
+ repo — what everything is, who needs to touch it, and how to use it.
140
+
141
+ ## Contributing
142
+
143
+ See [`CONTRIBUTING.md`](./CONTRIBUTING.md) — this project deliberately
144
+ avoids "vibe coding": every change is type-checked, tested against a real
145
+ reproduced scenario, and dependency-audited before merge.
146
+
147
+ ## Security
148
+
149
+ Found a vulnerability in CodeVet itself? See [`SECURITY.md`](./SECURITY.md)
150
+ for private disclosure — please don't open a public issue for it.
151
+
152
+ ## Privacy
153
+
154
+ CodeVet runs entirely locally — no account, no telemetry, no CodeVet
155
+ server. See [`PRIVACY.md`](./PRIVACY.md) for exactly what data each check
156
+ touches and where it goes (mainly: `npm audit` talks to the npm registry,
157
+ same as running it yourself).
158
+
159
+ ## Author
160
+
161
+ **Anurag Kumar Bharti**
162
+ Software Engineer
163
+
164
+ - **Portfolio:** [https://ace-akb.vercel.app](https://ace-akb.vercel.app)
165
+ - **GitHub:** [https://github.com/i-akb25](https://github.com/i-akb25)
166
+ - **LinkedIn:** [https://linkedin.com/in/anuragkumarbharti](https://linkedin.com/in/anuragkumarbharti)
167
+ - **Email:** anuragbhartiee25@gmail.com
168
+
169
+ ## License
170
+
171
+ MIT — see [`LICENSE`](./LICENSE).
package/dist/config.js ADDED
@@ -0,0 +1,44 @@
1
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ const DEFAULT_CONFIG = { secrets: true, dependencies: true, hygiene: true, dataFlow: true };
5
+ function configPath(projectRoot) {
6
+ return join(projectRoot, ".codevet", "config.json");
7
+ }
8
+ /**
9
+ * Loads per-project scanner toggles. IMPORTANT: only call this for a
10
+ * project the user actually owns/controls locally. Never call it on a
11
+ * freshly-cloned, not-yet-reviewed target — a malicious repo could ship
12
+ * its own .codevet/config.json set to "disabled" and bypass every check
13
+ * just by being cloned. Untrusted-clone scans always use the full default
14
+ * config, ignoring whatever the repo itself contains.
15
+ */
16
+ export async function loadConfig(projectRoot) {
17
+ const path = configPath(projectRoot);
18
+ if (!existsSync(path))
19
+ return { ...DEFAULT_CONFIG };
20
+ try {
21
+ const raw = await readFile(path, "utf-8");
22
+ return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
23
+ }
24
+ catch {
25
+ return { ...DEFAULT_CONFIG };
26
+ }
27
+ }
28
+ export async function saveConfig(projectRoot, config) {
29
+ const dir = join(projectRoot, ".codevet");
30
+ await mkdir(dir, { recursive: true });
31
+ await writeFile(configPath(projectRoot), JSON.stringify(config, null, 2));
32
+ await ensureGitignored(projectRoot);
33
+ }
34
+ async function ensureGitignored(projectRoot) {
35
+ const gitignorePath = join(projectRoot, ".gitignore");
36
+ const existing = existsSync(gitignorePath)
37
+ ? await readFile(gitignorePath, "utf-8")
38
+ : "";
39
+ if (existing.split("\n").some((line) => line.trim() === ".codevet/")) {
40
+ return;
41
+ }
42
+ const separator = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
43
+ await writeFile(gitignorePath, existing + separator + ".codevet/\n");
44
+ }
@@ -0,0 +1,28 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ const SIGNALS = [
4
+ { stack: "node", file: "package.json" },
5
+ { stack: "python", file: "requirements.txt" },
6
+ { stack: "python", file: "pyproject.toml" },
7
+ { stack: "android", file: "build.gradle" },
8
+ { stack: "android", file: "build.gradle.kts" },
9
+ { stack: "ios", file: "Podfile" },
10
+ ];
11
+ /**
12
+ * Detects which stacks are present at the root of a project directory.
13
+ * A repo can match more than one stack — that's expected, not an error.
14
+ */
15
+ export function detectStack(projectRoot) {
16
+ const found = new Map();
17
+ for (const signal of SIGNALS) {
18
+ if (found.has(signal.stack))
19
+ continue;
20
+ if (existsSync(join(projectRoot, signal.file))) {
21
+ found.set(signal.stack, signal.file);
22
+ }
23
+ }
24
+ return Array.from(found.entries()).map(([stack, matchedOn]) => ({
25
+ stack,
26
+ matchedOn,
27
+ }));
28
+ }
@@ -0,0 +1,311 @@
1
+ /**
2
+ * Real, working code templates shown as suggested fixes. Each one is
3
+ * plain, dependency-minimal Express middleware — matches the "no
4
+ * unnecessary abstraction" engineering rule. These are not generated per
5
+ * finding; they're static, reviewed content, versioned here so updates
6
+ * propagate to every future scan instead of going stale in someone's repo.
7
+ */
8
+ export const HELMET_CONFIG = `// security/helmet.config.js
9
+ const helmet = require('helmet');
10
+
11
+ module.exports = helmet({
12
+ contentSecurityPolicy: {
13
+ directives: {
14
+ defaultSrc: ["'self'"],
15
+ scriptSrc: ["'self'"],
16
+ objectSrc: ["'none'"],
17
+ },
18
+ },
19
+ hsts: { maxAge: 31536000, includeSubDomains: true },
20
+ });
21
+
22
+ // Usage: app.use(require('./security/helmet.config'));
23
+ `;
24
+ export const CORS_CONFIG = `// security/cors.config.js
25
+ const cors = require('cors');
26
+
27
+ // Replace with your actual allowed origins — never use '*' once you have
28
+ // real users, since it lets any website read your API's responses.
29
+ const ALLOWED_ORIGINS = (process.env.ALLOWED_ORIGINS || '').split(',').filter(Boolean);
30
+
31
+ module.exports = cors({
32
+ origin(origin, callback) {
33
+ if (!origin || ALLOWED_ORIGINS.includes(origin)) {
34
+ callback(null, true);
35
+ } else {
36
+ callback(new Error('Not allowed by CORS'));
37
+ }
38
+ },
39
+ credentials: true,
40
+ });
41
+
42
+ // Usage: app.use(require('./security/cors.config'));
43
+ // .env: ALLOWED_ORIGINS=https://yourapp.com,https://staging.yourapp.com
44
+ `;
45
+ export const ACCOUNT_BACKOFF = `// security/accountBackoff.js
46
+ // Per-account exponential backoff — complements a per-IP rate limiter,
47
+ // since an attacker distributing attempts across many IPs would otherwise
48
+ // bypass IP-based limiting entirely. Uses exponential backoff (delay
49
+ // doubles each failed attempt) rather than a hard lockout, since hard
50
+ // lockouts let an attacker lock a real user out of their own account (a
51
+ // denial-of-service against your own users).
52
+ //
53
+ // const { checkAccountBackoff, recordFailedAttempt, recordSuccessfulAttempt } = require('./security/accountBackoff');
54
+ //
55
+ // app.post('/login', async (req, res) => {
56
+ // const { email } = req.body;
57
+ // const backoff = checkAccountBackoff(email);
58
+ // if (backoff.blocked) {
59
+ // return res.status(429).json({ error: \`Try again in \${backoff.retryAfterSeconds}s\` });
60
+ // }
61
+ // const valid = await verifyPassword(...);
62
+ // if (!valid) {
63
+ // recordFailedAttempt(email);
64
+ // return res.status(401).json({ error: 'Invalid credentials' });
65
+ // }
66
+ // recordSuccessfulAttempt(email);
67
+ // // ...issue token
68
+ // });
69
+ //
70
+ // In-memory by default — fine for a single instance. For multi-instance
71
+ // deployments, swap the Map for Redis (same interface, different backing
72
+ // store) so backoff state is shared across instances.
73
+
74
+ const attempts = new Map();
75
+
76
+ const BASE_DELAY_MS = parseInt(process.env.AUTH_BACKOFF_BASE_MS ?? '1000', 10);
77
+ const MAX_DELAY_MS = parseInt(process.env.AUTH_BACKOFF_MAX_MS ?? '300000', 10);
78
+ const RESET_AFTER_MS = parseInt(process.env.AUTH_BACKOFF_RESET_MS ?? '3600000', 10);
79
+
80
+ function checkAccountBackoff(accountKey) {
81
+ const record = attempts.get(accountKey);
82
+ if (!record) return { blocked: false };
83
+
84
+ const elapsedSinceLastAttempt = Date.now() - record.lastAttemptAt;
85
+ if (elapsedSinceLastAttempt > RESET_AFTER_MS) {
86
+ attempts.delete(accountKey);
87
+ return { blocked: false };
88
+ }
89
+
90
+ const requiredDelay = Math.min(BASE_DELAY_MS * 2 ** record.failCount, MAX_DELAY_MS);
91
+
92
+ if (elapsedSinceLastAttempt < requiredDelay) {
93
+ return { blocked: true, retryAfterSeconds: Math.ceil((requiredDelay - elapsedSinceLastAttempt) / 1000) };
94
+ }
95
+
96
+ return { blocked: false };
97
+ }
98
+
99
+ function recordFailedAttempt(accountKey) {
100
+ const record = attempts.get(accountKey) ?? { failCount: 0, lastAttemptAt: 0 };
101
+ record.failCount += 1;
102
+ record.lastAttemptAt = Date.now();
103
+ attempts.set(accountKey, record);
104
+ }
105
+
106
+ function recordSuccessfulAttempt(accountKey) {
107
+ attempts.delete(accountKey);
108
+ }
109
+
110
+ module.exports = { checkAccountBackoff, recordFailedAttempt, recordSuccessfulAttempt };
111
+ `;
112
+ export const RATE_LIMITER = `// security/rateLimiter.middleware.js
113
+ const rateLimit = require('express-rate-limit');
114
+
115
+ // Stricter limits on auth routes, looser on general API traffic — apply
116
+ // the right one per route rather than one blanket limiter for everything.
117
+ function makeLimiter({ windowMs, max }) {
118
+ return rateLimit({
119
+ windowMs,
120
+ max,
121
+ standardHeaders: true,
122
+ legacyHeaders: false,
123
+ message: { error: 'Too many requests, please try again later.' },
124
+ });
125
+ }
126
+
127
+ module.exports = {
128
+ authLimiter: makeLimiter({
129
+ windowMs: Number(process.env.AUTH_RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000,
130
+ max: Number(process.env.AUTH_RATE_LIMIT_MAX) || 5,
131
+ }),
132
+ publicLimiter: makeLimiter({
133
+ windowMs: Number(process.env.PUBLIC_RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000,
134
+ max: Number(process.env.PUBLIC_RATE_LIMIT_MAX) || 100,
135
+ }),
136
+ authenticatedLimiter: makeLimiter({
137
+ windowMs: Number(process.env.AUTHED_RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000,
138
+ max: Number(process.env.AUTHED_RATE_LIMIT_MAX) || 300,
139
+ }),
140
+ };
141
+
142
+ // Usage: app.use('/login', authLimiter); app.use('/api', publicLimiter);
143
+ // Thresholds are env-configurable, not hardcoded, per the checklist.
144
+ `;
145
+ export const ERROR_HANDLER = `// security/errorHandler.middleware.js
146
+ // Must be registered LAST, after all routes.
147
+ module.exports = function errorHandler(err, req, res, next) {
148
+ // Full details go to server-side logs — never to the client.
149
+ console.error(err);
150
+
151
+ // The client only ever sees a generic message, never a stack trace,
152
+ // file path, or raw database error.
153
+ res.status(err.statusCode || 500).json({
154
+ error: 'Something went wrong. Please try again.',
155
+ });
156
+ };
157
+
158
+ // Usage: app.use(errorHandler); // after every other app.use()/route
159
+ `;
160
+ export const HASH_PASSWORD = `// security/auth/hashPassword.js
161
+ const argon2 = require('argon2');
162
+
163
+ async function hashPassword(plainPassword) {
164
+ return argon2.hash(plainPassword);
165
+ }
166
+
167
+ async function verifyPassword(plainPassword, hash) {
168
+ return argon2.verify(hash, plainPassword);
169
+ }
170
+
171
+ module.exports = { hashPassword, verifyPassword };
172
+ `;
173
+ export const JWT_TOKEN = `// security/auth/generateToken.js
174
+ const jwt = require('jsonwebtoken');
175
+
176
+ const SECRET = process.env.JWT_SECRET;
177
+ if (!SECRET) {
178
+ throw new Error('JWT_SECRET is not set — refusing to start without it.');
179
+ }
180
+
181
+ function generateToken(payload, expiresIn = '15m') {
182
+ return jwt.sign(payload, SECRET, { expiresIn });
183
+ }
184
+
185
+ function verifyToken(token) {
186
+ return jwt.verify(token, SECRET); // throws on invalid/expired
187
+ }
188
+
189
+ module.exports = { generateToken, verifyToken };
190
+ // Pair a short-lived access token with a separate, longer-lived refresh
191
+ // token stored server-side (or httpOnly cookie) — never a single
192
+ // long-lived token for everything.
193
+ `;
194
+ export const VALIDATION_SCHEMA = `// security/validation/authSchemas.js
195
+ const { z } = require('zod');
196
+
197
+ // Strict schema: type, length, and format all enforced. Anything that
198
+ // doesn't match is REJECTED, not silently sanitized/escaped.
199
+ const signupSchema = z.object({
200
+ email: z.string().email().max(254),
201
+ password: z.string().min(12).max(128),
202
+ });
203
+
204
+ const loginSchema = z.object({
205
+ email: z.string().email().max(254),
206
+ password: z.string().min(1).max(128),
207
+ });
208
+
209
+ module.exports = { signupSchema, loginSchema };
210
+ // Usage: const data = signupSchema.parse(req.body); // throws on invalid input
211
+ `;
212
+ export const ENV_EXAMPLE = `# .env.example — copy to .env and fill in real values. Never commit .env itself.
213
+ JWT_SECRET=
214
+ ALLOWED_ORIGINS=https://yourapp.com
215
+ AUTH_RATE_LIMIT_MAX=5
216
+ PUBLIC_RATE_LIMIT_MAX=100
217
+ DATABASE_URL=
218
+ `;
219
+ export const SECURITY_CHECK_WORKFLOW = `# .github/workflows/security-check.yml
220
+ name: security-check
221
+ on: [push, pull_request]
222
+ jobs:
223
+ audit:
224
+ runs-on: ubuntu-latest
225
+ steps:
226
+ - uses: actions/checkout@v4
227
+ - uses: actions/setup-node@v4
228
+ with: { node-version: 20 }
229
+ - run: npm ci
230
+ - run: npm audit --audit-level=high
231
+ - uses: gitleaks/gitleaks-action@v2
232
+ env:
233
+ GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
234
+ `;
235
+ export const PRE_COMMIT_HOOK = `#!/usr/bin/env sh
236
+ # .husky/pre-commit — requires: npm install -D husky && npx husky init
237
+ npx gitleaks protect --staged --no-banner || {
238
+ echo "Blocked: gitleaks found a potential secret in your staged changes.";
239
+ exit 1;
240
+ }
241
+ `;
242
+ export const FILE_UPLOAD = `// security/fileUpload.middleware.js
243
+ const multer = require('multer');
244
+ const path = require('node:path');
245
+ const crypto = require('node:crypto');
246
+
247
+ const ALLOWED_MIME_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp', 'application/pdf']);
248
+ const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
249
+
250
+ // Store outside the web root — never somewhere a webserver would execute
251
+ // an uploaded file (e.g. never inside a directory served with script
252
+ // execution enabled).
253
+ const UPLOAD_DIR = path.join(process.cwd(), 'private-uploads');
254
+
255
+ const storage = multer.diskStorage({
256
+ destination: UPLOAD_DIR,
257
+ filename(req, file, cb) {
258
+ // Never trust the original filename — generate a random one instead.
259
+ cb(null, crypto.randomBytes(16).toString('hex'));
260
+ },
261
+ });
262
+
263
+ const upload = multer({
264
+ storage,
265
+ limits: { fileSize: MAX_FILE_SIZE },
266
+ fileFilter(req, file, cb) {
267
+ if (!ALLOWED_MIME_TYPES.has(file.mimetype)) {
268
+ return cb(new Error('Unsupported file type'));
269
+ }
270
+ cb(null, true);
271
+ },
272
+ });
273
+
274
+ // IMPORTANT: fileFilter only checks the client-provided mimetype header,
275
+ // which can be spoofed. After upload, verify actual file content with a
276
+ // library like 'file-type' (reads magic bytes) before trusting it.
277
+
278
+ module.exports = { upload, UPLOAD_DIR };
279
+ // Usage: app.post('/upload', upload.single('file'), uploadHandler);
280
+ `;
281
+ export const RLS_ENABLE = `-- Enable Row Level Security on a table, then add a policy.
282
+ -- Without RLS, the public anon key can read/write this table directly —
283
+ -- this is the #1 cause of drained/defaced Supabase-backed apps.
284
+
285
+ ALTER TABLE your_table_name ENABLE ROW LEVEL SECURITY;
286
+
287
+ -- Example: users can only read their own rows.
288
+ CREATE POLICY "Users can read their own rows"
289
+ ON your_table_name
290
+ FOR SELECT
291
+ USING (auth.uid() = user_id);
292
+
293
+ -- Repeat ALTER TABLE ... ENABLE ROW LEVEL SECURITY for every table —
294
+ -- there are no exceptions. A table with RLS enabled but no policies
295
+ -- denies all access by default, which is a safe starting point.
296
+ `;
297
+ export const FIX_TEMPLATES = {
298
+ helmet: { id: "helmet", title: "security/helmet.config.js", code: HELMET_CONFIG },
299
+ cors: { id: "cors", title: "security/cors.config.js", code: CORS_CONFIG },
300
+ rateLimit: { id: "rateLimit", title: "security/rateLimiter.middleware.js", code: RATE_LIMITER },
301
+ accountBackoff: { id: "accountBackoff", title: "security/accountBackoff.js", code: ACCOUNT_BACKOFF },
302
+ errorHandler: { id: "errorHandler", title: "security/errorHandler.middleware.js", code: ERROR_HANDLER },
303
+ hashPassword: { id: "hashPassword", title: "security/auth/hashPassword.js", code: HASH_PASSWORD },
304
+ jwt: { id: "jwt", title: "security/auth/generateToken.js", code: JWT_TOKEN },
305
+ validation: { id: "validation", title: "security/validation/authSchemas.js", code: VALIDATION_SCHEMA },
306
+ envExample: { id: "envExample", title: ".env.example", code: ENV_EXAMPLE },
307
+ ciWorkflow: { id: "ciWorkflow", title: ".github/workflows/security-check.yml", code: SECURITY_CHECK_WORKFLOW },
308
+ preCommit: { id: "preCommit", title: ".husky/pre-commit", code: PRE_COMMIT_HOOK },
309
+ fileUpload: { id: "fileUpload", title: "security/fileUpload.middleware.js", code: FILE_UPLOAD },
310
+ rlsEnable: { id: "rlsEnable", title: "enable-rls.sql", code: RLS_ENABLE },
311
+ };