ship18ion 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/README.md +105 -0
- package/SHIPPING.md +57 -0
- package/dist/cli/index.js +35 -0
- package/dist/engine/ast.js +108 -0
- package/dist/engine/config.js +22 -0
- package/dist/engine/runner.js +24 -0
- package/dist/engine/scanner.js +14 -0
- package/dist/engine/secrets.js +28 -0
- package/dist/engine/types.js +2 -0
- package/dist/reporters/console.js +61 -0
- package/dist/rules/build.js +66 -0
- package/dist/rules/env.js +98 -0
- package/dist/rules/frameworks/nextjs.js +31 -0
- package/dist/rules/git.js +34 -0
- package/dist/rules/secrets.js +53 -0
- package/dist/rules/security.js +52 -0
- package/package.json +37 -0
- package/src/cli/index.ts +34 -0
- package/src/engine/ast.ts +84 -0
- package/src/engine/config.ts +28 -0
- package/src/engine/runner.ts +27 -0
- package/src/engine/scanner.ts +13 -0
- package/src/engine/secrets.ts +26 -0
- package/src/engine/types.ts +15 -0
- package/src/reporters/console.ts +62 -0
- package/src/rules/build.ts +74 -0
- package/src/rules/env.ts +99 -0
- package/src/rules/frameworks/nextjs.ts +33 -0
- package/src/rules/git.ts +36 -0
- package/src/rules/secrets.ts +53 -0
- package/src/rules/security.ts +55 -0
- package/tests/fixtures/leaky-app/.env +3 -0
- package/tests/fixtures/leaky-app/package.json +7 -0
- package/tests/fixtures/leaky-app/src/index.js +21 -0
- package/tsconfig.json +15 -0
- package/walkthrough.md +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# π ship18ion
|
|
2
|
+
|
|
3
|
+
> **"Production Readiness Inspector" for your apps.**
|
|
4
|
+
|
|
5
|
+
`ship18ion` (read as "ship-tion") is a CLI tool designed to prevent production disasters before they happen. It scans your codebase for environment configuration issues, leaked secrets, dangerous security misconfigurations, and build artifacts that shouldn't be there.
|
|
6
|
+
|
|
7
|
+
Think of it as `eslint` but for **deployability**.
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
## β¨ Features
|
|
13
|
+
|
|
14
|
+
- **π± Environment Hygiene**:
|
|
15
|
+
- Finds unused variables in your `.env` files.
|
|
16
|
+
- Detects usage of `process.env.VAR` that are missing definitions.
|
|
17
|
+
- Supports `.env`, `.env.production` and `import.meta.env` (Vite).
|
|
18
|
+
|
|
19
|
+
- **π Secret Detection**:
|
|
20
|
+
- Catches hardcoded secrets (AWS keys, Stripe keys, generic private keys).
|
|
21
|
+
- Uses entropy heuristics to find potential secrets hidden in plain sight.
|
|
22
|
+
- **Next.js Safety**: Warns if `NEXT_PUBLIC_` variables contain high-entropy strings (potential accidental leaks).
|
|
23
|
+
|
|
24
|
+
- **β οΈ Security & Config**:
|
|
25
|
+
- Alerts on `debug: true` or `NODE_ENV` mismatches.
|
|
26
|
+
- Detects dangerous CORS configurations (`origin: '*'`).
|
|
27
|
+
- Finds hardcoded database connection strings.
|
|
28
|
+
|
|
29
|
+
- **π¦ Build Safety**:
|
|
30
|
+
- Prevents source maps (`.map`) from leaking into production builds.
|
|
31
|
+
- Ensures `.env` files are not bundled into build output directories.
|
|
32
|
+
- Checks for dev dependencies (like `eslint`) accidentally listed in `dependencies`.
|
|
33
|
+
|
|
34
|
+
- **π· CI/CD Ready**:
|
|
35
|
+
- Zero config by default.
|
|
36
|
+
- Returns exit code `1` on failure to block bad builds.
|
|
37
|
+
|
|
38
|
+
## π¦ Installation
|
|
39
|
+
|
|
40
|
+
You can use it directly with `npx`:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx ship18ion check
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Or install it as a dev dependency:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm install --save-dev ship18ion
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## π Usage
|
|
53
|
+
|
|
54
|
+
Run the check in your project root:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npx ship18ion
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### CI Mode
|
|
61
|
+
For Continuous Integration pipelines (GitHub Actions, GitLab CI, etc.), use the `--ci` flag for minimal output and standard exit codes:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npx ship18ion check --ci
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## βοΈ Configuration
|
|
68
|
+
|
|
69
|
+
`ship18ion` works out of the box, but you can customize it by creating a `ship18ion.config.json` file in your root directory:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"env": {
|
|
74
|
+
"required": ["DATABASE_URL", "JWT_SECRET"],
|
|
75
|
+
"disallowed": ["DEBUG_TOKEN"]
|
|
76
|
+
},
|
|
77
|
+
"security": {
|
|
78
|
+
"noCorsWildcard": true,
|
|
79
|
+
"requireRateLimit": false
|
|
80
|
+
},
|
|
81
|
+
"ignore": [
|
|
82
|
+
"**/legacy-code/**",
|
|
83
|
+
"**/test-fixtures/**"
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## π‘οΈ Rules Breakdown
|
|
89
|
+
|
|
90
|
+
| Category | Rule | Description |
|
|
91
|
+
|----------|------|-------------|
|
|
92
|
+
| **Env** | `env-unused` | A variable is defined in `.env` but never referenced in code. |
|
|
93
|
+
| **Env** | `env-missing` | A variable is used in code (`process.env.X`) but not defined. |
|
|
94
|
+
| **Secrets** | `secret-pattern` | Matches regex for known keys (AWS, Stripe, OpenAI). |
|
|
95
|
+
| **Next.js** | `nextjs-public-secret` | High-entropy string found in `NEXT_PUBLIC_` variable. |
|
|
96
|
+
| **Security** | `security-cors` | Detects wildcard `Access-Control-Allow-Origin`. |
|
|
97
|
+
| **Git** | `git-dirty` | Warns if deploying with uncommitted changes. |
|
|
98
|
+
|
|
99
|
+
## π€ Contributing
|
|
100
|
+
|
|
101
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
102
|
+
|
|
103
|
+
## π License
|
|
104
|
+
|
|
105
|
+
ISC
|
package/SHIPPING.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Shipping ship18ion
|
|
2
|
+
|
|
3
|
+
## 1. Local Testing (Link)
|
|
4
|
+
To test `ship18ion` on your other local projects without publishing, use `npm link`.
|
|
5
|
+
|
|
6
|
+
1. **In this directory (ship18ion):**
|
|
7
|
+
```bash
|
|
8
|
+
npm run build
|
|
9
|
+
npm link
|
|
10
|
+
```
|
|
11
|
+
This creates a global symlink to your local version.
|
|
12
|
+
|
|
13
|
+
2. **In your target project directory:**
|
|
14
|
+
```bash
|
|
15
|
+
npm link ship18ion
|
|
16
|
+
```
|
|
17
|
+
Now you can run:
|
|
18
|
+
```bash
|
|
19
|
+
npx ship18ion check
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
3. **To unlink:**
|
|
23
|
+
```bash
|
|
24
|
+
# In target project
|
|
25
|
+
npm unlink -g ship18ion
|
|
26
|
+
|
|
27
|
+
# In ship18ion directory
|
|
28
|
+
npm unlink -g
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 2. Publishing to NPM
|
|
32
|
+
To share this tool with the world.
|
|
33
|
+
|
|
34
|
+
1. **Login to NPM:**
|
|
35
|
+
```bash
|
|
36
|
+
npm login
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
2. **Prepare:**
|
|
40
|
+
- Ensure `package.json` has the correct version.
|
|
41
|
+
- Run tests: `npm test` (or verifying script).
|
|
42
|
+
|
|
43
|
+
3. **Publish:**
|
|
44
|
+
```bash
|
|
45
|
+
npm publish
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
If you have a scoped package (e.g. `@yourname/ship18ion`), use:
|
|
49
|
+
```bash
|
|
50
|
+
npm publish --access public
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 3. Usage for Users
|
|
54
|
+
Once published, anyone can use it without installation:
|
|
55
|
+
```bash
|
|
56
|
+
npx ship18ion@latest
|
|
57
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const config_1 = require("../engine/config");
|
|
10
|
+
const runner_1 = require("../engine/runner");
|
|
11
|
+
const console_1 = require("../reporters/console");
|
|
12
|
+
const program = new commander_1.Command();
|
|
13
|
+
program
|
|
14
|
+
.name('ship18ion')
|
|
15
|
+
.description('Production Readiness Inspector')
|
|
16
|
+
.version('0.1.0');
|
|
17
|
+
program
|
|
18
|
+
.command('check', { isDefault: true })
|
|
19
|
+
.description('Run production readiness checks')
|
|
20
|
+
.option('--ci', 'Run in CI mode (minimal output, exit codes)')
|
|
21
|
+
.action(async (options) => {
|
|
22
|
+
// console.log(chalk.blue('Starting ship18ion checks...'));
|
|
23
|
+
const cwd = process.cwd();
|
|
24
|
+
const config = await (0, config_1.loadConfig)(cwd);
|
|
25
|
+
try {
|
|
26
|
+
const results = await (0, runner_1.runChecks)(config, cwd);
|
|
27
|
+
// Uses console reporter for both normal and CI for now (it handles exit codes)
|
|
28
|
+
(0, console_1.reportConsole)(results, cwd);
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
console.error(chalk_1.default.red('Error running checks:'), e);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.findEnvUsages = findEnvUsages;
|
|
40
|
+
const fs_1 = __importDefault(require("fs"));
|
|
41
|
+
const parser = __importStar(require("@babel/parser"));
|
|
42
|
+
const traverse_1 = __importDefault(require("@babel/traverse"));
|
|
43
|
+
const t = __importStar(require("@babel/types"));
|
|
44
|
+
function findEnvUsages(filePath) {
|
|
45
|
+
if (!fs_1.default.existsSync(filePath))
|
|
46
|
+
return [];
|
|
47
|
+
const code = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
48
|
+
const usages = [];
|
|
49
|
+
// Only parse JS/TS files
|
|
50
|
+
if (!/\.(js|ts|jsx|tsx)$/.test(filePath)) {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const ast = parser.parse(code, {
|
|
55
|
+
sourceType: 'module',
|
|
56
|
+
plugins: ['typescript', 'jsx'],
|
|
57
|
+
});
|
|
58
|
+
(0, traverse_1.default)(ast, {
|
|
59
|
+
MemberExpression(path) {
|
|
60
|
+
// 1. Check for process.env.VAR
|
|
61
|
+
if (t.isMemberExpression(path.node.object) &&
|
|
62
|
+
t.isIdentifier(path.node.object.object) &&
|
|
63
|
+
path.node.object.object.name === 'process' &&
|
|
64
|
+
t.isIdentifier(path.node.object.property) &&
|
|
65
|
+
path.node.object.property.name === 'env') {
|
|
66
|
+
if (t.isIdentifier(path.node.property)) {
|
|
67
|
+
usages.push({
|
|
68
|
+
name: path.node.property.name,
|
|
69
|
+
line: path.node.loc?.start.line || 0,
|
|
70
|
+
file: filePath
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
else if (t.isStringLiteral(path.node.property)) {
|
|
74
|
+
usages.push({
|
|
75
|
+
name: path.node.property.value,
|
|
76
|
+
line: path.node.loc?.start.line || 0,
|
|
77
|
+
file: filePath
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// 2. Check for import.meta.env.VAR (Vite)
|
|
82
|
+
// AST structure: MemberExpression
|
|
83
|
+
// object: MemberExpression
|
|
84
|
+
// object: MetaProperty (import.meta)
|
|
85
|
+
// property: Identifier (env)
|
|
86
|
+
// property: Identifier (VAR)
|
|
87
|
+
if (t.isMemberExpression(path.node.object) &&
|
|
88
|
+
t.isMetaProperty(path.node.object.object) &&
|
|
89
|
+
path.node.object.object.meta.name === 'import' &&
|
|
90
|
+
path.node.object.object.property.name === 'meta' &&
|
|
91
|
+
t.isIdentifier(path.node.object.property) &&
|
|
92
|
+
path.node.object.property.name === 'env') {
|
|
93
|
+
if (t.isIdentifier(path.node.property)) {
|
|
94
|
+
usages.push({
|
|
95
|
+
name: path.node.property.name,
|
|
96
|
+
line: path.node.loc?.start.line || 0,
|
|
97
|
+
file: filePath
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
// console.warn(`Failed to parse ${filePath}:`, e);
|
|
106
|
+
}
|
|
107
|
+
return usages;
|
|
108
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
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.loadConfig = loadConfig;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
async function loadConfig(cwd = process.cwd()) {
|
|
10
|
+
const configPath = path_1.default.join(cwd, 'ship18ion.config.json');
|
|
11
|
+
if (fs_1.default.existsSync(configPath)) {
|
|
12
|
+
const content = fs_1.default.readFileSync(configPath, 'utf-8');
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(content);
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
console.error('Failed to parse config file:', e);
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runChecks = runChecks;
|
|
4
|
+
const scanner_1 = require("./scanner");
|
|
5
|
+
const env_1 = require("../rules/env");
|
|
6
|
+
const secrets_1 = require("../rules/secrets");
|
|
7
|
+
const security_1 = require("../rules/security");
|
|
8
|
+
const build_1 = require("../rules/build");
|
|
9
|
+
const nextjs_1 = require("../rules/frameworks/nextjs");
|
|
10
|
+
const git_1 = require("../rules/git");
|
|
11
|
+
async function runChecks(config, cwd) {
|
|
12
|
+
const files = await (0, scanner_1.scanFiles)(cwd, config.ignore);
|
|
13
|
+
const ctx = { config, files, cwd };
|
|
14
|
+
const results = [];
|
|
15
|
+
// Run all checks
|
|
16
|
+
results.push(...await (0, env_1.checkEnvVars)(ctx));
|
|
17
|
+
results.push(...await (0, secrets_1.checkSecrets)(ctx));
|
|
18
|
+
results.push(...await (0, security_1.checkSecurity)(ctx));
|
|
19
|
+
results.push(...await (0, build_1.checkDependencies)(ctx));
|
|
20
|
+
results.push(...await (0, build_1.checkBuild)(ctx));
|
|
21
|
+
results.push(...await (0, nextjs_1.checkNextJs)(ctx));
|
|
22
|
+
results.push(...await (0, git_1.checkGit)(ctx));
|
|
23
|
+
return results;
|
|
24
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.scanFiles = scanFiles;
|
|
4
|
+
const glob_1 = require("glob");
|
|
5
|
+
async function scanFiles(cwd, ignore = []) {
|
|
6
|
+
const defaultIgnore = ['**/node_modules/**', '**/dist/**', '**/.git/**'];
|
|
7
|
+
// Scan for relevant files: JS/TS code, Configs (JSON/YAML), Env files
|
|
8
|
+
return (0, glob_1.glob)('**/*.{js,ts,jsx,tsx,json,yaml,yml,env,env.*}', {
|
|
9
|
+
cwd,
|
|
10
|
+
ignore: [...defaultIgnore, ...ignore],
|
|
11
|
+
absolute: true,
|
|
12
|
+
dot: true, // Include .env files
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SECRET_PATTERNS = void 0;
|
|
4
|
+
exports.calculateEntropy = calculateEntropy;
|
|
5
|
+
exports.isHighEntropy = isHighEntropy;
|
|
6
|
+
exports.SECRET_PATTERNS = [
|
|
7
|
+
{ name: 'AWS Access Key', regex: /AKIA[0-9A-Z]{16}/ },
|
|
8
|
+
{ name: 'Google API Key', regex: /AIza[0-9A-Za-z\\-_]{35}/ },
|
|
9
|
+
{ name: 'Stripe Secret Key', regex: /sk_live_[0-9a-zA-Z]{24}/ },
|
|
10
|
+
{ name: 'GitHub Personal Access Token', regex: /ghp_[0-9a-zA-Z]{36}/ },
|
|
11
|
+
{ name: 'Generic Private Key', regex: /-----BEGIN .* PRIVATE KEY-----/ },
|
|
12
|
+
{ name: 'Slack Bot Token', regex: /xoxb-[0-9]{11}-[0-9]{12}-[0-9a-zA-Z]{24}/ },
|
|
13
|
+
{ name: 'OpenAI API Key', regex: /sk-[a-zA-Z0-9]{48}/ }
|
|
14
|
+
];
|
|
15
|
+
function calculateEntropy(str) {
|
|
16
|
+
const len = str.length;
|
|
17
|
+
const frequencies = Array.from(str).reduce((freq, char) => {
|
|
18
|
+
freq[char] = (freq[char] || 0) + 1;
|
|
19
|
+
return freq;
|
|
20
|
+
}, {});
|
|
21
|
+
return Object.values(frequencies).reduce((sum, f) => {
|
|
22
|
+
const p = f / len;
|
|
23
|
+
return sum - (p * Math.log2(p));
|
|
24
|
+
}, 0);
|
|
25
|
+
}
|
|
26
|
+
function isHighEntropy(str, threshold = 4.5) {
|
|
27
|
+
return calculateEntropy(str) > threshold;
|
|
28
|
+
}
|
|
@@ -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.reportConsole = reportConsole;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const CATEGORIES = {
|
|
10
|
+
'env': { icon: 'π±', label: 'Environment' },
|
|
11
|
+
'secret': { icon: 'π', label: 'Secrets' },
|
|
12
|
+
'security': { icon: 'β οΈ', label: 'Security' },
|
|
13
|
+
'dep': { icon: 'π¦', label: 'Dependency & Build' },
|
|
14
|
+
'build': { icon: 'π¦', label: 'Dependency & Build' },
|
|
15
|
+
};
|
|
16
|
+
function getCategory(ruleId) {
|
|
17
|
+
const prefix = ruleId.split('-')[0];
|
|
18
|
+
return CATEGORIES[prefix] || { icon: 'β', label: 'Other' };
|
|
19
|
+
}
|
|
20
|
+
function reportConsole(results, cwd) {
|
|
21
|
+
if (results.length === 0) {
|
|
22
|
+
console.log(chalk_1.default.green('\nβ
Production Readiness Check Passed!\n'));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const fails = results.filter(r => r.status === 'fail');
|
|
26
|
+
const warns = results.filter(r => r.status === 'warn');
|
|
27
|
+
if (fails.length > 0) {
|
|
28
|
+
console.log(chalk_1.default.red('\nβ Production Readiness Check Failed\n'));
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
console.log(chalk_1.default.yellow('\nβ οΈ Production Readiness Check Passed with Warnings\n'));
|
|
32
|
+
}
|
|
33
|
+
// Group by category
|
|
34
|
+
const grouped = {};
|
|
35
|
+
results.forEach(r => {
|
|
36
|
+
const cat = getCategory(r.ruleId);
|
|
37
|
+
const key = `${cat.icon} ${cat.label}`;
|
|
38
|
+
if (!grouped[key])
|
|
39
|
+
grouped[key] = [];
|
|
40
|
+
grouped[key].push(r);
|
|
41
|
+
});
|
|
42
|
+
for (const [category, items] of Object.entries(grouped)) {
|
|
43
|
+
console.log(chalk_1.default.bold(category));
|
|
44
|
+
for (const item of items) {
|
|
45
|
+
const sym = item.status === 'fail' ? chalk_1.default.red('β') : chalk_1.default.yellow('!');
|
|
46
|
+
const location = item.file ? `${path_1.default.relative(cwd, item.file)}${item.line ? `:${item.line}` : ''}` : '';
|
|
47
|
+
console.log(` ${sym} ${item.message} ${chalk_1.default.gray(location)}`);
|
|
48
|
+
}
|
|
49
|
+
console.log('');
|
|
50
|
+
}
|
|
51
|
+
const summary = [];
|
|
52
|
+
if (fails.length > 0)
|
|
53
|
+
summary.push(chalk_1.default.red(`${fails.length} errors`));
|
|
54
|
+
if (warns.length > 0)
|
|
55
|
+
summary.push(chalk_1.default.yellow(`${warns.length} warnings`));
|
|
56
|
+
console.log(`Summary: ${summary.join(', ')}`);
|
|
57
|
+
console.log('');
|
|
58
|
+
if (fails.length > 0) {
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
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.checkDependencies = checkDependencies;
|
|
7
|
+
exports.checkBuild = checkBuild;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
async function checkDependencies(ctx) {
|
|
10
|
+
const results = [];
|
|
11
|
+
const packageJsons = ctx.files.filter(f => f.endsWith('package.json') && !f.includes('node_modules'));
|
|
12
|
+
const devToolsInProd = ['eslint', 'jest', 'mocha', 'nodemon', 'ts-node', 'typescript', 'webpack', 'babel-loader'];
|
|
13
|
+
for (const pkgFile of packageJsons) {
|
|
14
|
+
try {
|
|
15
|
+
const content = JSON.parse(fs_1.default.readFileSync(pkgFile, 'utf-8'));
|
|
16
|
+
const deps = content.dependencies || {};
|
|
17
|
+
for (const tool of devToolsInProd) {
|
|
18
|
+
if (deps[tool]) {
|
|
19
|
+
results.push({
|
|
20
|
+
status: 'warn',
|
|
21
|
+
message: `Dev dependency found in 'dependencies': ${tool}. Should be in 'devDependencies'?`,
|
|
22
|
+
ruleId: 'dep-dev-in-prod',
|
|
23
|
+
file: pkgFile
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
// ignore
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return results;
|
|
33
|
+
}
|
|
34
|
+
async function checkBuild(ctx) {
|
|
35
|
+
const results = [];
|
|
36
|
+
// Check for source maps in potential build dirs (dist, build, out, .next)
|
|
37
|
+
// Scanner ignores dist by default, but if we want to check build artifacts we might need to scan explicitly OR assumes user runs this in root.
|
|
38
|
+
// If the scanner ignores 'dist', we won't see them.
|
|
39
|
+
// So this check is effective only if scanner INCLUDES build dirs or we explicitly look for them.
|
|
40
|
+
// Let's explicitly check common build folders in CWD if they exist, ignoring scanner's ignore list for this specific check?
|
|
41
|
+
// Or just warn if we find .map files in the file list (meaning they were NOT ignored/cleaned).
|
|
42
|
+
const mapFiles = ctx.files.filter(f => f.endsWith('.map'));
|
|
43
|
+
for (const file of mapFiles) {
|
|
44
|
+
// Only if it looks like a build artifact
|
|
45
|
+
if (file.includes('/dist/') || file.includes('/build/') || file.includes('/.next/')) {
|
|
46
|
+
results.push({
|
|
47
|
+
status: 'warn',
|
|
48
|
+
message: 'Source map found in build output (information leak)',
|
|
49
|
+
ruleId: 'build-source-map',
|
|
50
|
+
file
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
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
|
+
return results;
|
|
66
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
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.checkEnvVars = checkEnvVars;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
9
|
+
const ast_1 = require("../engine/ast");
|
|
10
|
+
async function checkEnvVars(ctx) {
|
|
11
|
+
const results = [];
|
|
12
|
+
const declaredEnvs = new Set();
|
|
13
|
+
const usedEnvs = new Map();
|
|
14
|
+
// 1. Find and parse .env files (definition detection)
|
|
15
|
+
const envFiles = ctx.files.filter(f => f.match(/\.env(\..+)?$/));
|
|
16
|
+
for (const file of envFiles) {
|
|
17
|
+
const content = fs_1.default.readFileSync(file, 'utf-8');
|
|
18
|
+
try {
|
|
19
|
+
const parsed = dotenv_1.default.parse(content);
|
|
20
|
+
Object.keys(parsed).forEach(k => declaredEnvs.add(k));
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
results.push({
|
|
24
|
+
status: 'warn',
|
|
25
|
+
message: `Failed to parse env file: ${file}`,
|
|
26
|
+
ruleId: 'env-parse-error',
|
|
27
|
+
file: file
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// 2. Scan for usages
|
|
32
|
+
const codeFiles = ctx.files.filter(f => f.match(/\.(js|ts|jsx|tsx)$/));
|
|
33
|
+
for (const file of codeFiles) {
|
|
34
|
+
const usages = (0, ast_1.findEnvUsages)(file);
|
|
35
|
+
for (const u of usages) {
|
|
36
|
+
if (!usedEnvs.has(u.name)) {
|
|
37
|
+
usedEnvs.set(u.name, []);
|
|
38
|
+
}
|
|
39
|
+
usedEnvs.get(u.name)?.push({ file, line: u.line });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// 3. Rule: Unused env vars
|
|
43
|
+
// Fail if Env vars exist but never used
|
|
44
|
+
for (const env of declaredEnvs) {
|
|
45
|
+
if (!usedEnvs.has(env)) {
|
|
46
|
+
// Ignore some common framework vars if needed, but strict mode says unused is bad.
|
|
47
|
+
results.push({
|
|
48
|
+
status: 'warn', // Warn for now, maybe fail? User said "Fail if Env vars exist but never used"
|
|
49
|
+
message: `Unused environment variable: ${env}`,
|
|
50
|
+
ruleId: 'env-unused',
|
|
51
|
+
file: envFiles[0] // Just point to first env file for now
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// 4. Rule: Missing required env vars
|
|
56
|
+
// "App references process.env.X But itβs not defined anywhere"
|
|
57
|
+
// Also check strict list from config
|
|
58
|
+
const required = ctx.config.env?.required || [];
|
|
59
|
+
// Check missing from strict config
|
|
60
|
+
for (const req of required) {
|
|
61
|
+
if (!declaredEnvs.has(req)) {
|
|
62
|
+
results.push({
|
|
63
|
+
status: 'fail',
|
|
64
|
+
message: `Missing required environment variable (configured): ${req}`,
|
|
65
|
+
ruleId: 'env-missing-config',
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Check usage without definition
|
|
70
|
+
const commonSystemVars = ['NODE_ENV', 'PORT', 'CI'];
|
|
71
|
+
for (const [env, locs] of usedEnvs) {
|
|
72
|
+
if (!declaredEnvs.has(env) && !commonSystemVars.includes(env)) {
|
|
73
|
+
// Check if it is in disallowed list?
|
|
74
|
+
if (ctx.config.env?.disallowed?.includes(env)) {
|
|
75
|
+
results.push({
|
|
76
|
+
status: 'fail',
|
|
77
|
+
message: `Disallowed environment variable used: ${env}`,
|
|
78
|
+
ruleId: 'env-disallowed',
|
|
79
|
+
file: locs[0].file,
|
|
80
|
+
line: locs[0].line
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
// It's used but not in .env.
|
|
85
|
+
// We should probably warn unless we are in strict mode.
|
|
86
|
+
// User said: "Fail if Required env var is missing" -> checking usage implies requirement.
|
|
87
|
+
results.push({
|
|
88
|
+
status: 'warn',
|
|
89
|
+
message: `Environment variable used but not defined in .env: ${env}`,
|
|
90
|
+
ruleId: 'env-missing-definition',
|
|
91
|
+
file: locs[0].file,
|
|
92
|
+
line: locs[0].line
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return results;
|
|
98
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkNextJs = checkNextJs;
|
|
4
|
+
const ast_1 = require("../../engine/ast");
|
|
5
|
+
async function checkNextJs(ctx) {
|
|
6
|
+
const results = [];
|
|
7
|
+
// 1. Check for NEXT_PUBLIC_ secrets
|
|
8
|
+
const codeFiles = ctx.files.filter(f => f.match(/\.(js|ts|jsx|tsx)$/));
|
|
9
|
+
for (const file of codeFiles) {
|
|
10
|
+
const usages = (0, ast_1.findEnvUsages)(file);
|
|
11
|
+
for (const usage of usages) {
|
|
12
|
+
if (usage.name.startsWith('NEXT_PUBLIC_')) {
|
|
13
|
+
// Heuristic: Does it look like a secret?
|
|
14
|
+
// e.g. NEXT_PUBLIC_SECRET_KEY, NEXT_PUBLIC_API_SECRET
|
|
15
|
+
if (usage.name.match(/SECRET|PASSWORD|TOKEN|KEY|AUTH/i)) {
|
|
16
|
+
// Exception: PUBLIC_KEY is often safe
|
|
17
|
+
if (!usage.name.match(/PUBLIC_KEY/i)) {
|
|
18
|
+
results.push({
|
|
19
|
+
status: 'warn',
|
|
20
|
+
message: `Potential secret exposed via NEXT_PUBLIC_ variable: ${usage.name}`,
|
|
21
|
+
ruleId: 'nextjs-public-secret',
|
|
22
|
+
file: file,
|
|
23
|
+
line: usage.line
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return results;
|
|
31
|
+
}
|