ready-to-ship 1.0.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/ENABLE_2FA.md +90 -0
- package/ENABLE_2FA_SECURITY_KEY.md +109 -0
- package/GO_LIVE.md +187 -0
- package/LICENSE +22 -0
- package/PUBLISH.md +110 -0
- package/PUBLISH_STEPS.md +106 -0
- package/QUICKSTART.md +71 -0
- package/README.md +196 -0
- package/UNIQUE_FEATURES.md +114 -0
- package/package.json +53 -0
- package/publish.sh +64 -0
- package/src/cli.js +155 -0
- package/src/modules/api.js +152 -0
- package/src/modules/auth.js +152 -0
- package/src/modules/database.js +178 -0
- package/src/modules/dependencies.js +151 -0
- package/src/modules/env.js +117 -0
- package/src/modules/project.js +175 -0
- package/src/modules/report.js +118 -0
- package/src/modules/security.js +149 -0
- package/src/utils/fileHelpers.js +95 -0
- package/src/utils/fixHelpers.js +203 -0
- package/src/utils/logHelpers.js +77 -0
- package/src/utils/parseHelpers.js +151 -0
- package/templates/.github/workflows/ready-to-ship.yml +35 -0
- package/templates/README.md +23 -0
- package/templates/github-actions.yml +35 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { findFiles, readFile, fileExists } = require('../utils/fileHelpers');
|
|
3
|
+
const { error, success, verdict, printIssues } = require('../utils/logHelpers');
|
|
4
|
+
const { extractRoutes } = require('../utils/parseHelpers');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Validate API endpoints and health checks
|
|
8
|
+
*/
|
|
9
|
+
async function validate(projectPath = process.cwd()) {
|
|
10
|
+
const issues = [];
|
|
11
|
+
const warnings = [];
|
|
12
|
+
|
|
13
|
+
// Find route files
|
|
14
|
+
const routePatterns = [
|
|
15
|
+
'**/routes/**/*.{js,ts,jsx,tsx}',
|
|
16
|
+
'**/routes.{js,ts,jsx,tsx}',
|
|
17
|
+
'**/api/**/*.{js,ts,jsx,tsx}',
|
|
18
|
+
'**/src/**/*.{js,ts,jsx,tsx}'
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
let routeFiles = [];
|
|
22
|
+
for (const pattern of routePatterns) {
|
|
23
|
+
const files = await findFiles(pattern, projectPath);
|
|
24
|
+
routeFiles.push(...files);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// If no route files found, try to find main app file
|
|
28
|
+
if (routeFiles.length === 0) {
|
|
29
|
+
const mainFiles = await findFiles('**/{app,server,index,main}.{js,ts}', projectPath);
|
|
30
|
+
routeFiles.push(...mainFiles);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (routeFiles.length === 0) {
|
|
34
|
+
warnings.push('No route files found. Make sure your project structure is standard.');
|
|
35
|
+
verdict(false, 'API: NOT READY (No routes found)');
|
|
36
|
+
return { passed: false, issues, warnings };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Check for health endpoint
|
|
40
|
+
let hasHealthEndpoint = false;
|
|
41
|
+
const healthPatterns = ['/health', '/healthz', '/ping', '/status', '/api/health'];
|
|
42
|
+
|
|
43
|
+
for (const filePath of routeFiles) {
|
|
44
|
+
const code = await readFile(filePath);
|
|
45
|
+
if (!code) continue;
|
|
46
|
+
|
|
47
|
+
const routes = extractRoutes(code);
|
|
48
|
+
|
|
49
|
+
// Check if any route matches health patterns
|
|
50
|
+
const healthRoutes = routes.filter(route =>
|
|
51
|
+
healthPatterns.some(pattern => route.path === pattern || route.path.startsWith(pattern + '/'))
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
if (healthRoutes.length > 0) {
|
|
55
|
+
hasHealthEndpoint = true;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!hasHealthEndpoint) {
|
|
61
|
+
issues.push('/health endpoint missing (recommended for monitoring and load balancers)');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check route consistency
|
|
65
|
+
const allRoutes = [];
|
|
66
|
+
for (const filePath of routeFiles) {
|
|
67
|
+
const code = await readFile(filePath);
|
|
68
|
+
if (!code) continue;
|
|
69
|
+
|
|
70
|
+
const routes = extractRoutes(code);
|
|
71
|
+
routes.forEach(route => {
|
|
72
|
+
allRoutes.push({
|
|
73
|
+
...route,
|
|
74
|
+
file: path.relative(projectPath, filePath)
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Group routes by path
|
|
80
|
+
const routesByPath = {};
|
|
81
|
+
allRoutes.forEach(route => {
|
|
82
|
+
if (!routesByPath[route.path]) {
|
|
83
|
+
routesByPath[route.path] = [];
|
|
84
|
+
}
|
|
85
|
+
routesByPath[route.path].push(route.method);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Auth/action endpoints that don't need GET (false positive prevention)
|
|
89
|
+
const actionEndpoints = [
|
|
90
|
+
'login', 'logout', 'register', 'signin', 'signout', 'signup',
|
|
91
|
+
'refresh-token', 'reset-password', 'forgot-password', 'verify',
|
|
92
|
+
'authenticate', 'authorize', 'callback', 'webhook'
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
const isActionEndpoint = (path) => {
|
|
96
|
+
const pathLower = path.toLowerCase();
|
|
97
|
+
return actionEndpoints.some(action => pathLower.includes(action));
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Check for common REST inconsistencies
|
|
101
|
+
Object.keys(routesByPath).forEach(path => {
|
|
102
|
+
const methods = routesByPath[path];
|
|
103
|
+
|
|
104
|
+
// Skip action endpoints (login, register, etc.)
|
|
105
|
+
if (isActionEndpoint(path)) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// If POST exists, usually should have GET for the collection
|
|
110
|
+
if (methods.includes('POST') && !methods.includes('GET')) {
|
|
111
|
+
const collectionPath = path.replace(/\/[^/]+$/, '');
|
|
112
|
+
if (collectionPath !== path && !routesByPath[collectionPath]?.includes('GET')) {
|
|
113
|
+
warnings.push(`POST ${path} exists but no GET endpoint for collection`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// If PUT/PATCH exists, usually should have GET for the resource
|
|
118
|
+
if ((methods.includes('PUT') || methods.includes('PATCH')) && !methods.includes('GET')) {
|
|
119
|
+
warnings.push(`${methods.find(m => ['PUT', 'PATCH'].includes(m))} ${path} exists but no GET endpoint`);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Print results
|
|
124
|
+
console.log('\n🔹 API VALIDATION\n');
|
|
125
|
+
|
|
126
|
+
if (hasHealthEndpoint) {
|
|
127
|
+
success('Health endpoint found');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (issues.length === 0 && warnings.length === 0) {
|
|
131
|
+
success('API structure looks good');
|
|
132
|
+
verdict(true, 'API: READY');
|
|
133
|
+
return { passed: true, issues: [], warnings, routes: allRoutes };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
printIssues(issues, 'error');
|
|
137
|
+
printIssues(warnings, 'warning');
|
|
138
|
+
|
|
139
|
+
verdict(false, 'API: NOT READY');
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
passed: issues.length === 0,
|
|
143
|
+
issues,
|
|
144
|
+
warnings,
|
|
145
|
+
routes: allRoutes
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = {
|
|
150
|
+
validate
|
|
151
|
+
};
|
|
152
|
+
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { getAllFiles, readFile } = require('../utils/fileHelpers');
|
|
3
|
+
const { error, success, verdict, printIssues } = require('../utils/logHelpers');
|
|
4
|
+
const { extractRoutes, hasAuthMiddleware, extractJWTExpiry } = require('../utils/parseHelpers');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Validate authentication and route protection
|
|
8
|
+
*/
|
|
9
|
+
async function validate(projectPath = process.cwd()) {
|
|
10
|
+
const issues = [];
|
|
11
|
+
const warnings = [];
|
|
12
|
+
const vulnerableRoutes = [];
|
|
13
|
+
|
|
14
|
+
// Find route files
|
|
15
|
+
const routePatterns = [
|
|
16
|
+
'**/routes/**/*.{js,ts,jsx,tsx}',
|
|
17
|
+
'**/routes.{js,ts,jsx,tsx}',
|
|
18
|
+
'**/api/**/*.{js,ts,jsx,tsx}',
|
|
19
|
+
'**/controllers/**/*.{js,ts,jsx,tsx}',
|
|
20
|
+
'**/src/**/*.{js,ts,jsx,tsx}'
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
let routeFiles = [];
|
|
24
|
+
for (const pattern of routePatterns) {
|
|
25
|
+
const files = await require('../utils/fileHelpers').findFiles(pattern, projectPath);
|
|
26
|
+
routeFiles.push(...files);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// If no route files found, try to find main app file
|
|
30
|
+
if (routeFiles.length === 0) {
|
|
31
|
+
const mainFiles = await require('../utils/fileHelpers').findFiles('**/{app,server,index,main}.{js,ts}', projectPath);
|
|
32
|
+
routeFiles.push(...mainFiles);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (routeFiles.length === 0) {
|
|
36
|
+
warnings.push('No route files found. Make sure your project structure is standard.');
|
|
37
|
+
verdict(false, 'AUTH: NOT READY (No routes found)');
|
|
38
|
+
return { passed: false, issues, warnings, vulnerableRoutes: [] };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Analyze each route file
|
|
42
|
+
for (const filePath of routeFiles) {
|
|
43
|
+
const code = await readFile(filePath);
|
|
44
|
+
if (!code) continue;
|
|
45
|
+
|
|
46
|
+
const routes = extractRoutes(code);
|
|
47
|
+
const lines = code.split('\n');
|
|
48
|
+
|
|
49
|
+
routes.forEach(route => {
|
|
50
|
+
const routePath = route.path;
|
|
51
|
+
const method = route.method;
|
|
52
|
+
const isSensitive = isSensitiveRoute(routePath);
|
|
53
|
+
|
|
54
|
+
if (isSensitive) {
|
|
55
|
+
// Check if route has auth middleware
|
|
56
|
+
const hasAuth = hasAuthMiddleware(code, route.line);
|
|
57
|
+
|
|
58
|
+
if (!hasAuth) {
|
|
59
|
+
vulnerableRoutes.push({
|
|
60
|
+
method,
|
|
61
|
+
path: routePath,
|
|
62
|
+
file: path.relative(projectPath, filePath),
|
|
63
|
+
line: route.line
|
|
64
|
+
});
|
|
65
|
+
issues.push(`Route ${method} ${routePath} missing auth middleware (${path.relative(projectPath, filePath)})`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Check JWT expiry configuration
|
|
71
|
+
const jwtExpiry = extractJWTExpiry(code);
|
|
72
|
+
if (jwtExpiry !== null) {
|
|
73
|
+
const maxRecommendedExpiry = 7 * 24 * 60 * 60; // 7 days in seconds
|
|
74
|
+
const oneYearInSeconds = 365 * 24 * 60 * 60;
|
|
75
|
+
|
|
76
|
+
if (jwtExpiry > oneYearInSeconds) {
|
|
77
|
+
issues.push(`JWT expiry too long: ${formatDuration(jwtExpiry)} (recommended: < 7 days)`);
|
|
78
|
+
} else if (jwtExpiry > maxRecommendedExpiry) {
|
|
79
|
+
warnings.push(`JWT expiry is ${formatDuration(jwtExpiry)} (recommended: < 7 days)`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Print results
|
|
85
|
+
console.log('\n🔹 AUTH VALIDATION\n');
|
|
86
|
+
|
|
87
|
+
if (issues.length === 0 && warnings.length === 0) {
|
|
88
|
+
success('All routes are properly protected');
|
|
89
|
+
verdict(true, 'AUTH: READY');
|
|
90
|
+
return { passed: true, issues: [], warnings: [], vulnerableRoutes: [] };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
printIssues(issues, 'error');
|
|
94
|
+
printIssues(warnings, 'warning');
|
|
95
|
+
|
|
96
|
+
if (vulnerableRoutes.length > 0) {
|
|
97
|
+
console.log('\n📋 Vulnerable Routes:');
|
|
98
|
+
vulnerableRoutes.forEach(route => {
|
|
99
|
+
error(`${route.method} ${route.path} (${route.file}:${route.line})`);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
verdict(false, 'AUTH: NOT READY');
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
passed: issues.length === 0,
|
|
107
|
+
issues,
|
|
108
|
+
warnings,
|
|
109
|
+
vulnerableRoutes
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Check if route is sensitive and requires auth
|
|
115
|
+
*/
|
|
116
|
+
function isSensitiveRoute(path) {
|
|
117
|
+
const sensitivePatterns = [
|
|
118
|
+
/^\/admin/i,
|
|
119
|
+
/^\/api\/admin/i,
|
|
120
|
+
/\/users/i,
|
|
121
|
+
/\/profile/i,
|
|
122
|
+
/\/settings/i,
|
|
123
|
+
/\/account/i,
|
|
124
|
+
/\/dashboard/i,
|
|
125
|
+
/\/delete/i,
|
|
126
|
+
/\/update/i,
|
|
127
|
+
/\/create/i,
|
|
128
|
+
/\/edit/i,
|
|
129
|
+
/\/password/i,
|
|
130
|
+
/\/auth\/change/i,
|
|
131
|
+
/\/api\/v\d+\/.*(?:user|admin|auth|profile|settings)/i
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
return sensitivePatterns.some(pattern => pattern.test(path));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Format duration in seconds to human-readable string
|
|
139
|
+
*/
|
|
140
|
+
function formatDuration(seconds) {
|
|
141
|
+
if (seconds < 60) return `${seconds} seconds`;
|
|
142
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)} minutes`;
|
|
143
|
+
if (seconds < 86400) return `${Math.floor(seconds / 3600)} hours`;
|
|
144
|
+
if (seconds < 2592000) return `${Math.floor(seconds / 86400)} days`;
|
|
145
|
+
if (seconds < 31536000) return `${Math.floor(seconds / 2592000)} months`;
|
|
146
|
+
return `${Math.floor(seconds / 31536000)} years`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = {
|
|
150
|
+
validate
|
|
151
|
+
};
|
|
152
|
+
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { findFiles, readFile, parseEnvFile, fileExists } = require('../utils/fileHelpers');
|
|
3
|
+
const { error, success, verdict, printIssues } = require('../utils/logHelpers');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Validate database configuration
|
|
7
|
+
*/
|
|
8
|
+
async function validate(projectPath = process.cwd()) {
|
|
9
|
+
const issues = [];
|
|
10
|
+
const warnings = [];
|
|
11
|
+
|
|
12
|
+
// Check for database connection strings in .env
|
|
13
|
+
const envPath = path.join(projectPath, '.env');
|
|
14
|
+
let envVars = {};
|
|
15
|
+
|
|
16
|
+
if (await fileExists(envPath)) {
|
|
17
|
+
envVars = await parseEnvFile(envPath);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Detect database type from env vars
|
|
21
|
+
const dbVars = Object.keys(envVars).filter(key =>
|
|
22
|
+
/DATABASE|DB|MONGO|POSTGRES|MYSQL|REDIS/i.test(key)
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
let hasDbConfig = dbVars.length > 0;
|
|
26
|
+
let dbType = null;
|
|
27
|
+
|
|
28
|
+
if (envVars.DATABASE_URL) {
|
|
29
|
+
const dbUrl = envVars.DATABASE_URL.toLowerCase();
|
|
30
|
+
if (dbUrl.includes('mongodb')) dbType = 'MongoDB';
|
|
31
|
+
else if (dbUrl.includes('postgres')) dbType = 'PostgreSQL';
|
|
32
|
+
else if (dbUrl.includes('mysql')) dbType = 'MySQL';
|
|
33
|
+
else if (dbUrl.includes('redis')) dbType = 'Redis';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check package.json for database drivers
|
|
37
|
+
const packageJsonPath = path.join(projectPath, 'package.json');
|
|
38
|
+
const packageJsonContent = await readFile(packageJsonPath);
|
|
39
|
+
let packageJson = {};
|
|
40
|
+
|
|
41
|
+
if (packageJsonContent) {
|
|
42
|
+
try {
|
|
43
|
+
packageJson = JSON.parse(packageJsonContent);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
// Invalid JSON
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
50
|
+
const dbPackages = {
|
|
51
|
+
'mongoose': 'MongoDB',
|
|
52
|
+
'mongodb': 'MongoDB',
|
|
53
|
+
'pg': 'PostgreSQL',
|
|
54
|
+
'mysql2': 'MySQL',
|
|
55
|
+
'mysql': 'MySQL',
|
|
56
|
+
'redis': 'Redis',
|
|
57
|
+
'ioredis': 'Redis',
|
|
58
|
+
'prisma': 'Prisma ORM',
|
|
59
|
+
'sequelize': 'Sequelize ORM',
|
|
60
|
+
'typeorm': 'TypeORM'
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
let detectedDbPackage = null;
|
|
64
|
+
Object.keys(dbPackages).forEach(pkg => {
|
|
65
|
+
if (deps[pkg]) {
|
|
66
|
+
detectedDbPackage = dbPackages[pkg];
|
|
67
|
+
if (!dbType) dbType = dbPackages[pkg];
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Check for database connection files
|
|
72
|
+
const dbFiles = await findFiles('**/{db,database,config}/**/*.{js,ts}', projectPath);
|
|
73
|
+
const connectionFiles = await findFiles('**/*connection*.{js,ts}', projectPath);
|
|
74
|
+
const modelFiles = await findFiles('**/{models,schemas}/**/*.{js,ts}', projectPath);
|
|
75
|
+
|
|
76
|
+
let hasConnectionFile = dbFiles.length > 0 || connectionFiles.length > 0;
|
|
77
|
+
let hasModels = modelFiles.length > 0;
|
|
78
|
+
|
|
79
|
+
// Check for connection error handling
|
|
80
|
+
let hasConnectionHandling = false;
|
|
81
|
+
for (const filePath of [...dbFiles, ...connectionFiles].slice(0, 10)) {
|
|
82
|
+
const code = await readFile(filePath);
|
|
83
|
+
if (code) {
|
|
84
|
+
if (/catch|error|on\('error'\)/i.test(code)) {
|
|
85
|
+
hasConnectionHandling = true;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Validation checks
|
|
92
|
+
if (!hasDbConfig && !detectedDbPackage) {
|
|
93
|
+
warnings.push('No database configuration detected');
|
|
94
|
+
} else {
|
|
95
|
+
if (dbType) {
|
|
96
|
+
success(`Database type detected: ${dbType}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Check for connection string security
|
|
100
|
+
if (envVars.DATABASE_URL) {
|
|
101
|
+
const dbUrl = envVars.DATABASE_URL;
|
|
102
|
+
if (dbUrl.includes('localhost') || dbUrl.includes('127.0.0.1')) {
|
|
103
|
+
warnings.push('Database URL points to localhost (ensure production uses remote database)');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Check for credentials in URL (should be in env)
|
|
107
|
+
if (!dbUrl.startsWith('${') && dbUrl.includes('@') && !dbUrl.includes('://')) {
|
|
108
|
+
// Might be okay, but check format
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Check for missing connection handling
|
|
113
|
+
if (!hasConnectionHandling && hasConnectionFile) {
|
|
114
|
+
issues.push('Database connection error handling not detected');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check for connection pooling
|
|
118
|
+
let hasPooling = false;
|
|
119
|
+
for (const filePath of [...dbFiles, ...connectionFiles].slice(0, 10)) {
|
|
120
|
+
const code = await readFile(filePath);
|
|
121
|
+
if (code && /pool|pooling|max.*connection/i.test(code)) {
|
|
122
|
+
hasPooling = true;
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!hasPooling && detectedDbPackage) {
|
|
128
|
+
warnings.push('Connection pooling not detected (recommended for production)');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Check for migration files
|
|
133
|
+
const migrationFiles = await findFiles('**/{migrations,migrate}/**/*.{js,ts,sql}', projectPath);
|
|
134
|
+
if (migrationFiles.length === 0 && detectedDbPackage) {
|
|
135
|
+
warnings.push('No migration files detected (recommended for database versioning)');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Print results
|
|
139
|
+
console.log('\n🔹 DATABASE VALIDATION\n');
|
|
140
|
+
|
|
141
|
+
if (hasDbConfig || detectedDbPackage) {
|
|
142
|
+
if (hasConnectionFile) success('Database connection file found');
|
|
143
|
+
if (hasModels) success('Database models/schemas found');
|
|
144
|
+
if (hasConnectionHandling) success('Connection error handling found');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (issues.length === 0 && warnings.length === 0 && (hasDbConfig || detectedDbPackage)) {
|
|
148
|
+
success('Database configuration looks good');
|
|
149
|
+
verdict(true, 'DATABASE: READY');
|
|
150
|
+
return { passed: true, issues: [], warnings: [] };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!hasDbConfig && !detectedDbPackage) {
|
|
154
|
+
verdict(true, 'DATABASE: SKIPPED (No database detected)');
|
|
155
|
+
return { passed: true, issues: [], warnings: [], skipped: true };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
printIssues(issues, 'error');
|
|
159
|
+
printIssues(warnings, 'warning');
|
|
160
|
+
|
|
161
|
+
if (issues.length === 0) {
|
|
162
|
+
verdict(true, 'DATABASE: READY (with warnings)');
|
|
163
|
+
} else {
|
|
164
|
+
verdict(false, 'DATABASE: NOT READY');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
passed: issues.length === 0,
|
|
169
|
+
issues,
|
|
170
|
+
warnings,
|
|
171
|
+
dbType
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = {
|
|
176
|
+
validate
|
|
177
|
+
};
|
|
178
|
+
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { readFile, fileExists } = require('../utils/fileHelpers');
|
|
3
|
+
const { error, success, verdict, printIssues, warning } = require('../utils/logHelpers');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Validate dependencies
|
|
7
|
+
*/
|
|
8
|
+
async function validate(projectPath = process.cwd()) {
|
|
9
|
+
const issues = [];
|
|
10
|
+
const warnings = [];
|
|
11
|
+
|
|
12
|
+
const packageJsonPath = path.join(projectPath, 'package.json');
|
|
13
|
+
const packageLockPath = path.join(projectPath, 'package-lock.json');
|
|
14
|
+
const yarnLockPath = path.join(projectPath, 'yarn.lock');
|
|
15
|
+
|
|
16
|
+
// Check for package.json
|
|
17
|
+
if (!await fileExists(packageJsonPath)) {
|
|
18
|
+
issues.push('package.json not found');
|
|
19
|
+
verdict(false, 'DEPENDENCIES: NOT READY');
|
|
20
|
+
return { passed: false, issues, warnings: [] };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const packageJsonContent = await readFile(packageJsonPath);
|
|
24
|
+
let packageJson = {};
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
packageJson = JSON.parse(packageJsonContent);
|
|
28
|
+
} catch (e) {
|
|
29
|
+
issues.push('package.json is malformed');
|
|
30
|
+
verdict(false, 'DEPENDENCIES: NOT READY');
|
|
31
|
+
return { passed: false, issues, warnings: [] };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check for lock file
|
|
35
|
+
const hasLockFile = await fileExists(packageLockPath) || await fileExists(yarnLockPath);
|
|
36
|
+
if (!hasLockFile) {
|
|
37
|
+
warnings.push('Lock file (package-lock.json or yarn.lock) not found (recommended for reproducible builds)');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check for critical dependencies
|
|
41
|
+
const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
42
|
+
const criticalDeps = {
|
|
43
|
+
'express': 'Express.js',
|
|
44
|
+
'fastify': 'Fastify',
|
|
45
|
+
'koa': 'Koa',
|
|
46
|
+
'nestjs': 'NestJS'
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
let hasFramework = false;
|
|
50
|
+
Object.keys(criticalDeps).forEach(dep => {
|
|
51
|
+
if (deps[dep]) {
|
|
52
|
+
hasFramework = true;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (!hasFramework) {
|
|
57
|
+
warnings.push('No major web framework detected (Express, Fastify, Koa, NestJS)');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check for outdated packages (basic check)
|
|
61
|
+
const nodeVersion = process.version;
|
|
62
|
+
const nodeMajor = parseInt(nodeVersion.replace('v', '').split('.')[0]);
|
|
63
|
+
|
|
64
|
+
if (nodeMajor < 14) {
|
|
65
|
+
warnings.push(`Node.js version ${nodeVersion} is outdated (recommended: >= 14.0.0)`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Check for common security-related packages
|
|
69
|
+
const securityPackages = ['helmet', 'cors', 'express-rate-limit', 'bcrypt', 'jsonwebtoken'];
|
|
70
|
+
const missingSecurity = securityPackages.filter(pkg => !deps[pkg]);
|
|
71
|
+
|
|
72
|
+
if (missingSecurity.length > 0) {
|
|
73
|
+
warnings.push(`Missing security packages: ${missingSecurity.join(', ')}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Check for vulnerable patterns in dependencies
|
|
77
|
+
const vulnerablePatterns = {
|
|
78
|
+
'express': { min: '4.17.0', reason: 'Older versions have security vulnerabilities' },
|
|
79
|
+
'lodash': { min: '4.17.21', reason: 'Older versions have security vulnerabilities' }
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
Object.keys(vulnerablePatterns).forEach(pkg => {
|
|
83
|
+
if (deps[pkg]) {
|
|
84
|
+
const version = deps[pkg].replace(/[\^~]/, '');
|
|
85
|
+
const pattern = vulnerablePatterns[pkg];
|
|
86
|
+
// Basic version check (simplified)
|
|
87
|
+
if (version && version.includes('4.') && pkg === 'express') {
|
|
88
|
+
warnings.push(`${pkg} version might be outdated - ${pattern.reason}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Check for too many dependencies (maintenance burden)
|
|
94
|
+
const totalDeps = Object.keys(deps).length;
|
|
95
|
+
if (totalDeps > 100) {
|
|
96
|
+
warnings.push(`Large number of dependencies (${totalDeps}) - consider reviewing for unused packages`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Check for dev dependencies in production
|
|
100
|
+
if (packageJson.scripts && packageJson.scripts.start) {
|
|
101
|
+
const startScript = packageJson.scripts.start;
|
|
102
|
+
if (startScript.includes('nodemon') || startScript.includes('ts-node-dev')) {
|
|
103
|
+
warnings.push('Development tools in start script (use production tools in production)');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Try to run npm audit (if available)
|
|
108
|
+
let auditIssues = [];
|
|
109
|
+
try {
|
|
110
|
+
// Check if npm audit is available (don't run it, just check)
|
|
111
|
+
const hasNpmAudit = true; // Assume available
|
|
112
|
+
if (hasNpmAudit) {
|
|
113
|
+
warnings.push('Run "npm audit" to check for known vulnerabilities');
|
|
114
|
+
}
|
|
115
|
+
} catch (e) {
|
|
116
|
+
// npm audit not available or failed
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Print results
|
|
120
|
+
console.log('\n🔹 DEPENDENCIES VALIDATION\n');
|
|
121
|
+
|
|
122
|
+
if (hasLockFile) success('Lock file found');
|
|
123
|
+
if (hasFramework) success('Web framework detected');
|
|
124
|
+
|
|
125
|
+
if (issues.length === 0 && warnings.length === 0) {
|
|
126
|
+
success('Dependencies look good');
|
|
127
|
+
verdict(true, 'DEPENDENCIES: READY');
|
|
128
|
+
return { passed: true, issues: [], warnings: [] };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
printIssues(issues, 'error');
|
|
132
|
+
printIssues(warnings, 'warning');
|
|
133
|
+
|
|
134
|
+
if (issues.length === 0) {
|
|
135
|
+
verdict(true, 'DEPENDENCIES: READY (with warnings)');
|
|
136
|
+
} else {
|
|
137
|
+
verdict(false, 'DEPENDENCIES: NOT READY');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
passed: issues.length === 0,
|
|
142
|
+
issues,
|
|
143
|
+
warnings,
|
|
144
|
+
totalDependencies: totalDeps
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = {
|
|
149
|
+
validate
|
|
150
|
+
};
|
|
151
|
+
|