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
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { RuleContext, RuleResult } from '../../engine/types';
|
|
2
|
+
import { findEnvUsages } from '../../engine/ast';
|
|
3
|
+
|
|
4
|
+
export async function checkNextJs(ctx: RuleContext): Promise<RuleResult[]> {
|
|
5
|
+
const results: RuleResult[] = [];
|
|
6
|
+
|
|
7
|
+
// 1. Check for NEXT_PUBLIC_ secrets
|
|
8
|
+
const codeFiles = ctx.files.filter(f => f.match(/\.(js|ts|jsx|tsx)$/));
|
|
9
|
+
|
|
10
|
+
for (const file of codeFiles) {
|
|
11
|
+
const usages = findEnvUsages(file);
|
|
12
|
+
for (const usage of usages) {
|
|
13
|
+
if (usage.name.startsWith('NEXT_PUBLIC_')) {
|
|
14
|
+
// Heuristic: Does it look like a secret?
|
|
15
|
+
// e.g. NEXT_PUBLIC_SECRET_KEY, NEXT_PUBLIC_API_SECRET
|
|
16
|
+
if (usage.name.match(/SECRET|PASSWORD|TOKEN|KEY|AUTH/i)) {
|
|
17
|
+
// Exception: PUBLIC_KEY is often safe
|
|
18
|
+
if (!usage.name.match(/PUBLIC_KEY/i)) {
|
|
19
|
+
results.push({
|
|
20
|
+
status: 'warn',
|
|
21
|
+
message: `Potential secret exposed via NEXT_PUBLIC_ variable: ${usage.name}`,
|
|
22
|
+
ruleId: 'nextjs-public-secret',
|
|
23
|
+
file: file,
|
|
24
|
+
line: usage.line
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return results;
|
|
33
|
+
}
|
package/src/rules/git.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { RuleContext, RuleResult } from '../engine/types';
|
|
3
|
+
|
|
4
|
+
export async function checkGit(ctx: RuleContext): Promise<RuleResult[]> {
|
|
5
|
+
const results: RuleResult[] = [];
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
// Check for uncommitted changes
|
|
9
|
+
const status = execSync('git status --porcelain', { cwd: ctx.cwd, encoding: 'utf-8' });
|
|
10
|
+
if (status.trim().length > 0) {
|
|
11
|
+
results.push({
|
|
12
|
+
status: 'warn',
|
|
13
|
+
message: 'Git working directory is dirty (uncommitted changes). Verify before shipping.',
|
|
14
|
+
ruleId: 'git-dirty',
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Check current branch
|
|
19
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: ctx.cwd, encoding: 'utf-8' }).trim();
|
|
20
|
+
const allowedBranches = ['main', 'master', 'staging', 'production', 'prod'];
|
|
21
|
+
if (!allowedBranches.includes(branch)) {
|
|
22
|
+
results.push({
|
|
23
|
+
status: 'warn',
|
|
24
|
+
message: `You are on branch '${branch}'. Production builds typically come from main/master.`,
|
|
25
|
+
ruleId: 'git-branch',
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
} catch (e) {
|
|
30
|
+
// 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
|
+
}
|
|
34
|
+
|
|
35
|
+
return results;
|
|
36
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { RuleContext, RuleResult } from '../engine/types';
|
|
3
|
+
import { SECRET_PATTERNS } from '../engine/secrets';
|
|
4
|
+
|
|
5
|
+
export async function checkSecrets(ctx: RuleContext): Promise<RuleResult[]> {
|
|
6
|
+
const results: RuleResult[] = [];
|
|
7
|
+
|
|
8
|
+
// Skip binary files, lock files, node_modules (already ignored by scanner but specific check here)
|
|
9
|
+
const filesToCheck = ctx.files.filter(f => !f.match(/\.(png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|lock|pdf)$/));
|
|
10
|
+
|
|
11
|
+
for (const file of filesToCheck) {
|
|
12
|
+
try {
|
|
13
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
14
|
+
const lines = content.split('\n');
|
|
15
|
+
|
|
16
|
+
lines.forEach((line, index) => {
|
|
17
|
+
// Check Regex Patterns
|
|
18
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
19
|
+
if (pattern.regex.test(line)) {
|
|
20
|
+
results.push({
|
|
21
|
+
status: 'fail',
|
|
22
|
+
message: `Potential secret found: ${pattern.name}`,
|
|
23
|
+
ruleId: 'secret-pattern',
|
|
24
|
+
file,
|
|
25
|
+
line: index + 1
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Check Heuristics for assignments
|
|
31
|
+
// matches "key = '...'"
|
|
32
|
+
const genericMsg = line.match(/(api_?key|secret|token|password|auth)[\s]*[:=][\s]*['"]([a-zA-Z0-9_\-]{8,})['"]/i);
|
|
33
|
+
if (genericMsg) {
|
|
34
|
+
const match = genericMsg[2];
|
|
35
|
+
// Heuristic: Must be > 8 chars and not contain 'process.env' or template placeholders
|
|
36
|
+
if (match && match.length > 8 && !line.includes('process.env') && !match.includes('${')) {
|
|
37
|
+
results.push({
|
|
38
|
+
status: 'warn',
|
|
39
|
+
message: `Possible hardcoded secret (heuristic): ${genericMsg[1]}`,
|
|
40
|
+
ruleId: 'secret-heuristic',
|
|
41
|
+
file,
|
|
42
|
+
line: index + 1
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
} catch (e) {
|
|
48
|
+
// Ignore read errors
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return results;
|
|
53
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { RuleContext, RuleResult } from '../engine/types';
|
|
3
|
+
|
|
4
|
+
export async function checkSecurity(ctx: RuleContext): Promise<RuleResult[]> {
|
|
5
|
+
const results: RuleResult[] = [];
|
|
6
|
+
|
|
7
|
+
const codeFiles = ctx.files.filter(f => f.match(/\.(js|ts|jsx|tsx|json)$/));
|
|
8
|
+
|
|
9
|
+
for (const file of codeFiles) {
|
|
10
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
11
|
+
|
|
12
|
+
// 1. Check for Hardcoded NODE_ENV not being production?
|
|
13
|
+
// Actually we want to verify we are NOT hardcoding 'development' in prod context?
|
|
14
|
+
// Or "Debug / dev configs enabled" -> debug: true
|
|
15
|
+
|
|
16
|
+
if (content.match(/debug:\s*true/)) {
|
|
17
|
+
results.push({
|
|
18
|
+
status: 'warn',
|
|
19
|
+
message: 'Debug mode enabled (debug: true) found',
|
|
20
|
+
ruleId: 'security-debug-enabled',
|
|
21
|
+
file
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 2. CORS Wildcard
|
|
26
|
+
// "origin: '*'" or "origin: *"
|
|
27
|
+
if (content.match(/origin:\s*['"]?\*['"]?/)) {
|
|
28
|
+
// Default: Enabled (fail on wildcard) unless explicitly set to false
|
|
29
|
+
if (ctx.config.security?.noCorsWildcard !== false) {
|
|
30
|
+
results.push({
|
|
31
|
+
status: 'fail',
|
|
32
|
+
message: 'CORS wildcard origin (*) detected',
|
|
33
|
+
ruleId: 'security-cors-wildcard',
|
|
34
|
+
file
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 3. Hardcoded credentials (simple db keywords)
|
|
40
|
+
// postgres://user:pass@...
|
|
41
|
+
if (content.match(/:\/\/[a-zA-Z0-9]+:[a-zA-Z0-9]+@/)) {
|
|
42
|
+
// Exclude localhost
|
|
43
|
+
if (!content.includes('localhost') && !content.includes('127.0.0.1')) {
|
|
44
|
+
results.push({
|
|
45
|
+
status: 'fail',
|
|
46
|
+
message: 'Hardcoded database credentials in connection string',
|
|
47
|
+
ruleId: 'security-db-creds',
|
|
48
|
+
file
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return results;
|
|
55
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const app = express();
|
|
3
|
+
|
|
4
|
+
// Hardcoded secret (fake AWS key)
|
|
5
|
+
const awsKey = "AKIA1234567890123456";
|
|
6
|
+
|
|
7
|
+
// Missing env var usage (API_KEY is not in .env)
|
|
8
|
+
const apiKey = process.env.API_KEY;
|
|
9
|
+
|
|
10
|
+
// Debug mode enabled
|
|
11
|
+
const config = {
|
|
12
|
+
debug: true
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// CORS wildcard
|
|
16
|
+
app.use(cors({ origin: '*' }));
|
|
17
|
+
|
|
18
|
+
// Hardcoded DB credentials
|
|
19
|
+
const db = "postgres://user:password@production-db.com/db";
|
|
20
|
+
|
|
21
|
+
app.listen(process.env.PORT || 3000);
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"outDir": "./dist",
|
|
6
|
+
"rootDir": "./src",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"resolveJsonModule": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"],
|
|
14
|
+
"exclude": ["node_modules", "**/*.test.ts"]
|
|
15
|
+
}
|
package/walkthrough.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
ship18ion - Production Readiness Inspector
|
|
2
|
+
I have successfully built ship18ion, a CLI tool to check for production readiness.
|
|
3
|
+
|
|
4
|
+
Features Implemented
|
|
5
|
+
1. Environment Variable Safety
|
|
6
|
+
Unused Variable Detection: Scans
|
|
7
|
+
|
|
8
|
+
.env
|
|
9
|
+
files and code to find variables defined but never used.
|
|
10
|
+
Missing Variable Detection: Identifies process.env.VAR usages that lack a corresponding definition in
|
|
11
|
+
|
|
12
|
+
.env
|
|
13
|
+
(or config).
|
|
14
|
+
Format Support: Supports
|
|
15
|
+
|
|
16
|
+
.env
|
|
17
|
+
, .env.production files.
|
|
18
|
+
Robust AST Parsing: Correctly detects process.env.VAR, import.meta.env.VAR (Vite), and process.env["VAR"].
|
|
19
|
+
2. Secrets Detection
|
|
20
|
+
Pattern Matching: Detects AWS Keys, Google API Keys, Stripe Keys, and generic private keys.
|
|
21
|
+
Entropy Heuristics: Detects potential high-entropy strings assigned to "secret" or "key" variables.
|
|
22
|
+
3. Framework & Security Checks
|
|
23
|
+
Next.js Safety: Scans for NEXT_PUBLIC_ variables that appear to contain secrets (e.g. NEXT_PUBLIC_SECRET_KEY).
|
|
24
|
+
Git Safety: Warns if deploying from a dirty working directory or a non-production branch.
|
|
25
|
+
Debug Mode: Alerts on debug: true.
|
|
26
|
+
CORS Wildcards: Fails if origin: '*' is detected.
|
|
27
|
+
Database Credentials: Detects hardcoded connection strings.
|
|
28
|
+
4. Dependency & Build Safety
|
|
29
|
+
Dev Dependencies: Warns if eslint or other dev tools are in dependencies.
|
|
30
|
+
Build Artifacts: Alerts if source maps (.map) or
|
|
31
|
+
|
|
32
|
+
.env
|
|
33
|
+
files are found in build directories.
|
|
34
|
+
Usage
|
|
35
|
+
# In your project root
|
|
36
|
+
npx ship18ion check
|
|
37
|
+
# CI Mode (minimal output)
|
|
38
|
+
npx ship18ion check --ci
|
|
39
|
+
How to Ship & Share
|
|
40
|
+
See
|
|
41
|
+
|
|
42
|
+
SHIPPING.md
|
|
43
|
+
for detailed instructions on:
|
|
44
|
+
|
|
45
|
+
Local Testing: Using npm link to test on your other projects instantly.
|
|
46
|
+
Publishing: Pushing to NPM so anyone can use npx ship18ion.
|
|
47
|
+
Architecture
|
|
48
|
+
CLI: Built with commander.
|
|
49
|
+
Engine: TypeScript-based rule engine.
|
|
50
|
+
Parsing: Babel-based AST parsing.
|
|
51
|
+
Config: ship18ion.config.json support.
|