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,117 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { fileExists, parseEnvFile, readFile } = require('../utils/fileHelpers');
|
|
3
|
+
const { error, success, verdict, printIssues } = require('../utils/logHelpers');
|
|
4
|
+
const { isValidUrl, isValidEmail, isValidNumber } = require('../utils/parseHelpers');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Validate environment variables
|
|
8
|
+
*/
|
|
9
|
+
async function validate(projectPath = process.cwd()) {
|
|
10
|
+
const issues = [];
|
|
11
|
+
const warnings = [];
|
|
12
|
+
|
|
13
|
+
const envPath = path.join(projectPath, '.env');
|
|
14
|
+
const envExamplePath = path.join(projectPath, '.env.example');
|
|
15
|
+
|
|
16
|
+
// Check if .env exists
|
|
17
|
+
const envExists = await fileExists(envPath);
|
|
18
|
+
const envExampleExists = await fileExists(envExamplePath);
|
|
19
|
+
|
|
20
|
+
if (!envExists) {
|
|
21
|
+
issues.push('MISSING: .env file not found');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!envExampleExists) {
|
|
25
|
+
warnings.push('WARNING: .env.example file not found (recommended for team collaboration)');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Parse .env.example to get expected variables
|
|
29
|
+
let expectedVars = {};
|
|
30
|
+
if (envExampleExists) {
|
|
31
|
+
expectedVars = await parseEnvFile(envExamplePath);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Parse .env file
|
|
35
|
+
let actualVars = {};
|
|
36
|
+
if (envExists) {
|
|
37
|
+
actualVars = await parseEnvFile(envPath);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check for missing variables
|
|
41
|
+
const missingVars = Object.keys(expectedVars).filter(key => !(key in actualVars));
|
|
42
|
+
missingVars.forEach(key => {
|
|
43
|
+
issues.push(`MISSING: ${key}`);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Check for weak secrets
|
|
47
|
+
const secretKeys = Object.keys(actualVars).filter(key =>
|
|
48
|
+
/SECRET|KEY|PASSWORD|TOKEN/i.test(key)
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
secretKeys.forEach(key => {
|
|
52
|
+
const value = actualVars[key];
|
|
53
|
+
if (!value) {
|
|
54
|
+
issues.push(`EMPTY: ${key} is empty`);
|
|
55
|
+
} else if (value.length < 32 && key.toUpperCase().includes('SECRET')) {
|
|
56
|
+
issues.push(`WEAK SECRET: ${key} (length < 32)`);
|
|
57
|
+
} else if (value.length < 16 && /PASSWORD|KEY/i.test(key)) {
|
|
58
|
+
warnings.push(`WEAK: ${key} might be too short (length < 16)`);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Check for unused variables (in .env but not in .env.example)
|
|
63
|
+
if (envExampleExists) {
|
|
64
|
+
const unusedVars = Object.keys(actualVars).filter(key =>
|
|
65
|
+
!(key in expectedVars) && !key.startsWith('#')
|
|
66
|
+
);
|
|
67
|
+
if (unusedVars.length > 0) {
|
|
68
|
+
warnings.push(`UNUSED: Variables in .env but not in .env.example: ${unusedVars.join(', ')}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Type validation (basic checks)
|
|
73
|
+
Object.keys(actualVars).forEach(key => {
|
|
74
|
+
const value = actualVars[key];
|
|
75
|
+
|
|
76
|
+
if (key.includes('URL') && value && !isValidUrl(value) && !value.startsWith('${')) {
|
|
77
|
+
warnings.push(`INVALID URL: ${key} does not appear to be a valid URL`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (key.includes('EMAIL') && value && !isValidEmail(value) && !value.startsWith('${')) {
|
|
81
|
+
warnings.push(`INVALID EMAIL: ${key} does not appear to be a valid email`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (key.includes('PORT') && value && !isValidNumber(value)) {
|
|
85
|
+
warnings.push(`INVALID PORT: ${key} should be a number`);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Print results
|
|
90
|
+
console.log('\n🔹 ENV VALIDATION\n');
|
|
91
|
+
|
|
92
|
+
if (issues.length === 0 && warnings.length === 0) {
|
|
93
|
+
success('All environment variables are properly configured');
|
|
94
|
+
verdict(true, 'ENV: READY');
|
|
95
|
+
return { passed: true, issues: [], warnings: [] };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
printIssues(issues, 'error');
|
|
99
|
+
printIssues(warnings, 'warning');
|
|
100
|
+
|
|
101
|
+
if (issues.length === 0) {
|
|
102
|
+
verdict(true, 'ENV: READY (with warnings)');
|
|
103
|
+
} else {
|
|
104
|
+
verdict(false, 'ENV: NOT READY');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
passed: issues.length === 0,
|
|
109
|
+
issues,
|
|
110
|
+
warnings
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = {
|
|
115
|
+
validate
|
|
116
|
+
};
|
|
117
|
+
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { fileExists, readFile, getAllFiles } = require('../utils/fileHelpers');
|
|
3
|
+
const { error, success, verdict, printIssues } = require('../utils/logHelpers');
|
|
4
|
+
const { hasErrorHandling } = require('../utils/parseHelpers');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Validate project structure and best practices
|
|
8
|
+
*/
|
|
9
|
+
async function validate(projectPath = process.cwd()) {
|
|
10
|
+
const issues = [];
|
|
11
|
+
const warnings = [];
|
|
12
|
+
|
|
13
|
+
// Check for .env.example
|
|
14
|
+
const envExamplePath = path.join(projectPath, '.env.example');
|
|
15
|
+
const envExampleExists = await fileExists(envExamplePath);
|
|
16
|
+
|
|
17
|
+
if (!envExampleExists) {
|
|
18
|
+
issues.push('.env.example missing (required for team collaboration)');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Check for README
|
|
22
|
+
const readmePaths = [
|
|
23
|
+
path.join(projectPath, 'README.md'),
|
|
24
|
+
path.join(projectPath, 'README.txt'),
|
|
25
|
+
path.join(projectPath, 'readme.md')
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
let readmeExists = false;
|
|
29
|
+
let readmePath = null;
|
|
30
|
+
let readmeContent = '';
|
|
31
|
+
|
|
32
|
+
for (const readmePathOption of readmePaths) {
|
|
33
|
+
if (await fileExists(readmePathOption)) {
|
|
34
|
+
readmeExists = true;
|
|
35
|
+
readmePath = readmePathOption;
|
|
36
|
+
readmeContent = await readFile(readmePathOption) || '';
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!readmeExists) {
|
|
42
|
+
issues.push('README missing');
|
|
43
|
+
} else {
|
|
44
|
+
// Check if README has meaningful content
|
|
45
|
+
const meaningfulContent = readmeContent.length > 100;
|
|
46
|
+
const hasInstallation = /install|setup|getting started/i.test(readmeContent);
|
|
47
|
+
const hasUsage = /usage|how to|example/i.test(readmeContent);
|
|
48
|
+
|
|
49
|
+
if (!meaningfulContent) {
|
|
50
|
+
warnings.push('README exists but seems too short or empty');
|
|
51
|
+
} else if (!hasInstallation && !hasUsage) {
|
|
52
|
+
warnings.push('README missing installation or usage instructions');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check folder structure
|
|
57
|
+
const expectedFolders = ['src', 'routes', 'config', 'middleware', 'controllers', 'models'];
|
|
58
|
+
const existingFolders = [];
|
|
59
|
+
|
|
60
|
+
for (const folder of expectedFolders) {
|
|
61
|
+
const folderPath = path.join(projectPath, folder);
|
|
62
|
+
if (await fileExists(folderPath)) {
|
|
63
|
+
existingFolders.push(folder);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Check for src/ or similar structure
|
|
68
|
+
const srcPath = path.join(projectPath, 'src');
|
|
69
|
+
const hasSrc = await fileExists(srcPath);
|
|
70
|
+
|
|
71
|
+
if (!hasSrc && existingFolders.length === 0) {
|
|
72
|
+
warnings.push('No standard project structure detected (src/, routes/, etc.)');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check for error handling
|
|
76
|
+
let hasGlobalErrorHandler = false;
|
|
77
|
+
const errorHandlerPatterns = [
|
|
78
|
+
'**/middleware/**/*.{js,ts}',
|
|
79
|
+
'**/middleware.{js,ts}',
|
|
80
|
+
'**/error*.{js,ts}',
|
|
81
|
+
'**/src/**/*.{js,ts}'
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
for (const pattern of errorHandlerPatterns) {
|
|
85
|
+
const files = await require('../utils/fileHelpers').findFiles(pattern, projectPath);
|
|
86
|
+
for (const filePath of files.slice(0, 10)) { // Limit to first 10 files
|
|
87
|
+
const code = await readFile(filePath);
|
|
88
|
+
if (code && hasErrorHandling(code)) {
|
|
89
|
+
hasGlobalErrorHandler = true;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (hasGlobalErrorHandler) break;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Also check main app file
|
|
97
|
+
if (!hasGlobalErrorHandler) {
|
|
98
|
+
const mainFiles = await require('../utils/fileHelpers').findFiles('**/{app,server,index,main}.{js,ts}', projectPath);
|
|
99
|
+
for (const filePath of mainFiles.slice(0, 3)) {
|
|
100
|
+
const code = await readFile(filePath);
|
|
101
|
+
if (code && hasErrorHandling(code)) {
|
|
102
|
+
hasGlobalErrorHandler = true;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!hasGlobalErrorHandler) {
|
|
109
|
+
issues.push('Error handling middleware not found (recommended: global error handler)');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Check for package.json
|
|
113
|
+
const packageJsonPath = path.join(projectPath, 'package.json');
|
|
114
|
+
const packageJsonExists = await fileExists(packageJsonPath);
|
|
115
|
+
|
|
116
|
+
if (!packageJsonExists) {
|
|
117
|
+
issues.push('package.json missing');
|
|
118
|
+
} else {
|
|
119
|
+
const packageJsonContent = await readFile(packageJsonPath);
|
|
120
|
+
if (packageJsonContent) {
|
|
121
|
+
try {
|
|
122
|
+
const pkg = JSON.parse(packageJsonContent);
|
|
123
|
+
|
|
124
|
+
// Check for start script
|
|
125
|
+
if (!pkg.scripts || !pkg.scripts.start) {
|
|
126
|
+
warnings.push('package.json missing "start" script');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Check for description
|
|
130
|
+
if (!pkg.description) {
|
|
131
|
+
warnings.push('package.json missing "description" field');
|
|
132
|
+
}
|
|
133
|
+
} catch (e) {
|
|
134
|
+
warnings.push('package.json might be malformed');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Print results
|
|
140
|
+
console.log('\n🔹 PROJECT VALIDATION\n');
|
|
141
|
+
|
|
142
|
+
if (readmeExists) {
|
|
143
|
+
success('README found');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (envExampleExists) {
|
|
147
|
+
success('.env.example found');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (hasGlobalErrorHandler) {
|
|
151
|
+
success('Error handling found');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (issues.length === 0 && warnings.length === 0) {
|
|
155
|
+
success('Project structure looks good');
|
|
156
|
+
verdict(true, 'PROJECT: READY');
|
|
157
|
+
return { passed: true, issues: [], warnings: [] };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
printIssues(issues, 'error');
|
|
161
|
+
printIssues(warnings, 'warning');
|
|
162
|
+
|
|
163
|
+
verdict(false, 'PROJECT: NOT READY');
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
passed: issues.length === 0,
|
|
167
|
+
issues,
|
|
168
|
+
warnings
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = {
|
|
173
|
+
validate
|
|
174
|
+
};
|
|
175
|
+
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const envModule = require('./env');
|
|
2
|
+
const authModule = require('./auth');
|
|
3
|
+
const apiModule = require('./api');
|
|
4
|
+
const projectModule = require('./project');
|
|
5
|
+
const securityModule = require('./security');
|
|
6
|
+
const dependenciesModule = require('./dependencies');
|
|
7
|
+
const databaseModule = require('./database');
|
|
8
|
+
const { header, verdict, error, success } = require('../utils/logHelpers');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generate comprehensive report combining all checks
|
|
12
|
+
*/
|
|
13
|
+
async function generate(projectPath = process.cwd(), options = {}) {
|
|
14
|
+
const { json = false, verbose = false, skip = [] } = options;
|
|
15
|
+
|
|
16
|
+
header('READY-TO-SHIP REPORT');
|
|
17
|
+
|
|
18
|
+
// Run all validations (skip specified modules)
|
|
19
|
+
const allModules = {
|
|
20
|
+
env: envModule,
|
|
21
|
+
auth: authModule,
|
|
22
|
+
api: apiModule,
|
|
23
|
+
project: projectModule,
|
|
24
|
+
security: securityModule,
|
|
25
|
+
dependencies: dependenciesModule,
|
|
26
|
+
database: databaseModule
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const results = {};
|
|
30
|
+
for (const [name, module] of Object.entries(allModules)) {
|
|
31
|
+
if (!skip.includes(name)) {
|
|
32
|
+
results[name] = await module.validate(projectPath);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Calculate overall status
|
|
37
|
+
const allPassed = Object.values(results).every(result => result.passed);
|
|
38
|
+
|
|
39
|
+
// Print summary
|
|
40
|
+
console.log('\n' + '='.repeat(50));
|
|
41
|
+
console.log('SUMMARY');
|
|
42
|
+
console.log('='.repeat(50) + '\n');
|
|
43
|
+
|
|
44
|
+
const statusIcon = (passed) => passed ? '✅' : '❌';
|
|
45
|
+
const statusText = (passed) => passed ? 'PASS' : 'FAIL';
|
|
46
|
+
|
|
47
|
+
// Print module statuses
|
|
48
|
+
Object.entries(results).forEach(([name, result]) => {
|
|
49
|
+
const nameUpper = name.toUpperCase().padEnd(12);
|
|
50
|
+
console.log(`${nameUpper} ${statusIcon(result.passed)} ${statusText(result.passed)}`);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Count issues
|
|
54
|
+
const totalIssues = Object.values(results).reduce((sum, result) => sum + (result.issues?.length || 0), 0);
|
|
55
|
+
const totalWarnings = Object.values(results).reduce((sum, result) => sum + (result.warnings?.length || 0), 0);
|
|
56
|
+
|
|
57
|
+
console.log('\n' + '='.repeat(50));
|
|
58
|
+
console.log(`Total Issues: ${totalIssues}`);
|
|
59
|
+
console.log(`Total Warnings: ${totalWarnings}`);
|
|
60
|
+
console.log('='.repeat(50) + '\n');
|
|
61
|
+
|
|
62
|
+
// Final verdict
|
|
63
|
+
if (allPassed) {
|
|
64
|
+
verdict(true, 'FINAL VERDICT: ✅ READY TO SHIP');
|
|
65
|
+
success('Your backend project looks ready for deployment!');
|
|
66
|
+
} else {
|
|
67
|
+
verdict(false, 'FINAL VERDICT: ❌ NOT READY');
|
|
68
|
+
error('Please fix the issues above before deploying.');
|
|
69
|
+
|
|
70
|
+
if (verbose) {
|
|
71
|
+
console.log('\n📋 Detailed Issues:\n');
|
|
72
|
+
Object.entries(results).forEach(([module, result]) => {
|
|
73
|
+
if (result.issues && result.issues.length > 0) {
|
|
74
|
+
console.log(`\n${module.toUpperCase()}:`);
|
|
75
|
+
result.issues.forEach(issue => error(` - ${issue}`));
|
|
76
|
+
}
|
|
77
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
78
|
+
result.warnings.forEach(warning => console.log(` ⚠️ ${warning}`));
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Export to JSON if requested
|
|
85
|
+
if (json) {
|
|
86
|
+
const jsonReport = {
|
|
87
|
+
timestamp: new Date().toISOString(),
|
|
88
|
+
projectPath,
|
|
89
|
+
verdict: allPassed ? 'READY' : 'NOT_READY',
|
|
90
|
+
summary: Object.fromEntries(
|
|
91
|
+
Object.entries(results).map(([name, result]) => [
|
|
92
|
+
name,
|
|
93
|
+
{
|
|
94
|
+
passed: result.passed,
|
|
95
|
+
issues: result.issues?.length || 0,
|
|
96
|
+
warnings: result.warnings?.length || 0
|
|
97
|
+
}
|
|
98
|
+
])
|
|
99
|
+
),
|
|
100
|
+
details: results
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const fs = require('fs-extra');
|
|
104
|
+
const reportPath = require('path').join(projectPath, 'ready-to-ship-report.json');
|
|
105
|
+
await fs.writeJson(reportPath, jsonReport, { spaces: 2 });
|
|
106
|
+
console.log(`\n📄 JSON report saved to: ${reportPath}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
passed: allPassed,
|
|
111
|
+
results
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = {
|
|
116
|
+
generate
|
|
117
|
+
};
|
|
118
|
+
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { findFiles, readFile } = require('../utils/fileHelpers');
|
|
3
|
+
const { error, success, verdict, printIssues } = require('../utils/logHelpers');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Validate security configurations
|
|
7
|
+
*/
|
|
8
|
+
async function validate(projectPath = process.cwd()) {
|
|
9
|
+
const issues = [];
|
|
10
|
+
const warnings = [];
|
|
11
|
+
|
|
12
|
+
// Find main app files
|
|
13
|
+
const appFiles = await findFiles('**/{app,server,index,main}.{js,ts}', projectPath);
|
|
14
|
+
const configFiles = await findFiles('**/config/**/*.{js,ts}', projectPath);
|
|
15
|
+
const middlewareFiles = await findFiles('**/middleware/**/*.{js,ts}', projectPath);
|
|
16
|
+
|
|
17
|
+
const allFiles = [...appFiles, ...configFiles, ...middlewareFiles];
|
|
18
|
+
|
|
19
|
+
let hasCORS = false;
|
|
20
|
+
let hasHelmet = false;
|
|
21
|
+
let hasRateLimit = false;
|
|
22
|
+
let hasSecurityHeaders = false;
|
|
23
|
+
let corsConfig = null;
|
|
24
|
+
|
|
25
|
+
// Analyze files for security features
|
|
26
|
+
for (const filePath of allFiles.slice(0, 20)) { // Limit to first 20 files
|
|
27
|
+
const code = await readFile(filePath);
|
|
28
|
+
if (!code) continue;
|
|
29
|
+
|
|
30
|
+
// Check for CORS
|
|
31
|
+
if (/cors|CORS/i.test(code)) {
|
|
32
|
+
hasCORS = true;
|
|
33
|
+
const corsMatch = code.match(/cors\s*\([^)]*\)/i);
|
|
34
|
+
if (corsMatch) {
|
|
35
|
+
corsConfig = corsMatch[0];
|
|
36
|
+
// Check for overly permissive CORS
|
|
37
|
+
if (/origin\s*:\s*['"]\*['"]/i.test(corsConfig)) {
|
|
38
|
+
issues.push('CORS configured with wildcard origin (*) - security risk');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check for Helmet.js (security headers)
|
|
44
|
+
if (/helmet|helmet\(\)/i.test(code)) {
|
|
45
|
+
hasHelmet = true;
|
|
46
|
+
hasSecurityHeaders = true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Check for security headers manually
|
|
50
|
+
if (/x-frame-options|x-content-type-options|x-xss-protection|strict-transport-security/i.test(code)) {
|
|
51
|
+
hasSecurityHeaders = true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check for rate limiting
|
|
55
|
+
if (/rateLimit|rate-limit|express-rate-limit|limiter/i.test(code)) {
|
|
56
|
+
hasRateLimit = true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check package.json for security dependencies
|
|
61
|
+
const packageJsonPath = path.join(projectPath, 'package.json');
|
|
62
|
+
const packageJsonContent = await readFile(packageJsonPath);
|
|
63
|
+
let packageJson = {};
|
|
64
|
+
|
|
65
|
+
if (packageJsonContent) {
|
|
66
|
+
try {
|
|
67
|
+
packageJson = JSON.parse(packageJsonContent);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
// Invalid JSON
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
74
|
+
const securityPackages = {
|
|
75
|
+
'helmet': 'helmet',
|
|
76
|
+
'cors': 'cors',
|
|
77
|
+
'express-rate-limit': 'express-rate-limit',
|
|
78
|
+
'express-slow-down': 'express-slow-down',
|
|
79
|
+
'hpp': 'hpp',
|
|
80
|
+
'xss': 'xss'
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
Object.keys(securityPackages).forEach(pkg => {
|
|
84
|
+
if (!deps[pkg] && !hasHelmet && pkg === 'helmet') {
|
|
85
|
+
warnings.push(`Security package "${pkg}" not found (recommended: npm install ${pkg})`);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Security checks
|
|
90
|
+
if (!hasCORS) {
|
|
91
|
+
warnings.push('CORS middleware not detected (recommended for API security)');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!hasSecurityHeaders) {
|
|
95
|
+
issues.push('Security headers not configured (use Helmet.js or configure manually)');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!hasRateLimit) {
|
|
99
|
+
warnings.push('Rate limiting not detected (recommended to prevent abuse)');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check for common security anti-patterns (exclude node_modules)
|
|
103
|
+
for (const filePath of allFiles.slice(0, 20)) {
|
|
104
|
+
// Skip node_modules
|
|
105
|
+
if (filePath.includes('node_modules')) continue;
|
|
106
|
+
|
|
107
|
+
const code = await readFile(filePath);
|
|
108
|
+
if (!code) continue;
|
|
109
|
+
|
|
110
|
+
// Check for eval usage
|
|
111
|
+
if (/\beval\s*\(/i.test(code)) {
|
|
112
|
+
issues.push(`eval() usage detected in ${path.relative(projectPath, filePath)} - security risk`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Check for dangerous regex
|
|
116
|
+
if (/new RegExp\([^)]*\+/i.test(code)) {
|
|
117
|
+
warnings.push(`Dynamic regex construction in ${path.relative(projectPath, filePath)} - potential ReDoS risk`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Print results
|
|
122
|
+
console.log('\n🔹 SECURITY VALIDATION\n');
|
|
123
|
+
|
|
124
|
+
if (hasCORS) success('CORS configured');
|
|
125
|
+
if (hasSecurityHeaders) success('Security headers configured');
|
|
126
|
+
if (hasRateLimit) success('Rate limiting detected');
|
|
127
|
+
|
|
128
|
+
if (issues.length === 0 && warnings.length === 0) {
|
|
129
|
+
success('Security configuration looks good');
|
|
130
|
+
verdict(true, 'SECURITY: READY');
|
|
131
|
+
return { passed: true, issues: [], warnings: [] };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
printIssues(issues, 'error');
|
|
135
|
+
printIssues(warnings, 'warning');
|
|
136
|
+
|
|
137
|
+
verdict(false, 'SECURITY: NOT READY');
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
passed: issues.length === 0,
|
|
141
|
+
issues,
|
|
142
|
+
warnings
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = {
|
|
147
|
+
validate
|
|
148
|
+
};
|
|
149
|
+
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { glob } = require('glob');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Read file content safely
|
|
7
|
+
*/
|
|
8
|
+
async function readFile(filePath) {
|
|
9
|
+
try {
|
|
10
|
+
return await fs.readFile(filePath, 'utf-8');
|
|
11
|
+
} catch (error) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Check if file exists
|
|
18
|
+
*/
|
|
19
|
+
async function fileExists(filePath) {
|
|
20
|
+
try {
|
|
21
|
+
return await fs.pathExists(filePath);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Find files matching pattern
|
|
29
|
+
*/
|
|
30
|
+
async function findFiles(pattern, cwd = process.cwd()) {
|
|
31
|
+
try {
|
|
32
|
+
const files = await glob(pattern, { cwd, absolute: true });
|
|
33
|
+
return files;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get all files in directory recursively
|
|
41
|
+
*/
|
|
42
|
+
async function getAllFiles(dir, extensions = ['.js', '.ts', '.jsx', '.tsx']) {
|
|
43
|
+
const files = [];
|
|
44
|
+
try {
|
|
45
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
const fullPath = path.join(dir, entry.name);
|
|
48
|
+
if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
|
|
49
|
+
files.push(...await getAllFiles(fullPath, extensions));
|
|
50
|
+
} else if (entry.isFile()) {
|
|
51
|
+
const ext = path.extname(entry.name);
|
|
52
|
+
if (extensions.length === 0 || extensions.includes(ext)) {
|
|
53
|
+
files.push(fullPath);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
// Directory doesn't exist or permission denied
|
|
59
|
+
}
|
|
60
|
+
return files;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Parse .env file
|
|
65
|
+
*/
|
|
66
|
+
async function parseEnvFile(filePath) {
|
|
67
|
+
const content = await readFile(filePath);
|
|
68
|
+
if (!content) return {};
|
|
69
|
+
|
|
70
|
+
const env = {};
|
|
71
|
+
const lines = content.split('\n');
|
|
72
|
+
|
|
73
|
+
for (const line of lines) {
|
|
74
|
+
const trimmed = line.trim();
|
|
75
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
76
|
+
|
|
77
|
+
const match = trimmed.match(/^([^=]+)=(.*)$/);
|
|
78
|
+
if (match) {
|
|
79
|
+
const key = match[1].trim();
|
|
80
|
+
const value = match[2].trim().replace(/^["']|["']$/g, '');
|
|
81
|
+
env[key] = value;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return env;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = {
|
|
89
|
+
readFile,
|
|
90
|
+
fileExists,
|
|
91
|
+
findFiles,
|
|
92
|
+
getAllFiles,
|
|
93
|
+
parseEnvFile
|
|
94
|
+
};
|
|
95
|
+
|