@preflight-agent/cli 0.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.
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runVulnerabilityChecks = runVulnerabilityChecks;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const glob_1 = require("glob");
10
+ async function runVulnerabilityChecks(dir) {
11
+ const results = [];
12
+ const allTsFiles = await (0, glob_1.glob)(`${dir}/src/**/*.{ts,tsx,js,jsx}`, { ignore: ['**/node_modules/**', '**/.next/**'] });
13
+ const allFiles = await (0, glob_1.glob)(`${dir}/**/*.{ts,tsx,js,jsx,json,env,yml,yaml,config.*}`, { ignore: ['**/node_modules/**', '**/.next/**', '**/dist/**'] });
14
+ // --- 1. XSS: dangerouslySetInnerHTML / innerHTML ---
15
+ let hasUnsafeHTML = false;
16
+ let unsafeHTMLFile;
17
+ for (const file of allTsFiles) {
18
+ const content = fs_1.default.readFileSync(file, 'utf-8');
19
+ if (content.includes('dangerouslySetInnerHTML') || content.includes('.innerHTML =')) {
20
+ hasUnsafeHTML = true;
21
+ unsafeHTMLFile = path_1.default.relative(dir, file);
22
+ break;
23
+ }
24
+ }
25
+ results.push(hasUnsafeHTML
26
+ ? { status: 'warn', message: 'dangerouslySetInnerHTML or innerHTML assignment found \u2014 potential XSS risk', file: unsafeHTMLFile }
27
+ : { status: 'pass', message: 'No dangerouslySetInnerHTML or innerHTML assignments found' });
28
+ // --- 2. eval() usage ---
29
+ let hasEval = false;
30
+ let evalFile;
31
+ for (const file of allTsFiles) {
32
+ const content = fs_1.default.readFileSync(file, 'utf-8');
33
+ const lines = content.split('\n');
34
+ for (let i = 0; i < lines.length; i++) {
35
+ const trimmed = lines[i].trim();
36
+ if (/eval\s*\(/.test(trimmed) && !trimmed.startsWith('//') && !trimmed.startsWith('*')) {
37
+ hasEval = true;
38
+ evalFile = path_1.default.relative(dir, file);
39
+ break;
40
+ }
41
+ }
42
+ if (hasEval)
43
+ break;
44
+ }
45
+ results.push(hasEval
46
+ ? { status: 'fail', message: 'eval() usage detected \u2014 arbitrary code execution risk', file: evalFile }
47
+ : { status: 'pass', message: 'No eval() usage detected' });
48
+ // --- 3. Sniffing protection: X-Content-Type-Options ---
49
+ const configFiles = [
50
+ 'next.config.ts', 'next.config.js', 'next.config.mjs',
51
+ '.htaccess', 'nginx.conf', 'web.config',
52
+ ];
53
+ let hasNosniff = false;
54
+ let nosniffFile;
55
+ for (const cfg of configFiles) {
56
+ const cfgPath = path_1.default.join(dir, cfg);
57
+ if (fs_1.default.existsSync(cfgPath)) {
58
+ const content = fs_1.default.readFileSync(cfgPath, 'utf-8');
59
+ if (content.includes('nosniff') || content.includes('X-Content-Type-Options')) {
60
+ hasNosniff = true;
61
+ nosniffFile = cfg;
62
+ break;
63
+ }
64
+ }
65
+ }
66
+ results.push(hasNosniff
67
+ ? { status: 'pass', message: 'X-Content-Type-Options: nosniff found \u2014 MIME sniffing protection enabled', file: nosniffFile }
68
+ : { status: 'warn', message: 'No X-Content-Type-Options: nosniff found \u2014 browser may MIME-sniff responses' });
69
+ // --- 4. HSTS ---
70
+ let hasHSTS = false;
71
+ let hstsFile;
72
+ for (const cfg of configFiles) {
73
+ const cfgPath = path_1.default.join(dir, cfg);
74
+ if (fs_1.default.existsSync(cfgPath)) {
75
+ const content = fs_1.default.readFileSync(cfgPath, 'utf-8');
76
+ if (content.includes('Strict-Transport-Security') || content.includes('HSTS') || content.includes('hsts')) {
77
+ hasHSTS = true;
78
+ hstsFile = cfg;
79
+ break;
80
+ }
81
+ }
82
+ }
83
+ results.push(hasHSTS
84
+ ? { status: 'pass', message: 'HSTS (Strict-Transport-Security) configured', file: hstsFile }
85
+ : { status: 'warn', message: 'No HSTS header found \u2014 users may connect over HTTP instead of HTTPS' });
86
+ // --- 5. Mixed content: http:// URLs in source ---
87
+ let hasMixedContent = false;
88
+ const mixedContentFiles = [];
89
+ for (const file of allTsFiles) {
90
+ const content = fs_1.default.readFileSync(file, 'utf-8');
91
+ const httpMatches = content.match(/https?:\/\/(?!localhost)(?!127\.0\.0\.1)(?![\w.-]*\.\w{2,}\/)[^\s"'`)*]+/g);
92
+ if (httpMatches) {
93
+ const insecureUrls = httpMatches.filter(u => u.startsWith('http://'));
94
+ if (insecureUrls.length > 0) {
95
+ hasMixedContent = true;
96
+ mixedContentFiles.push(path_1.default.relative(dir, file));
97
+ }
98
+ }
99
+ }
100
+ results.push(hasMixedContent
101
+ ? { status: 'warn', message: `http:// URLs found in ${mixedContentFiles.length} file(s) \u2014 mixed content vulnerability`, file: mixedContentFiles[0] }
102
+ : { status: 'pass', message: 'No mixed content (http:// URLs) detected in source' });
103
+ // --- 6. CSRF protection ---
104
+ const csrfPatterns = [
105
+ 'csrf', 'CSRF', 'csrfToken', 'xsrf', 'XSRF',
106
+ 'SameSite', 'sameSite', 'same-site',
107
+ 'doubleSubmit', 'csrfProtection',
108
+ ];
109
+ let hasCSRF = false;
110
+ let csrfFile;
111
+ for (const file of allTsFiles) {
112
+ const content = fs_1.default.readFileSync(file, 'utf-8');
113
+ for (const pattern of csrfPatterns) {
114
+ if (content.includes(pattern)) {
115
+ hasCSRF = true;
116
+ csrfFile = path_1.default.relative(dir, file);
117
+ break;
118
+ }
119
+ }
120
+ if (hasCSRF)
121
+ break;
122
+ }
123
+ results.push(hasCSRF
124
+ ? { status: 'pass', message: 'CSRF protection detected (token or SameSite cookie)', file: csrfFile }
125
+ : { status: 'warn', message: 'No CSRF protection detected \u2014 forms and API mutations may be vulnerable to cross-site requests' });
126
+ // --- 7. Debug mode / information disclosure ---
127
+ let hasDebugLeak = false;
128
+ let debugFile;
129
+ for (const file of allTsFiles) {
130
+ const content = fs_1.default.readFileSync(file, 'utf-8');
131
+ const lines = content.split('\n');
132
+ for (let i = 0; i < lines.length; i++) {
133
+ const line = lines[i];
134
+ if ((line.includes('debug: true') || line.includes('debug = true') || line.includes('isDebug = true')) &&
135
+ !line.trim().startsWith('//')) {
136
+ hasDebugLeak = true;
137
+ debugFile = path_1.default.relative(dir, file);
138
+ break;
139
+ }
140
+ }
141
+ if (hasDebugLeak)
142
+ break;
143
+ }
144
+ results.push(hasDebugLeak
145
+ ? { status: 'warn', message: 'Debug mode enabled in source \u2014 may expose sensitive info in production', file: debugFile }
146
+ : { status: 'pass', message: 'No debug mode flags detected in source' });
147
+ // --- 8. Check .gitignore for common build artifacts ---
148
+ const gitignorePath = path_1.default.join(dir, '.gitignore');
149
+ if (fs_1.default.existsSync(gitignorePath)) {
150
+ const gitignore = fs_1.default.readFileSync(gitignorePath, 'utf-8');
151
+ const expectedEntries = ['.next', 'dist', 'build', '.env.local', 'coverage', '.turbo'];
152
+ const missing = expectedEntries.filter(e => !gitignore.includes(e));
153
+ if (missing.length === 0) {
154
+ results.push({ status: 'pass', message: 'All common build artifacts are gitignored (.next, dist, build, .env.local)' });
155
+ }
156
+ else {
157
+ results.push({ status: 'warn', message: `Build artifacts not gitignored: ${missing.join(', ')} \u2014 they may leak in the repo` });
158
+ }
159
+ }
160
+ return results;
161
+ }
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runWebChecks = runWebChecks;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const glob_1 = require("glob");
10
+ async function runWebChecks(dir) {
11
+ const results = [];
12
+ const nextConfigPaths = [
13
+ path_1.default.join(dir, 'next.config.ts'),
14
+ path_1.default.join(dir, 'next.config.js'),
15
+ path_1.default.join(dir, 'next.config.mjs'),
16
+ ];
17
+ const existingConfigs = nextConfigPaths.filter(fs_1.default.existsSync);
18
+ if (existingConfigs.length > 0) {
19
+ const content = fs_1.default.readFileSync(existingConfigs[0], 'utf-8');
20
+ const hasCSP = content.includes('Content-Security-Policy') || content.includes('csp');
21
+ const hasFrameOptions = content.includes('X-Frame-Options') || content.includes('frame-ancestors');
22
+ const hasCORS = content.includes('cors') || content.includes('CORS') || content.includes('Access-Control');
23
+ if (hasCSP) {
24
+ results.push({ status: 'pass', message: 'Content Security Policy (CSP) configured', file: path_1.default.relative(dir, existingConfigs[0]) });
25
+ }
26
+ else {
27
+ results.push({ status: 'warn', message: 'No Content Security Policy (CSP) found \u2014 your app is vulnerable to XSS' });
28
+ }
29
+ if (hasFrameOptions) {
30
+ results.push({ status: 'pass', message: 'Clickjacking protection found (X-Frame-Options / frame-ancestors)' });
31
+ }
32
+ else {
33
+ results.push({ status: 'warn', message: 'No clickjacking protection \u2014 your app can be embedded in iframes' });
34
+ }
35
+ if (hasCORS) {
36
+ results.push({ status: 'pass', message: 'CORS configuration detected' });
37
+ }
38
+ else {
39
+ results.push({ status: 'warn', message: 'No explicit CORS configuration found' });
40
+ }
41
+ }
42
+ else {
43
+ results.push({ status: 'warn', message: 'No Next.js config found \u2014 skipping security header checks' });
44
+ }
45
+ const cookieFiles = await (0, glob_1.glob)(`${dir}/**/*.ts`, { ignore: ['**/node_modules/**', '**/.next/**'] });
46
+ let hasSecureCookies = false;
47
+ let cookieFile;
48
+ for (const file of cookieFiles) {
49
+ const content = fs_1.default.readFileSync(file, 'utf-8');
50
+ const hasCookie = content.includes('cookie') || content.includes('Cookie') || content.includes('cookies');
51
+ if (hasCookie && (content.includes('httpOnly') || content.includes('secure') || content.includes('sameSite'))) {
52
+ hasSecureCookies = true;
53
+ cookieFile = path_1.default.relative(dir, file);
54
+ break;
55
+ }
56
+ }
57
+ results.push(hasSecureCookies
58
+ ? { status: 'pass', message: 'Cookie security flags (httpOnly/secure/sameSite) detected', file: cookieFile }
59
+ : { status: 'warn', message: 'No secure cookie flags detected \u2014 cookies may be accessible to JavaScript' });
60
+ return results;
61
+ }
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const commander_1 = require("commander");
5
+ const scan_1 = require("./scan");
6
+ const program = new commander_1.Command();
7
+ program
8
+ .name('preflight')
9
+ .description('Pre-deploy checklist CLI for vibe coders')
10
+ .version('0.1.0');
11
+ program
12
+ .command('scan [dir]')
13
+ .description('Scan a project for common pre-deploy issues')
14
+ .option('--json', 'Output results as JSON')
15
+ .option('--strict', 'Exit with code 1 if any checks fail')
16
+ .option('--only <category>', 'Run only one category: security|auth|payments|database|api|web|graphql|realtime|vulnerabilities')
17
+ .action(async (dir = '.', options) => {
18
+ await (0, scan_1.runScan)(dir, options);
19
+ });
20
+ program.parse();
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.renderReport = renderReport;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ function icon(status) {
9
+ if (status === 'pass')
10
+ return chalk_1.default.green('\u2705');
11
+ if (status === 'fail')
12
+ return chalk_1.default.red('\u274C');
13
+ return chalk_1.default.yellow('\u26A0\uFE0F');
14
+ }
15
+ function score(categories) {
16
+ const all = categories.flatMap((c) => c.checks);
17
+ const passed = all.filter((c) => c.status === 'pass').length;
18
+ return { passed, total: all.length };
19
+ }
20
+ function renderReport(dir, categories) {
21
+ console.log('');
22
+ console.log(chalk_1.default.bold('\u{1F6EB} Agent Preflight \u2014 Pre-Deploy Scan'));
23
+ console.log(chalk_1.default.dim(`Scanning: ${dir}`));
24
+ console.log('');
25
+ for (const category of categories) {
26
+ console.log(chalk_1.default.bold(` ${category.name}`));
27
+ for (const check of category.checks) {
28
+ const location = check.file
29
+ ? chalk_1.default.dim(` (${check.file}${check.line ? `:${check.line}` : ''})`)
30
+ : '';
31
+ console.log(` ${icon(check.status)} ${check.message}${location}`);
32
+ }
33
+ console.log('');
34
+ }
35
+ const { passed, total } = score(categories);
36
+ const ratio = `${passed}/${total}`;
37
+ const fails = categories.flatMap((c) => c.checks).filter((c) => c.status === 'fail');
38
+ if (fails.length === 0) {
39
+ console.log(chalk_1.default.green(`Score: ${ratio} \u2014 All clear. Ready to deploy. \u{1F680}`));
40
+ }
41
+ else {
42
+ console.log(chalk_1.default.red(`Score: ${ratio} \u2014 Fix ${fails.length} critical issue${fails.length > 1 ? 's' : ''} before deploying.`));
43
+ }
44
+ console.log('');
45
+ }
package/dist/scan.js ADDED
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runScan = runScan;
7
+ const path_1 = __importDefault(require("path"));
8
+ const ora_1 = __importDefault(require("ora"));
9
+ const security_1 = require("./checks/security");
10
+ const auth_1 = require("./checks/auth");
11
+ const payments_1 = require("./checks/payments");
12
+ const database_1 = require("./checks/database");
13
+ const api_1 = require("./checks/api");
14
+ const web_1 = require("./checks/web");
15
+ const graphql_1 = require("./checks/graphql");
16
+ const realtime_1 = require("./checks/realtime");
17
+ const vulnerabilities_1 = require("./checks/vulnerabilities");
18
+ const reporter_1 = require("./reporter");
19
+ async function runScan(dir, options) {
20
+ const absoluteDir = path_1.default.resolve(process.cwd(), dir);
21
+ const spinner = (0, ora_1.default)(`Scanning: ${absoluteDir}`).start();
22
+ try {
23
+ const categories = [];
24
+ const shouldRun = (name) => !options.only || options.only.toLowerCase() === name;
25
+ if (shouldRun('security')) {
26
+ spinner.text = 'Checking security...';
27
+ categories.push({
28
+ name: 'Security',
29
+ checks: await (0, security_1.runSecurityChecks)(absoluteDir),
30
+ });
31
+ }
32
+ if (shouldRun('auth')) {
33
+ spinner.text = 'Checking authentication...';
34
+ categories.push({
35
+ name: 'Authentication',
36
+ checks: await (0, auth_1.runAuthChecks)(absoluteDir),
37
+ });
38
+ }
39
+ if (shouldRun('payments')) {
40
+ spinner.text = 'Checking payments...';
41
+ categories.push({
42
+ name: 'Payments',
43
+ checks: await (0, payments_1.runPaymentChecks)(absoluteDir),
44
+ });
45
+ }
46
+ if (shouldRun('database')) {
47
+ spinner.text = 'Checking database...';
48
+ categories.push({
49
+ name: 'Database',
50
+ checks: await (0, database_1.runDatabaseChecks)(absoluteDir),
51
+ });
52
+ }
53
+ if (shouldRun('api')) {
54
+ spinner.text = 'Checking API & Validation...';
55
+ categories.push({
56
+ name: 'API & Validation',
57
+ checks: await (0, api_1.runApiChecks)(absoluteDir),
58
+ });
59
+ }
60
+ if (shouldRun('web')) {
61
+ spinner.text = 'Checking web security...';
62
+ categories.push({
63
+ name: 'Web Security',
64
+ checks: await (0, web_1.runWebChecks)(absoluteDir),
65
+ });
66
+ }
67
+ if (shouldRun('graphql')) {
68
+ spinner.text = 'Checking GraphQL...';
69
+ categories.push({
70
+ name: 'GraphQL',
71
+ checks: await (0, graphql_1.runGraphqlChecks)(absoluteDir),
72
+ });
73
+ }
74
+ if (shouldRun('realtime')) {
75
+ spinner.text = 'Checking real-time connections...';
76
+ categories.push({
77
+ name: 'Real-Time',
78
+ checks: await (0, realtime_1.runRealtimeChecks)(absoluteDir),
79
+ });
80
+ }
81
+ if (shouldRun('vulnerabilities')) {
82
+ spinner.text = 'Checking for web vulnerabilities...';
83
+ categories.push({
84
+ name: 'Vulnerabilities',
85
+ checks: await (0, vulnerabilities_1.runVulnerabilityChecks)(absoluteDir),
86
+ });
87
+ }
88
+ spinner.stop();
89
+ if (options.json) {
90
+ console.log(JSON.stringify(categories, null, 2));
91
+ }
92
+ else {
93
+ (0, reporter_1.renderReport)(absoluteDir, categories);
94
+ }
95
+ const hasFail = categories
96
+ .flatMap((c) => c.checks)
97
+ .some((c) => c.status === 'fail');
98
+ if (options.strict && hasFail) {
99
+ process.exit(1);
100
+ }
101
+ }
102
+ catch (err) {
103
+ spinner.fail('Scan failed');
104
+ console.error(err);
105
+ process.exit(1);
106
+ }
107
+ }
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@preflight-agent/cli",
3
+ "version": "0.1.0",
4
+ "description": "Pre-deploy checklist CLI for vibe coders",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "preflight": "dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "ts-node src/index.ts",
12
+ "prepublishOnly": "npm run build"
13
+ },
14
+ "dependencies": {
15
+ "chalk": "^5.3.0",
16
+ "commander": "^12.0.0",
17
+ "dotenv": "^16.4.0",
18
+ "glob": "^10.3.0",
19
+ "ora": "^8.0.1"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^20.0.0",
23
+ "typescript": "^5.4.0",
24
+ "ts-node": "^10.9.0"
25
+ },
26
+ "license": "MIT"
27
+ }
@@ -0,0 +1,74 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { glob } from 'glob';
4
+ import type { CheckResult } from '../scan';
5
+
6
+ export async function runApiChecks(dir: string): Promise<CheckResult[]> {
7
+ const results: CheckResult[] = [];
8
+
9
+ const apiFiles: string[] = await glob(`${dir}/src/app/api/**/*.ts`, {
10
+ ignore: ['**/node_modules/**']
11
+ });
12
+
13
+ if (apiFiles.length === 0) {
14
+ const altFiles = await glob(`${dir}/pages/api/**/*.ts`, { ignore: ['**/node_modules/**'] });
15
+ if (altFiles.length === 0) {
16
+ results.push({ status: 'warn', message: 'No API routes found \u2014 skipping API checks' });
17
+ return results;
18
+ }
19
+ apiFiles.push(...altFiles);
20
+ }
21
+
22
+ let validatedRoutes = 0;
23
+ const unvalidatedRoutes: string[] = [];
24
+
25
+ for (const file of apiFiles) {
26
+ const content = fs.readFileSync(file, 'utf-8');
27
+ const hasPOST = content.includes('POST') || content.includes('req.body') || content.includes('request.json()');
28
+ const hasValidation =
29
+ content.includes('z.object') ||
30
+ content.includes('yup.object') ||
31
+ content.includes('v.object') ||
32
+ content.includes('.parse(') ||
33
+ content.includes('.safeParse(') ||
34
+ content.includes('.validate(');
35
+
36
+ if (hasPOST && hasValidation) {
37
+ validatedRoutes++;
38
+ } else if (hasPOST && !hasValidation) {
39
+ unvalidatedRoutes.push(path.relative(dir, file));
40
+ }
41
+ }
42
+
43
+ if (unvalidatedRoutes.length === 0) {
44
+ results.push({ status: 'pass', message: 'Input validation (Zod/Yup) found on POST routes' });
45
+ } else {
46
+ results.push({
47
+ status: 'warn',
48
+ message: `${unvalidatedRoutes.length} POST route${unvalidatedRoutes.length > 1 ? 's' : ''} missing input validation`,
49
+ });
50
+ for (const route of unvalidatedRoutes.slice(0, 2)) {
51
+ results.push({ status: 'warn', message: 'No input validation detected', file: route });
52
+ }
53
+ }
54
+
55
+ const allFiles = await glob(`${dir}/src/**/*.ts`, { ignore: ['**/node_modules/**'] });
56
+ const hasRateLimit = allFiles.some(file => {
57
+ const content = fs.readFileSync(file, 'utf-8');
58
+ return (
59
+ content.includes('ratelimit') ||
60
+ content.includes('rate-limit') ||
61
+ content.includes('upstash') ||
62
+ content.includes('redis') ||
63
+ content.includes('limiter')
64
+ );
65
+ });
66
+
67
+ results.push(
68
+ hasRateLimit
69
+ ? { status: 'pass', message: 'Rate limiting detected' }
70
+ : { status: 'warn', message: 'No rate limiting found \u2014 your API is open to abuse' }
71
+ );
72
+
73
+ return results;
74
+ }
@@ -0,0 +1,76 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { glob } from 'glob';
4
+ import type { CheckResult } from '../scan';
5
+
6
+ export async function runAuthChecks(dir: string): Promise<CheckResult[]> {
7
+ const results: CheckResult[] = [];
8
+
9
+ const middlewarePaths = [
10
+ path.join(dir, 'middleware.ts'),
11
+ path.join(dir, 'src/middleware.ts'),
12
+ path.join(dir, 'middleware.js'),
13
+ ];
14
+
15
+ const hasMiddleware = middlewarePaths.some(fs.existsSync);
16
+ if (hasMiddleware) {
17
+ results.push({ status: 'pass', message: 'Auth middleware file found' });
18
+ } else {
19
+ results.push({ status: 'warn', message: 'No middleware.ts found \u2014 protected routes may be unsecured' });
20
+ }
21
+
22
+ const envExamplePath = path.join(dir, '.env.example');
23
+ if (fs.existsSync(envExamplePath)) {
24
+ const envExample = fs.readFileSync(envExamplePath, 'utf-8');
25
+ const hasAuthSecret =
26
+ envExample.includes('JWT_SECRET') ||
27
+ envExample.includes('NEXTAUTH_SECRET') ||
28
+ envExample.includes('AUTH_SECRET');
29
+ if (hasAuthSecret) {
30
+ results.push({ status: 'pass', message: 'Auth secret documented in .env.example' });
31
+ } else {
32
+ results.push({ status: 'warn', message: 'No JWT_SECRET or NEXTAUTH_SECRET in .env.example' });
33
+ }
34
+ }
35
+
36
+ const apiFiles = await glob(`${dir}/src/app/api/**/*.ts`, { ignore: ['**/node_modules/**'] });
37
+ const suspiciousRoutes: string[] = [];
38
+
39
+ for (const file of apiFiles) {
40
+ const content = fs.readFileSync(file, 'utf-8');
41
+ const hasAuthCheck =
42
+ content.includes('getServerSession') ||
43
+ content.includes('auth()') ||
44
+ content.includes('verifyToken') ||
45
+ content.includes('supabase.auth') ||
46
+ content.includes('requireAuth') ||
47
+ content.includes('middleware');
48
+
49
+ const looksPrivate =
50
+ content.includes('userId') ||
51
+ content.includes('user_id') ||
52
+ content.includes('DELETE') ||
53
+ content.includes('UPDATE') ||
54
+ file.includes('/admin/') ||
55
+ file.includes('/user/') ||
56
+ file.includes('/profile/');
57
+
58
+ if (looksPrivate && !hasAuthCheck) {
59
+ suspiciousRoutes.push(path.relative(dir, file));
60
+ }
61
+ }
62
+
63
+ if (suspiciousRoutes.length > 0) {
64
+ for (const route of suspiciousRoutes.slice(0, 3)) {
65
+ results.push({
66
+ status: 'warn',
67
+ message: 'No auth check detected in route that may require it',
68
+ file: route,
69
+ });
70
+ }
71
+ } else if (apiFiles.length > 0) {
72
+ results.push({ status: 'pass', message: 'Auth checks found in API routes' });
73
+ }
74
+
75
+ return results;
76
+ }
@@ -0,0 +1,48 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { glob } from 'glob';
4
+ import type { CheckResult } from '../scan';
5
+
6
+ export async function runDatabaseChecks(dir: string): Promise<CheckResult[]> {
7
+ const results: CheckResult[] = [];
8
+
9
+ const migrationFiles = await glob(`${dir}/**/*.sql`, {
10
+ ignore: ['**/node_modules/**']
11
+ });
12
+
13
+ const supabaseDirs = [
14
+ path.join(dir, 'supabase'),
15
+ path.join(dir, 'src/supabase'),
16
+ ];
17
+ const hasSupabaseDir = supabaseDirs.some(fs.existsSync);
18
+
19
+ if (hasSupabaseDir || migrationFiles.length > 0) {
20
+ let hasRLS = false;
21
+ for (const file of migrationFiles) {
22
+ const content = fs.readFileSync(file, 'utf-8');
23
+ if (content.toLowerCase().includes('row level security') || content.toLowerCase().includes('enable rls')) {
24
+ hasRLS = true;
25
+ break;
26
+ }
27
+ }
28
+ results.push(
29
+ hasRLS
30
+ ? { status: 'pass', message: 'Row Level Security (RLS) enabled in migrations' }
31
+ : { status: 'fail', message: 'No RLS policies found in SQL migrations \u2014 your database may be fully open' }
32
+ );
33
+ } else {
34
+ results.push({ status: 'warn', message: 'No SQL migrations found \u2014 skipping RLS check' });
35
+ }
36
+
37
+ const envExamplePath = path.join(dir, '.env.example');
38
+ if (fs.existsSync(envExamplePath)) {
39
+ const env = fs.readFileSync(envExamplePath, 'utf-8');
40
+ if (env.includes('DATABASE_URL') || env.includes('SUPABASE_URL') || env.includes('NEXT_PUBLIC_SUPABASE_URL')) {
41
+ results.push({ status: 'pass', message: 'Database URL documented in .env.example' });
42
+ } else {
43
+ results.push({ status: 'warn', message: 'No database URL found in .env.example' });
44
+ }
45
+ }
46
+
47
+ return results;
48
+ }