ship18ion 1.0.0 → 1.1.3
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/dist/cli/index.js +28 -8
- package/dist/engine/detector.js +34 -0
- package/dist/engine/runner.js +25 -3
- package/dist/engine/scanner.js +10 -1
- package/dist/reporters/console.js +4 -1
- package/dist/rules/build.js +26 -22
- package/dist/rules/git.js +58 -2
- package/package.json +6 -1
- package/src/cli/index.ts +30 -8
- package/src/engine/detector.ts +27 -0
- package/src/engine/runner.ts +29 -3
- package/src/engine/scanner.ts +10 -1
- package/src/engine/types.ts +2 -0
- package/src/reporters/console.ts +5 -1
- package/src/rules/build.ts +29 -26
- package/src/rules/git.ts +61 -2
package/dist/cli/index.js
CHANGED
|
@@ -10,25 +10,45 @@ const config_1 = require("../engine/config");
|
|
|
10
10
|
const runner_1 = require("../engine/runner");
|
|
11
11
|
const console_1 = require("../reporters/console");
|
|
12
12
|
const program = new commander_1.Command();
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
const figlet_1 = __importDefault(require("figlet"));
|
|
14
|
+
const gradient_string_1 = __importDefault(require("gradient-string"));
|
|
15
|
+
const ora_1 = __importDefault(require("ora"));
|
|
16
|
+
const detector_1 = require("../engine/detector");
|
|
17
17
|
program
|
|
18
18
|
.command('check', { isDefault: true })
|
|
19
19
|
.description('Run production readiness checks')
|
|
20
20
|
.option('--ci', 'Run in CI mode (minimal output, exit codes)')
|
|
21
21
|
.action(async (options) => {
|
|
22
|
-
|
|
22
|
+
if (!options.ci) {
|
|
23
|
+
console.log(gradient_string_1.default.pastel.multiline(figlet_1.default.textSync('SHIP18ION')));
|
|
24
|
+
console.log(chalk_1.default.dim('Production Readiness Inspector\n'));
|
|
25
|
+
}
|
|
23
26
|
const cwd = process.cwd();
|
|
24
27
|
const config = await (0, config_1.loadConfig)(cwd);
|
|
28
|
+
const spinner = (0, ora_1.default)('Initializing...').start();
|
|
25
29
|
try {
|
|
26
|
-
|
|
30
|
+
let framework = 'unknown';
|
|
31
|
+
if (!options.ci) {
|
|
32
|
+
framework = await (0, detector_1.detectFramework)(cwd);
|
|
33
|
+
spinner.text = `Detected Framework: ${chalk_1.default.cyan(framework.toUpperCase())}`;
|
|
34
|
+
await new Promise(r => setTimeout(r, 800)); // Brief pause to show framework
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
// Even in CI, simple detection is useful for reporting if needed, or we just skip
|
|
38
|
+
framework = await (0, detector_1.detectFramework)(cwd);
|
|
39
|
+
}
|
|
40
|
+
const results = await (0, runner_1.runChecks)(config, cwd, (stage) => {
|
|
41
|
+
if (!options.ci)
|
|
42
|
+
spinner.text = stage;
|
|
43
|
+
});
|
|
44
|
+
spinner.succeed(chalk_1.default.green('Checks completed!'));
|
|
45
|
+
console.log('');
|
|
27
46
|
// Uses console reporter for both normal and CI for now (it handles exit codes)
|
|
28
|
-
(0, console_1.reportConsole)(results, cwd);
|
|
47
|
+
(0, console_1.reportConsole)(results, cwd, framework);
|
|
29
48
|
}
|
|
30
49
|
catch (e) {
|
|
31
|
-
|
|
50
|
+
spinner.fail(chalk_1.default.red('Error running checks'));
|
|
51
|
+
console.error(e);
|
|
32
52
|
process.exit(1);
|
|
33
53
|
}
|
|
34
54
|
});
|
|
@@ -0,0 +1,34 @@
|
|
|
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.detectFramework = detectFramework;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
async function detectFramework(cwd) {
|
|
10
|
+
const pkgPath = path_1.default.join(cwd, 'package.json');
|
|
11
|
+
if (!fs_1.default.existsSync(pkgPath)) {
|
|
12
|
+
return 'unknown';
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const pkg = JSON.parse(fs_1.default.readFileSync(pkgPath, 'utf-8'));
|
|
16
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
17
|
+
if (deps['next'])
|
|
18
|
+
return 'nextjs';
|
|
19
|
+
if (deps['@remix-run/react'])
|
|
20
|
+
return 'remix';
|
|
21
|
+
if (deps['vite'])
|
|
22
|
+
return 'vite';
|
|
23
|
+
if (deps['@nestjs/core'])
|
|
24
|
+
return 'nestjs';
|
|
25
|
+
if (deps['express'])
|
|
26
|
+
return 'express';
|
|
27
|
+
if (deps['fastify'])
|
|
28
|
+
return 'fastify';
|
|
29
|
+
return 'Node.js / Generic';
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
return 'unknown';
|
|
33
|
+
}
|
|
34
|
+
}
|
package/dist/engine/runner.js
CHANGED
|
@@ -8,17 +8,39 @@ const security_1 = require("../rules/security");
|
|
|
8
8
|
const build_1 = require("../rules/build");
|
|
9
9
|
const nextjs_1 = require("../rules/frameworks/nextjs");
|
|
10
10
|
const git_1 = require("../rules/git");
|
|
11
|
-
|
|
11
|
+
const detector_1 = require("./detector");
|
|
12
|
+
async function runChecks(config, cwd, onProgress) {
|
|
13
|
+
if (onProgress)
|
|
14
|
+
onProgress('Scanning files...');
|
|
12
15
|
const files = await (0, scanner_1.scanFiles)(cwd, config.ignore);
|
|
13
|
-
|
|
16
|
+
// Framework detection
|
|
17
|
+
const framework = await (0, detector_1.detectFramework)(cwd);
|
|
18
|
+
const ctx = { config, files, cwd, framework };
|
|
14
19
|
const results = [];
|
|
15
20
|
// Run all checks
|
|
21
|
+
if (onProgress)
|
|
22
|
+
onProgress('Checking environment variables...');
|
|
16
23
|
results.push(...await (0, env_1.checkEnvVars)(ctx));
|
|
24
|
+
if (onProgress)
|
|
25
|
+
onProgress('Scanning for secrets...');
|
|
17
26
|
results.push(...await (0, secrets_1.checkSecrets)(ctx));
|
|
27
|
+
if (onProgress)
|
|
28
|
+
onProgress('Analyzing security configurations...');
|
|
18
29
|
results.push(...await (0, security_1.checkSecurity)(ctx));
|
|
30
|
+
if (onProgress)
|
|
31
|
+
onProgress('Verifying dependencies...');
|
|
19
32
|
results.push(...await (0, build_1.checkDependencies)(ctx));
|
|
33
|
+
if (onProgress)
|
|
34
|
+
onProgress('Inspecting build artifacts...');
|
|
20
35
|
results.push(...await (0, build_1.checkBuild)(ctx));
|
|
21
|
-
|
|
36
|
+
// Framework specific checks
|
|
37
|
+
if (framework === 'nextjs') {
|
|
38
|
+
if (onProgress)
|
|
39
|
+
onProgress('Running Next.js specific checks...');
|
|
40
|
+
results.push(...await (0, nextjs_1.checkNextJs)(ctx));
|
|
41
|
+
}
|
|
42
|
+
if (onProgress)
|
|
43
|
+
onProgress('Checking git status...');
|
|
22
44
|
results.push(...await (0, git_1.checkGit)(ctx));
|
|
23
45
|
return results;
|
|
24
46
|
}
|
package/dist/engine/scanner.js
CHANGED
|
@@ -3,7 +3,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.scanFiles = scanFiles;
|
|
4
4
|
const glob_1 = require("glob");
|
|
5
5
|
async function scanFiles(cwd, ignore = []) {
|
|
6
|
-
|
|
6
|
+
// Ignore build artifacts, node_modules, and git
|
|
7
|
+
const defaultIgnore = [
|
|
8
|
+
'**/node_modules/**',
|
|
9
|
+
'**/.git/**',
|
|
10
|
+
'**/dist/**',
|
|
11
|
+
'**/build/**',
|
|
12
|
+
'**/.next/**',
|
|
13
|
+
'**/.turbo/**',
|
|
14
|
+
'**/coverage/**'
|
|
15
|
+
];
|
|
7
16
|
// Scan for relevant files: JS/TS code, Configs (JSON/YAML), Env files
|
|
8
17
|
return (0, glob_1.glob)('**/*.{js,ts,jsx,tsx,json,yaml,yml,env,env.*}', {
|
|
9
18
|
cwd,
|
|
@@ -17,7 +17,10 @@ function getCategory(ruleId) {
|
|
|
17
17
|
const prefix = ruleId.split('-')[0];
|
|
18
18
|
return CATEGORIES[prefix] || { icon: '❓', label: 'Other' };
|
|
19
19
|
}
|
|
20
|
-
function reportConsole(results, cwd) {
|
|
20
|
+
function reportConsole(results, cwd, framework) {
|
|
21
|
+
if (framework) {
|
|
22
|
+
console.log(chalk_1.default.blue(`ℹ️ Framework: ${framework.toUpperCase()}`));
|
|
23
|
+
}
|
|
21
24
|
if (results.length === 0) {
|
|
22
25
|
console.log(chalk_1.default.green('\n✅ Production Readiness Check Passed!\n'));
|
|
23
26
|
return;
|
package/dist/rules/build.js
CHANGED
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.checkDependencies = checkDependencies;
|
|
7
7
|
exports.checkBuild = checkBuild;
|
|
8
8
|
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
9
10
|
async function checkDependencies(ctx) {
|
|
10
11
|
const results = [];
|
|
11
12
|
const packageJsons = ctx.files.filter(f => f.endsWith('package.json') && !f.includes('node_modules'));
|
|
@@ -31,36 +32,39 @@ async function checkDependencies(ctx) {
|
|
|
31
32
|
}
|
|
32
33
|
return results;
|
|
33
34
|
}
|
|
35
|
+
const glob_1 = require("glob");
|
|
34
36
|
async function checkBuild(ctx) {
|
|
35
37
|
const results = [];
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
for
|
|
44
|
-
|
|
45
|
-
|
|
38
|
+
// Explicitly scan build folders (dist, build, .next, .output) for dangerous files
|
|
39
|
+
// The main scanner ignores these, so we check them separately here.
|
|
40
|
+
const buildDirs = ['dist', 'build', '.next', '.output'];
|
|
41
|
+
const foundBuildDirs = buildDirs.filter(d => fs_1.default.existsSync(path_1.default.join(ctx.cwd, d)));
|
|
42
|
+
if (foundBuildDirs.length === 0) {
|
|
43
|
+
return results;
|
|
44
|
+
}
|
|
45
|
+
// 1. Check for Source Maps (.map)
|
|
46
|
+
// We search inside the found build directories
|
|
47
|
+
for (const dir of foundBuildDirs) {
|
|
48
|
+
const mapFiles = await (0, glob_1.glob)(`${dir}/**/*.map`, { cwd: ctx.cwd, absolute: true });
|
|
49
|
+
if (mapFiles.length > 0) {
|
|
46
50
|
results.push({
|
|
47
51
|
status: 'warn',
|
|
48
|
-
message:
|
|
52
|
+
message: `Found ${mapFiles.length} source map files in '${dir}' (e.g. ${path_1.default.basename(mapFiles[0])}). Ensure these are not exposed publicly.`,
|
|
49
53
|
ruleId: 'build-source-map',
|
|
54
|
+
file: dir // Point to the directory itself
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
// 2. Check for .env files in build output
|
|
58
|
+
// We look for .env* files inside the build dir
|
|
59
|
+
const envFiles = await (0, glob_1.glob)(`${dir}/**/*.env*`, { cwd: ctx.cwd, absolute: true });
|
|
60
|
+
for (const file of envFiles) {
|
|
61
|
+
results.push({
|
|
62
|
+
status: 'fail',
|
|
63
|
+
message: `Environment file found in build output (${dir})!`,
|
|
64
|
+
ruleId: 'build-env-leak',
|
|
50
65
|
file
|
|
51
66
|
});
|
|
52
67
|
}
|
|
53
68
|
}
|
|
54
|
-
// Check for .env in build folders
|
|
55
|
-
const envInBuild = ctx.files.filter(f => (f.endsWith('.env') || f.includes('.env.')) &&
|
|
56
|
-
(f.includes('/dist/') || f.includes('/build/') || f.includes('/.next/')));
|
|
57
|
-
for (const file of envInBuild) {
|
|
58
|
-
results.push({
|
|
59
|
-
status: 'fail',
|
|
60
|
-
message: 'Environment file found in build output!',
|
|
61
|
-
ruleId: 'build-env-leak',
|
|
62
|
-
file
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
69
|
return results;
|
|
66
70
|
}
|
package/dist/rules/git.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.checkGit = checkGit;
|
|
4
7
|
const child_process_1 = require("child_process");
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
5
10
|
async function checkGit(ctx) {
|
|
6
11
|
const results = [];
|
|
7
12
|
try {
|
|
13
|
+
// ... (Existing git checks) ...
|
|
8
14
|
// Check for uncommitted changes
|
|
9
15
|
const status = (0, child_process_1.execSync)('git status --porcelain', { cwd: ctx.cwd, encoding: 'utf-8' });
|
|
10
16
|
if (status.trim().length > 0) {
|
|
@@ -18,17 +24,67 @@ async function checkGit(ctx) {
|
|
|
18
24
|
const branch = (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD', { cwd: ctx.cwd, encoding: 'utf-8' }).trim();
|
|
19
25
|
const allowedBranches = ['main', 'master', 'staging', 'production', 'prod'];
|
|
20
26
|
if (!allowedBranches.includes(branch)) {
|
|
27
|
+
// Warn, but maybe less aggressively? Keeping as warn.
|
|
21
28
|
results.push({
|
|
22
29
|
status: 'warn',
|
|
23
30
|
message: `You are on branch '${branch}'. Production builds typically come from main/master.`,
|
|
24
31
|
ruleId: 'git-branch',
|
|
25
32
|
});
|
|
26
33
|
}
|
|
34
|
+
// --- New: .gitignore Check ---
|
|
35
|
+
const gitignorePath = path_1.default.join(ctx.cwd, '.gitignore');
|
|
36
|
+
if (fs_1.default.existsSync(gitignorePath)) {
|
|
37
|
+
const content = fs_1.default.readFileSync(gitignorePath, 'utf-8');
|
|
38
|
+
const lines = content.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#'));
|
|
39
|
+
// Helper to check if item is ignored (naive grep)
|
|
40
|
+
const isIgnored = (item) => lines.some(l => l.includes(item));
|
|
41
|
+
const requiredIgnores = ['node_modules', '.env'];
|
|
42
|
+
if (ctx.framework === 'nextjs') {
|
|
43
|
+
requiredIgnores.push('.next');
|
|
44
|
+
}
|
|
45
|
+
else if (ctx.framework !== 'unknown') {
|
|
46
|
+
// For other frameworks, maybe 'dist' or 'build'
|
|
47
|
+
if (!isIgnored('dist') && !isIgnored('build')) {
|
|
48
|
+
// We can't strictly require one, but warn if NEITHER is found?
|
|
49
|
+
// Let's stick to safe defaults.
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
for (const item of requiredIgnores) {
|
|
53
|
+
if (!isIgnored(item)) {
|
|
54
|
+
results.push({
|
|
55
|
+
status: 'warn',
|
|
56
|
+
message: `.gitignore is missing '${item}'. This is critical for security and repo size.`,
|
|
57
|
+
ruleId: 'git-ignore-missing',
|
|
58
|
+
file: gitignorePath
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Check for specific dangerous files not being ignored
|
|
63
|
+
const dangerousPatterns = ['firebase.json', 'serviceAccountKey.json', '*.pem', '*.key'];
|
|
64
|
+
// This is tricky because firebase.json CAN be committed. serviceAccountKey.json should NOT.
|
|
65
|
+
if (!isIgnored('serviceAccountKey.json')) {
|
|
66
|
+
// Only warn if the FILE actually exists? Or just warn generic?
|
|
67
|
+
// Best to warn if the file exists AND isn't ignored.
|
|
68
|
+
if (fs_1.default.existsSync(path_1.default.join(ctx.cwd, 'serviceAccountKey.json'))) {
|
|
69
|
+
results.push({
|
|
70
|
+
status: 'fail',
|
|
71
|
+
message: 'serviceAccountKey.json exists but is NOT in .gitignore!',
|
|
72
|
+
ruleId: 'git-ignore-auth',
|
|
73
|
+
file: gitignorePath
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
results.push({
|
|
80
|
+
status: 'warn',
|
|
81
|
+
message: 'No .gitignore file found! node_modules and secrets might be committed.',
|
|
82
|
+
ruleId: 'git-no-ignore',
|
|
83
|
+
});
|
|
84
|
+
}
|
|
27
85
|
}
|
|
28
86
|
catch (e) {
|
|
29
87
|
// Not a git repo or git not found
|
|
30
|
-
// Silently fail or warn?
|
|
31
|
-
// results.push({ status: 'warn', message: 'Not a git repository or git command failed.', ruleId: 'git-error' });
|
|
32
88
|
}
|
|
33
89
|
return results;
|
|
34
90
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ship18ion",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/cli/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -27,11 +27,16 @@
|
|
|
27
27
|
"@babel/parser": "^7.28.5",
|
|
28
28
|
"@babel/traverse": "^7.28.5",
|
|
29
29
|
"@types/babel__traverse": "^7.28.0",
|
|
30
|
+
"@types/figlet": "^1.7.0",
|
|
31
|
+
"@types/gradient-string": "^1.1.6",
|
|
30
32
|
"@types/node": "^25.0.3",
|
|
31
33
|
"chalk": "^4.1.2",
|
|
32
34
|
"commander": "^14.0.2",
|
|
33
35
|
"dotenv": "^17.2.3",
|
|
36
|
+
"figlet": "^1.9.4",
|
|
34
37
|
"glob": "^13.0.0",
|
|
38
|
+
"gradient-string": "^3.0.0",
|
|
39
|
+
"ora": "^9.0.0",
|
|
35
40
|
"typescript": "^5.9.3"
|
|
36
41
|
}
|
|
37
42
|
}
|
package/src/cli/index.ts
CHANGED
|
@@ -7,26 +7,48 @@ import { reportConsole } from '../reporters/console';
|
|
|
7
7
|
|
|
8
8
|
const program = new Command();
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
import figlet from 'figlet';
|
|
11
|
+
import gradient from 'gradient-string';
|
|
12
|
+
import ora from 'ora';
|
|
13
|
+
import { detectFramework } from '../engine/detector';
|
|
14
14
|
|
|
15
15
|
program
|
|
16
16
|
.command('check', { isDefault: true })
|
|
17
17
|
.description('Run production readiness checks')
|
|
18
18
|
.option('--ci', 'Run in CI mode (minimal output, exit codes)')
|
|
19
19
|
.action(async (options) => {
|
|
20
|
-
|
|
20
|
+
if (!options.ci) {
|
|
21
|
+
console.log(gradient.pastel.multiline(figlet.textSync('SHIP18ION')));
|
|
22
|
+
console.log(chalk.dim('Production Readiness Inspector\n'));
|
|
23
|
+
}
|
|
24
|
+
|
|
21
25
|
const cwd = process.cwd();
|
|
22
26
|
const config = await loadConfig(cwd);
|
|
27
|
+
const spinner = ora('Initializing...').start();
|
|
23
28
|
|
|
24
29
|
try {
|
|
25
|
-
|
|
30
|
+
let framework: string = 'unknown';
|
|
31
|
+
if (!options.ci) {
|
|
32
|
+
framework = await detectFramework(cwd);
|
|
33
|
+
spinner.text = `Detected Framework: ${chalk.cyan(framework.toUpperCase())}`;
|
|
34
|
+
await new Promise(r => setTimeout(r, 800)); // Brief pause to show framework
|
|
35
|
+
} else {
|
|
36
|
+
// Even in CI, simple detection is useful for reporting if needed, or we just skip
|
|
37
|
+
framework = await detectFramework(cwd);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const results = await runChecks(config, cwd, (stage) => {
|
|
41
|
+
if (!options.ci) spinner.text = stage;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
spinner.succeed(chalk.green('Checks completed!'));
|
|
45
|
+
console.log('');
|
|
46
|
+
|
|
26
47
|
// Uses console reporter for both normal and CI for now (it handles exit codes)
|
|
27
|
-
reportConsole(results, cwd);
|
|
48
|
+
reportConsole(results, cwd, framework);
|
|
28
49
|
} catch (e) {
|
|
29
|
-
|
|
50
|
+
spinner.fail(chalk.red('Error running checks'));
|
|
51
|
+
console.error(e);
|
|
30
52
|
process.exit(1);
|
|
31
53
|
}
|
|
32
54
|
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export type FrameworkType = 'nextjs' | 'remix' | 'vite' | 'nestjs' | 'express' | 'fastify' | 'Node.js / Generic' | 'unknown';
|
|
5
|
+
|
|
6
|
+
export async function detectFramework(cwd: string): Promise<FrameworkType> {
|
|
7
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
8
|
+
if (!fs.existsSync(pkgPath)) {
|
|
9
|
+
return 'unknown';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
14
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
15
|
+
|
|
16
|
+
if (deps['next']) return 'nextjs';
|
|
17
|
+
if (deps['@remix-run/react']) return 'remix';
|
|
18
|
+
if (deps['vite']) return 'vite';
|
|
19
|
+
if (deps['@nestjs/core']) return 'nestjs';
|
|
20
|
+
if (deps['express']) return 'express';
|
|
21
|
+
if (deps['fastify']) return 'fastify';
|
|
22
|
+
|
|
23
|
+
return 'Node.js / Generic';
|
|
24
|
+
} catch (e) {
|
|
25
|
+
return 'unknown';
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/engine/runner.ts
CHANGED
|
@@ -8,19 +8,45 @@ import { checkDependencies, checkBuild } from '../rules/build';
|
|
|
8
8
|
import { checkNextJs } from '../rules/frameworks/nextjs';
|
|
9
9
|
import { checkGit } from '../rules/git';
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
import { detectFramework } from './detector';
|
|
12
|
+
|
|
13
|
+
export async function runChecks(
|
|
14
|
+
config: Ship18ionConfig,
|
|
15
|
+
cwd: string,
|
|
16
|
+
onProgress?: (stage: string) => void
|
|
17
|
+
): Promise<RuleResult[]> {
|
|
18
|
+
if (onProgress) onProgress('Scanning files...');
|
|
12
19
|
const files = await scanFiles(cwd, config.ignore);
|
|
13
|
-
|
|
20
|
+
// Framework detection
|
|
21
|
+
const framework = await detectFramework(cwd);
|
|
22
|
+
|
|
23
|
+
const ctx: RuleContext = { config, files, cwd, framework };
|
|
14
24
|
|
|
15
25
|
const results: RuleResult[] = [];
|
|
16
26
|
|
|
17
27
|
// Run all checks
|
|
28
|
+
if (onProgress) onProgress('Checking environment variables...');
|
|
18
29
|
results.push(...await checkEnvVars(ctx));
|
|
30
|
+
|
|
31
|
+
if (onProgress) onProgress('Scanning for secrets...');
|
|
19
32
|
results.push(...await checkSecrets(ctx));
|
|
33
|
+
|
|
34
|
+
if (onProgress) onProgress('Analyzing security configurations...');
|
|
20
35
|
results.push(...await checkSecurity(ctx));
|
|
36
|
+
|
|
37
|
+
if (onProgress) onProgress('Verifying dependencies...');
|
|
21
38
|
results.push(...await checkDependencies(ctx));
|
|
39
|
+
|
|
40
|
+
if (onProgress) onProgress('Inspecting build artifacts...');
|
|
22
41
|
results.push(...await checkBuild(ctx));
|
|
23
|
-
|
|
42
|
+
|
|
43
|
+
// Framework specific checks
|
|
44
|
+
if (framework === 'nextjs') {
|
|
45
|
+
if (onProgress) onProgress('Running Next.js specific checks...');
|
|
46
|
+
results.push(...await checkNextJs(ctx));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (onProgress) onProgress('Checking git status...');
|
|
24
50
|
results.push(...await checkGit(ctx));
|
|
25
51
|
|
|
26
52
|
return results;
|
package/src/engine/scanner.ts
CHANGED
|
@@ -2,7 +2,16 @@ import { glob } from 'glob';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
|
|
4
4
|
export async function scanFiles(cwd: string, ignore: string[] = []): Promise<string[]> {
|
|
5
|
-
|
|
5
|
+
// Ignore build artifacts, node_modules, and git
|
|
6
|
+
const defaultIgnore = [
|
|
7
|
+
'**/node_modules/**',
|
|
8
|
+
'**/.git/**',
|
|
9
|
+
'**/dist/**',
|
|
10
|
+
'**/build/**',
|
|
11
|
+
'**/.next/**',
|
|
12
|
+
'**/.turbo/**',
|
|
13
|
+
'**/coverage/**'
|
|
14
|
+
];
|
|
6
15
|
// Scan for relevant files: JS/TS code, Configs (JSON/YAML), Env files
|
|
7
16
|
return glob('**/*.{js,ts,jsx,tsx,json,yaml,yml,env,env.*}', {
|
|
8
17
|
cwd,
|
package/src/engine/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Ship18ionConfig } from './config';
|
|
2
|
+
import { FrameworkType } from './detector';
|
|
2
3
|
|
|
3
4
|
export interface RuleResult {
|
|
4
5
|
status: 'pass' | 'fail' | 'warn';
|
|
@@ -12,4 +13,5 @@ export interface RuleContext {
|
|
|
12
13
|
config: Ship18ionConfig;
|
|
13
14
|
files: string[];
|
|
14
15
|
cwd: string;
|
|
16
|
+
framework: FrameworkType;
|
|
15
17
|
}
|
package/src/reporters/console.ts
CHANGED
|
@@ -15,7 +15,11 @@ function getCategory(ruleId: string) {
|
|
|
15
15
|
return CATEGORIES[prefix] || { icon: '❓', label: 'Other' };
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export function reportConsole(results: RuleResult[], cwd: string) {
|
|
18
|
+
export function reportConsole(results: RuleResult[], cwd: string, framework?: string) {
|
|
19
|
+
if (framework) {
|
|
20
|
+
console.log(chalk.blue(`ℹ️ Framework: ${framework.toUpperCase()}`));
|
|
21
|
+
}
|
|
22
|
+
|
|
19
23
|
if (results.length === 0) {
|
|
20
24
|
console.log(chalk.green('\n✅ Production Readiness Check Passed!\n'));
|
|
21
25
|
return;
|
package/src/rules/build.ts
CHANGED
|
@@ -31,43 +31,46 @@ export async function checkDependencies(ctx: RuleContext): Promise<RuleResult[]>
|
|
|
31
31
|
return results;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
import { glob } from 'glob';
|
|
35
|
+
|
|
34
36
|
export async function checkBuild(ctx: RuleContext): Promise<RuleResult[]> {
|
|
35
37
|
const results: RuleResult[] = [];
|
|
36
38
|
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
// Explicitly scan build folders (dist, build, .next, .output) for dangerous files
|
|
40
|
+
// The main scanner ignores these, so we check them separately here.
|
|
41
|
+
|
|
42
|
+
const buildDirs = ['dist', 'build', '.next', '.output'];
|
|
43
|
+
const foundBuildDirs = buildDirs.filter(d => fs.existsSync(path.join(ctx.cwd, d)));
|
|
44
|
+
|
|
45
|
+
if (foundBuildDirs.length === 0) {
|
|
46
|
+
return results;
|
|
47
|
+
}
|
|
41
48
|
|
|
42
|
-
//
|
|
43
|
-
//
|
|
49
|
+
// 1. Check for Source Maps (.map)
|
|
50
|
+
// We search inside the found build directories
|
|
51
|
+
for (const dir of foundBuildDirs) {
|
|
52
|
+
const mapFiles = await glob(`${dir}/**/*.map`, { cwd: ctx.cwd, absolute: true });
|
|
44
53
|
|
|
45
|
-
|
|
46
|
-
for (const file of mapFiles) {
|
|
47
|
-
// Only if it looks like a build artifact
|
|
48
|
-
if (file.includes('/dist/') || file.includes('/build/') || file.includes('/.next/')) {
|
|
54
|
+
if (mapFiles.length > 0) {
|
|
49
55
|
results.push({
|
|
50
56
|
status: 'warn',
|
|
51
|
-
message:
|
|
57
|
+
message: `Found ${mapFiles.length} source map files in '${dir}' (e.g. ${path.basename(mapFiles[0])}). Ensure these are not exposed publicly.`,
|
|
52
58
|
ruleId: 'build-source-map',
|
|
53
|
-
file
|
|
59
|
+
file: dir // Point to the directory itself
|
|
54
60
|
});
|
|
55
61
|
}
|
|
56
|
-
}
|
|
57
62
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
(
|
|
61
|
-
(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
file
|
|
70
|
-
});
|
|
63
|
+
// 2. Check for .env files in build output
|
|
64
|
+
// We look for .env* files inside the build dir
|
|
65
|
+
const envFiles = await glob(`${dir}/**/*.env*`, { cwd: ctx.cwd, absolute: true });
|
|
66
|
+
for (const file of envFiles) {
|
|
67
|
+
results.push({
|
|
68
|
+
status: 'fail',
|
|
69
|
+
message: `Environment file found in build output (${dir})!`,
|
|
70
|
+
ruleId: 'build-env-leak',
|
|
71
|
+
file
|
|
72
|
+
});
|
|
73
|
+
}
|
|
71
74
|
}
|
|
72
75
|
|
|
73
76
|
return results;
|
package/src/rules/git.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { execSync } from 'child_process';
|
|
2
2
|
import { RuleContext, RuleResult } from '../engine/types';
|
|
3
3
|
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
|
|
4
7
|
export async function checkGit(ctx: RuleContext): Promise<RuleResult[]> {
|
|
5
8
|
const results: RuleResult[] = [];
|
|
6
9
|
|
|
7
10
|
try {
|
|
11
|
+
// ... (Existing git checks) ...
|
|
8
12
|
// Check for uncommitted changes
|
|
9
13
|
const status = execSync('git status --porcelain', { cwd: ctx.cwd, encoding: 'utf-8' });
|
|
10
14
|
if (status.trim().length > 0) {
|
|
@@ -19,6 +23,7 @@ export async function checkGit(ctx: RuleContext): Promise<RuleResult[]> {
|
|
|
19
23
|
const branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: ctx.cwd, encoding: 'utf-8' }).trim();
|
|
20
24
|
const allowedBranches = ['main', 'master', 'staging', 'production', 'prod'];
|
|
21
25
|
if (!allowedBranches.includes(branch)) {
|
|
26
|
+
// Warn, but maybe less aggressively? Keeping as warn.
|
|
22
27
|
results.push({
|
|
23
28
|
status: 'warn',
|
|
24
29
|
message: `You are on branch '${branch}'. Production builds typically come from main/master.`,
|
|
@@ -26,10 +31,64 @@ export async function checkGit(ctx: RuleContext): Promise<RuleResult[]> {
|
|
|
26
31
|
});
|
|
27
32
|
}
|
|
28
33
|
|
|
34
|
+
// --- New: .gitignore Check ---
|
|
35
|
+
const gitignorePath = path.join(ctx.cwd, '.gitignore');
|
|
36
|
+
if (fs.existsSync(gitignorePath)) {
|
|
37
|
+
const content = fs.readFileSync(gitignorePath, 'utf-8');
|
|
38
|
+
const lines = content.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#'));
|
|
39
|
+
|
|
40
|
+
// Helper to check if item is ignored (naive grep)
|
|
41
|
+
const isIgnored = (item: string) => lines.some(l => l.includes(item));
|
|
42
|
+
|
|
43
|
+
const requiredIgnores = ['node_modules', '.env'];
|
|
44
|
+
if (ctx.framework === 'nextjs') {
|
|
45
|
+
requiredIgnores.push('.next');
|
|
46
|
+
} else if (ctx.framework !== 'unknown') {
|
|
47
|
+
// For other frameworks, maybe 'dist' or 'build'
|
|
48
|
+
if (!isIgnored('dist') && !isIgnored('build')) {
|
|
49
|
+
// We can't strictly require one, but warn if NEITHER is found?
|
|
50
|
+
// Let's stick to safe defaults.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const item of requiredIgnores) {
|
|
55
|
+
if (!isIgnored(item)) {
|
|
56
|
+
results.push({
|
|
57
|
+
status: 'warn',
|
|
58
|
+
message: `.gitignore is missing '${item}'. This is critical for security and repo size.`,
|
|
59
|
+
ruleId: 'git-ignore-missing',
|
|
60
|
+
file: gitignorePath
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check for specific dangerous files not being ignored
|
|
66
|
+
const dangerousPatterns = ['firebase.json', 'serviceAccountKey.json', '*.pem', '*.key'];
|
|
67
|
+
// This is tricky because firebase.json CAN be committed. serviceAccountKey.json should NOT.
|
|
68
|
+
|
|
69
|
+
if (!isIgnored('serviceAccountKey.json')) {
|
|
70
|
+
// Only warn if the FILE actually exists? Or just warn generic?
|
|
71
|
+
// Best to warn if the file exists AND isn't ignored.
|
|
72
|
+
if (fs.existsSync(path.join(ctx.cwd, 'serviceAccountKey.json'))) {
|
|
73
|
+
results.push({
|
|
74
|
+
status: 'fail',
|
|
75
|
+
message: 'serviceAccountKey.json exists but is NOT in .gitignore!',
|
|
76
|
+
ruleId: 'git-ignore-auth',
|
|
77
|
+
file: gitignorePath
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
} else {
|
|
83
|
+
results.push({
|
|
84
|
+
status: 'warn',
|
|
85
|
+
message: 'No .gitignore file found! node_modules and secrets might be committed.',
|
|
86
|
+
ruleId: 'git-no-ignore',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
29
90
|
} catch (e) {
|
|
30
91
|
// Not a git repo or git not found
|
|
31
|
-
// Silently fail or warn?
|
|
32
|
-
// results.push({ status: 'warn', message: 'Not a git repository or git command failed.', ruleId: 'git-error' });
|
|
33
92
|
}
|
|
34
93
|
|
|
35
94
|
return results;
|