safelaunch 1.0.18 → 1.0.20
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/package.json +1 -1
- package/safelaunch/src/scan.js +225 -210
package/package.json
CHANGED
package/safelaunch/src/scan.js
CHANGED
|
@@ -1,62 +1,86 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const { track, shutdown } = require('./telemetry');
|
|
3
5
|
|
|
4
|
-
|
|
6
|
+
function impactBlock(lines) {
|
|
7
|
+
return ' Impact:\n' + lines.map(l => ' ' + l).join('\n');
|
|
8
|
+
}
|
|
5
9
|
|
|
6
10
|
const IMPACT = {
|
|
7
11
|
missing: (key) => impactBlock([
|
|
8
12
|
'Your app cannot use ' + key,
|
|
9
|
-
'
|
|
13
|
+
'\u2192 Will crash or behave incorrectly at runtime'
|
|
10
14
|
]),
|
|
11
15
|
empty: (key) => impactBlock([
|
|
12
16
|
key + ' is defined but has no value',
|
|
13
|
-
'
|
|
17
|
+
'\u2192 Your app will treat it as blank \u2014 this will break things'
|
|
14
18
|
]),
|
|
15
19
|
duplicate: (key) => impactBlock([
|
|
16
20
|
key + ' is defined more than once',
|
|
17
|
-
'
|
|
21
|
+
'\u2192 The last value silently wins \u2014 likely not what you want'
|
|
18
22
|
]),
|
|
19
23
|
depsNotInstalled: () => impactBlock([
|
|
20
24
|
'node_modules is missing entirely',
|
|
21
|
-
'
|
|
25
|
+
'\u2192 Your app will not start'
|
|
22
26
|
]),
|
|
23
27
|
depsDrift: (dep) => impactBlock([
|
|
24
28
|
dep + ' is in package.json but not installed',
|
|
25
|
-
'
|
|
29
|
+
'\u2192 Any code that imports it will fail'
|
|
30
|
+
]),
|
|
31
|
+
lockfileMissing: () => impactBlock([
|
|
32
|
+
'package-lock.json is missing',
|
|
33
|
+
'\u2192 npm install will resolve different versions each time \u2014 inconsistent deploys'
|
|
26
34
|
]),
|
|
27
35
|
prefixVite: (key) => impactBlock([
|
|
28
36
|
key + ' is missing the VITE_ prefix',
|
|
29
|
-
'
|
|
37
|
+
'\u2192 It will not be exposed to the browser \u2014 your frontend will not see it'
|
|
30
38
|
]),
|
|
31
39
|
prefixCra: (key) => impactBlock([
|
|
32
40
|
key + ' is missing the REACT_APP_ prefix',
|
|
33
|
-
'
|
|
41
|
+
'\u2192 It will not be exposed to the browser \u2014 your frontend will not see it'
|
|
34
42
|
]),
|
|
35
43
|
noEnvFile: () => impactBlock([
|
|
36
44
|
'No .env file found',
|
|
37
|
-
'
|
|
45
|
+
'\u2192 Every environment variable your app needs will be missing'
|
|
46
|
+
]),
|
|
47
|
+
envInGit: (file) => impactBlock([
|
|
48
|
+
file + ' is committed to your git history',
|
|
49
|
+
'\u2192 Your secrets are exposed to anyone with repo access'
|
|
50
|
+
]),
|
|
51
|
+
envNotIgnored: () => impactBlock([
|
|
52
|
+
'.env is not in your .gitignore',
|
|
53
|
+
'\u2192 You are one git add away from leaking your secrets'
|
|
54
|
+
]),
|
|
55
|
+
envExampleOutOfSync: (keys) => impactBlock([
|
|
56
|
+
keys.join(', ') + ' are in .env but missing from .env.example',
|
|
57
|
+
'\u2192 Teammates cloning this repo will not know these vars exist'
|
|
58
|
+
]),
|
|
59
|
+
hardcodedSecret: (file) => impactBlock([
|
|
60
|
+
'Possible secret found hardcoded in ' + file,
|
|
61
|
+
'\u2192 Secrets in source code get committed and exposed in version history'
|
|
62
|
+
]),
|
|
63
|
+
buildScriptMissing: () => impactBlock([
|
|
64
|
+
'No build script found in package.json',
|
|
65
|
+
'\u2192 Your deploy pipeline will not know how to build this project'
|
|
66
|
+
]),
|
|
67
|
+
uncommittedChanges: () => impactBlock([
|
|
68
|
+
'You have uncommitted changes',
|
|
69
|
+
'\u2192 These changes will not be included in your deploy'
|
|
70
|
+
]),
|
|
71
|
+
unpushedCommits: () => impactBlock([
|
|
72
|
+
'You have commits that have not been pushed',
|
|
73
|
+
'\u2192 Your deploy may not include your latest changes'
|
|
38
74
|
]),
|
|
39
75
|
};
|
|
40
76
|
|
|
41
|
-
|
|
42
|
-
return ' Impact:\n' + lines.map(l => ' ' + l).join('\n');
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// ─── Divider ──────────────────────────────────────────────────────────────────
|
|
46
|
-
|
|
47
|
-
const DIVIDER = '────────────────────────────';
|
|
48
|
-
|
|
49
|
-
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
77
|
+
const DIVIDER = '\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500';
|
|
50
78
|
|
|
51
79
|
function detectProjectType(cwd) {
|
|
52
80
|
if (fs.existsSync(path.join(cwd, 'vite.config.js')) ||
|
|
53
|
-
fs.existsSync(path.join(cwd, 'vite.config.ts')))
|
|
54
|
-
return 'vite';
|
|
55
|
-
}
|
|
81
|
+
fs.existsSync(path.join(cwd, 'vite.config.ts'))) return 'vite';
|
|
56
82
|
if (fs.existsSync(path.join(cwd, 'next.config.js')) ||
|
|
57
|
-
fs.existsSync(path.join(cwd, 'next.config.ts')))
|
|
58
|
-
return 'next';
|
|
59
|
-
}
|
|
83
|
+
fs.existsSync(path.join(cwd, 'next.config.ts'))) return 'next';
|
|
60
84
|
const packagePath = path.join(cwd, 'package.json');
|
|
61
85
|
if (fs.existsSync(packagePath)) {
|
|
62
86
|
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
@@ -77,9 +101,7 @@ function scanFiles(dir, extensions, pattern, found = new Set()) {
|
|
|
77
101
|
} else if (extensions.some(ext => item.endsWith(ext))) {
|
|
78
102
|
const content = fs.readFileSync(full, 'utf8');
|
|
79
103
|
const matches = content.matchAll(pattern);
|
|
80
|
-
for (const match of matches)
|
|
81
|
-
found.add(match[1]);
|
|
82
|
-
}
|
|
104
|
+
for (const match of matches) found.add(match[1]);
|
|
83
105
|
}
|
|
84
106
|
}
|
|
85
107
|
return found;
|
|
@@ -119,28 +141,100 @@ function checkDependencies(cwd) {
|
|
|
119
141
|
return { notInstalled: false, missing };
|
|
120
142
|
}
|
|
121
143
|
|
|
122
|
-
|
|
144
|
+
function checkLockfile(cwd) {
|
|
145
|
+
return !fs.existsSync(path.join(cwd, 'package-lock.json')) &&
|
|
146
|
+
!fs.existsSync(path.join(cwd, 'yarn.lock')) &&
|
|
147
|
+
!fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'));
|
|
148
|
+
}
|
|
123
149
|
|
|
124
|
-
function
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
'\n'
|
|
128
|
-
|
|
129
|
-
);
|
|
150
|
+
function checkEnvInGit(cwd) {
|
|
151
|
+
try {
|
|
152
|
+
const tracked = execSync('git ls-files', { cwd, stdio: ['pipe', 'pipe', 'ignore'] }).toString();
|
|
153
|
+
const files = tracked.split('\n');
|
|
154
|
+
return files.filter(f => f === '.env' || /^\.env\.(production|staging|live)$/.test(f));
|
|
155
|
+
} catch (_) { return []; }
|
|
130
156
|
}
|
|
131
157
|
|
|
132
|
-
|
|
158
|
+
function checkEnvInGitignore(cwd) {
|
|
159
|
+
const gitignorePath = path.join(cwd, '.gitignore');
|
|
160
|
+
if (!fs.existsSync(gitignorePath)) return false;
|
|
161
|
+
const content = fs.readFileSync(gitignorePath, 'utf8');
|
|
162
|
+
return content.split('\n').some(line => line.trim() === '.env');
|
|
163
|
+
}
|
|
133
164
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
165
|
+
function checkEnvExampleSync(cwd, envVars) {
|
|
166
|
+
const examplePath = path.join(cwd, '.env.example');
|
|
167
|
+
if (!fs.existsSync(examplePath)) return [];
|
|
168
|
+
const exampleContent = fs.readFileSync(examplePath, 'utf8');
|
|
169
|
+
const exampleKeys = new Set();
|
|
170
|
+
for (const line of exampleContent.split('\n')) {
|
|
171
|
+
const match = line.match(/^([^=]+)=/);
|
|
172
|
+
if (match) exampleKeys.add(match[1].trim());
|
|
173
|
+
}
|
|
174
|
+
return Object.keys(envVars).filter(k => !exampleKeys.has(k));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function checkHardcodedSecrets(cwd) {
|
|
178
|
+
const extensions = ['.js', '.ts', '.jsx', '.tsx'];
|
|
179
|
+
const secretPatterns = [
|
|
180
|
+
/sk_live_[a-zA-Z0-9]{20,}/,
|
|
181
|
+
/pk_live_[a-zA-Z0-9]{20,}/,
|
|
182
|
+
/AKIA[0-9A-Z]{16}/,
|
|
183
|
+
/-----BEGIN (RSA |EC )?PRIVATE KEY-----/,
|
|
184
|
+
/ghp_[a-zA-Z0-9]{36}/,
|
|
185
|
+
/xox[baprs]-[0-9a-zA-Z-]{10,}/,
|
|
186
|
+
];
|
|
187
|
+
const hits = [];
|
|
188
|
+
function walk(dir) {
|
|
189
|
+
try {
|
|
190
|
+
const items = fs.readdirSync(dir);
|
|
191
|
+
for (const item of items) {
|
|
192
|
+
if (item === 'node_modules' || item === '.git') continue;
|
|
193
|
+
const full = path.join(dir, item);
|
|
194
|
+
const stat = fs.statSync(full);
|
|
195
|
+
if (stat.isDirectory()) {
|
|
196
|
+
walk(full);
|
|
197
|
+
} else if (extensions.some(ext => item.endsWith(ext))) {
|
|
198
|
+
const content = fs.readFileSync(full, 'utf8');
|
|
199
|
+
for (const pattern of secretPatterns) {
|
|
200
|
+
if (pattern.test(content)) {
|
|
201
|
+
hits.push(path.relative(cwd, full));
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
} catch (_) {}
|
|
208
|
+
}
|
|
209
|
+
walk(cwd);
|
|
210
|
+
return hits;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function checkBuildScript(cwd) {
|
|
214
|
+
const packagePath = path.join(cwd, 'package.json');
|
|
215
|
+
if (!fs.existsSync(packagePath)) return false;
|
|
216
|
+
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
217
|
+
return !!(pkg.scripts && pkg.scripts.build);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function checkGitStatus(cwd) {
|
|
138
221
|
try {
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
222
|
+
const status = execSync('git status --porcelain', { cwd, stdio: ['pipe', 'pipe', 'ignore'] }).toString().trim();
|
|
223
|
+
const uncommitted = status.length > 0;
|
|
224
|
+
let unpushed = false;
|
|
225
|
+
try {
|
|
226
|
+
const ahead = execSync('git log @{u}.. --oneline', { cwd, stdio: ['pipe', 'pipe', 'ignore'] }).toString().trim();
|
|
227
|
+
unpushed = ahead.length > 0;
|
|
228
|
+
} catch (_) {}
|
|
229
|
+
return { uncommitted, unpushed };
|
|
230
|
+
} catch (_) { return { uncommitted: false, unpushed: false }; }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function renderIssue(number, title, impact) {
|
|
234
|
+
return number + '. ' + title + '\n\n' + impact + '\n';
|
|
235
|
+
}
|
|
143
236
|
|
|
237
|
+
async function scan() {
|
|
144
238
|
const cwd = process.cwd();
|
|
145
239
|
const projectType = detectProjectType(cwd);
|
|
146
240
|
const typeLabels = { vite: 'Vite', next: 'Next.js', cra: 'Create React App', node: 'Node.js' };
|
|
@@ -150,8 +244,6 @@ async function scan() {
|
|
|
150
244
|
console.log('Detected: ' + typeLabels[projectType]);
|
|
151
245
|
console.log('');
|
|
152
246
|
|
|
153
|
-
// ── Collect env vars used in codebase ──
|
|
154
|
-
|
|
155
247
|
const extensions = ['.js', '.ts', '.jsx', '.tsx', '.vue', '.svelte'];
|
|
156
248
|
let pattern;
|
|
157
249
|
if (projectType === 'vite') {
|
|
@@ -163,190 +255,115 @@ async function scan() {
|
|
|
163
255
|
}
|
|
164
256
|
|
|
165
257
|
const found = scanFiles(cwd, extensions, pattern);
|
|
166
|
-
|
|
167
|
-
// ── No env vars in codebase at all ──
|
|
168
|
-
|
|
169
|
-
if (found.size === 0) {
|
|
170
|
-
console.log('🛡️ Safelaunch Scan Report');
|
|
171
|
-
console.log('');
|
|
172
|
-
console.log('No environment variables found in your codebase.');
|
|
173
|
-
console.log('');
|
|
174
|
-
console.log(DIVIDER);
|
|
175
|
-
console.log('');
|
|
176
|
-
console.log('✅ Nothing to check');
|
|
177
|
-
console.log('');
|
|
178
|
-
console.log(' Your project does not appear to use process.env');
|
|
179
|
-
console.log('');
|
|
180
|
-
await track('safelaunch_scan_run', { project_type: projectType, vars_found: 0 });
|
|
181
|
-
await shutdown();
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// ── No .env file found ──
|
|
186
|
-
|
|
187
258
|
const result = readEnvDetailed(cwd);
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
console.log('This project is NOT safe to deploy');
|
|
207
|
-
console.log('');
|
|
208
|
-
console.log((varList.length + 1) + ' issues found');
|
|
209
|
-
console.log((varList.length + 1) + ' critical');
|
|
210
|
-
console.log('');
|
|
211
|
-
console.log(DIVIDER);
|
|
212
|
-
console.log('');
|
|
213
|
-
console.log('🚨 CRITICAL (will break your app)');
|
|
214
|
-
console.log('');
|
|
215
|
-
for (const block of criticals) {
|
|
216
|
-
console.log(block);
|
|
217
|
-
console.log('---');
|
|
218
|
-
console.log('');
|
|
219
|
-
}
|
|
220
|
-
console.log(DIVIDER);
|
|
221
|
-
console.log('');
|
|
222
|
-
console.log('💡 Next step');
|
|
223
|
-
console.log('');
|
|
224
|
-
console.log(' Create a .env file with your environment variables,');
|
|
225
|
-
console.log(' then run:');
|
|
226
|
-
console.log('');
|
|
227
|
-
console.log(' safelaunch init');
|
|
228
|
-
console.log('');
|
|
229
|
-
console.log(' Lock this configuration and prevent future breakage');
|
|
230
|
-
console.log('');
|
|
231
|
-
await track('safelaunch_scan_run', { project_type: projectType, vars_found: found.size, missing: found.size, no_env: true });
|
|
232
|
-
await shutdown();
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// ── Full scan ──
|
|
237
|
-
|
|
238
|
-
const { envVars, duplicates } = result;
|
|
239
|
-
const missing = [];
|
|
240
|
-
const empty = [];
|
|
241
|
-
const present = [];
|
|
242
|
-
|
|
243
|
-
for (const key of [...found].sort()) {
|
|
244
|
-
if (!(key in envVars)) {
|
|
245
|
-
missing.push(key);
|
|
246
|
-
} else if (envVars[key] === '') {
|
|
247
|
-
empty.push(key);
|
|
248
|
-
} else {
|
|
249
|
-
present.push(key);
|
|
259
|
+
const drift = checkDependencies(cwd);
|
|
260
|
+
const lockfileMissing = checkLockfile(cwd);
|
|
261
|
+
const envInGit = checkEnvInGit(cwd);
|
|
262
|
+
const envIgnored = checkEnvInGitignore(cwd);
|
|
263
|
+
const hardcodedSecrets = checkHardcodedSecrets(cwd);
|
|
264
|
+
const hasBuildScript = checkBuildScript(cwd);
|
|
265
|
+
const gitStatus = checkGitStatus(cwd);
|
|
266
|
+
|
|
267
|
+
let missing = [], empty = [], present = [], duplicates = [], envExampleOutOfSync = [];
|
|
268
|
+
|
|
269
|
+
if (result) {
|
|
270
|
+
const { envVars, duplicates: dups } = result;
|
|
271
|
+
duplicates = dups;
|
|
272
|
+
envExampleOutOfSync = checkEnvExampleSync(cwd, envVars);
|
|
273
|
+
for (const key of [...found].sort()) {
|
|
274
|
+
if (!(key in envVars)) missing.push(key);
|
|
275
|
+
else if (envVars[key] === '') empty.push(key);
|
|
276
|
+
else present.push(key);
|
|
250
277
|
}
|
|
278
|
+
} else {
|
|
279
|
+
missing = [...found].sort();
|
|
251
280
|
}
|
|
252
281
|
|
|
253
|
-
const drift = checkDependencies(cwd);
|
|
254
|
-
|
|
255
|
-
// Prefix issues (warnings only)
|
|
256
282
|
const prefixWarnings = [];
|
|
257
283
|
for (const key of found) {
|
|
258
284
|
if (key === 'NODE_ENV') continue;
|
|
259
|
-
if (projectType === 'vite' && !key.startsWith('VITE_')) {
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
if (projectType === 'cra' && !key.startsWith('REACT_APP_')) {
|
|
263
|
-
prefixWarnings.push({ key, type: 'cra' });
|
|
264
|
-
}
|
|
285
|
+
if (projectType === 'vite' && !key.startsWith('VITE_')) prefixWarnings.push({ key, type: 'vite' });
|
|
286
|
+
if (projectType === 'cra' && !key.startsWith('REACT_APP_')) prefixWarnings.push({ key, type: 'cra' });
|
|
265
287
|
}
|
|
266
288
|
|
|
267
|
-
|
|
289
|
+
const criticalIssues = [];
|
|
290
|
+
const warningIssues = [];
|
|
291
|
+
const passingChecks = [];
|
|
268
292
|
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
const
|
|
293
|
+
if (!result) criticalIssues.push({ title: '.env file is missing', impact: IMPACT.noEnvFile() });
|
|
294
|
+
for (const key of missing) criticalIssues.push({ title: key + ' is missing', impact: IMPACT.missing(key) });
|
|
295
|
+
for (const key of empty) criticalIssues.push({ title: key + ' is empty', impact: IMPACT.empty(key) });
|
|
296
|
+
if (drift && drift.notInstalled) criticalIssues.push({ title: 'Dependencies are not installed', impact: IMPACT.depsNotInstalled() });
|
|
297
|
+
for (const file of envInGit) criticalIssues.push({ title: file + ' is tracked by git', impact: IMPACT.envInGit(file) });
|
|
298
|
+
for (const file of hardcodedSecrets) criticalIssues.push({ title: 'Hardcoded secret in ' + file, impact: IMPACT.hardcodedSecret(file) });
|
|
272
299
|
|
|
273
|
-
// Criticals: missing vars
|
|
274
|
-
for (const key of missing) {
|
|
275
|
-
criticalIssues.push({ title: key + ' is missing', impact: IMPACT.missing(key) });
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Criticals: empty vars
|
|
279
|
-
for (const key of empty) {
|
|
280
|
-
criticalIssues.push({ title: key + ' is empty', impact: IMPACT.empty(key) });
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Criticals: deps not installed at all
|
|
284
|
-
if (drift && drift.notInstalled) {
|
|
285
|
-
criticalIssues.push({ title: 'Dependencies are not installed', impact: IMPACT.depsNotInstalled() });
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Warnings: dependency drift
|
|
289
300
|
if (drift && !drift.notInstalled && drift.missing.length > 0) {
|
|
290
|
-
for (const dep of drift.missing) {
|
|
291
|
-
warningIssues.push({ title: dep + ' is not installed', impact: IMPACT.depsDrift(dep) });
|
|
292
|
-
}
|
|
301
|
+
for (const dep of drift.missing) warningIssues.push({ title: dep + ' is not installed', impact: IMPACT.depsDrift(dep) });
|
|
293
302
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
for (const key of duplicates) {
|
|
297
|
-
warningIssues.push({ title: key + ' is defined more than once', impact: IMPACT.duplicate(key) });
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Warnings: prefix issues
|
|
303
|
+
if (lockfileMissing) warningIssues.push({ title: 'No lockfile found', impact: IMPACT.lockfileMissing() });
|
|
304
|
+
for (const key of duplicates) warningIssues.push({ title: key + ' is defined more than once', impact: IMPACT.duplicate(key) });
|
|
301
305
|
for (const { key, type } of prefixWarnings) {
|
|
302
|
-
if (type === 'vite') {
|
|
303
|
-
|
|
304
|
-
} else {
|
|
305
|
-
warningIssues.push({ title: key + ' is missing REACT_APP_ prefix', impact: IMPACT.prefixCra(key) });
|
|
306
|
-
}
|
|
306
|
+
if (type === 'vite') warningIssues.push({ title: key + ' is missing VITE_ prefix', impact: IMPACT.prefixVite(key) });
|
|
307
|
+
else warningIssues.push({ title: key + ' is missing REACT_APP_ prefix', impact: IMPACT.prefixCra(key) });
|
|
307
308
|
}
|
|
309
|
+
if (!envIgnored && fs.existsSync(path.join(cwd, '.env'))) warningIssues.push({ title: '.env is not in .gitignore', impact: IMPACT.envNotIgnored() });
|
|
310
|
+
if (envExampleOutOfSync.length > 0) warningIssues.push({ title: '.env.example is out of sync', impact: IMPACT.envExampleOutOfSync(envExampleOutOfSync) });
|
|
311
|
+
if (!hasBuildScript && fs.existsSync(path.join(cwd, 'package.json'))) warningIssues.push({ title: 'No build script in package.json', impact: IMPACT.buildScriptMissing() });
|
|
312
|
+
if (gitStatus.uncommitted) warningIssues.push({ title: 'You have uncommitted changes', impact: IMPACT.uncommittedChanges() });
|
|
313
|
+
if (gitStatus.unpushed) warningIssues.push({ title: 'You have unpushed commits', impact: IMPACT.unpushedCommits() });
|
|
308
314
|
|
|
309
|
-
// Passing checks
|
|
310
315
|
if (result) passingChecks.push('Environment file detected');
|
|
316
|
+
if (envIgnored) passingChecks.push('.env is in .gitignore');
|
|
317
|
+
if (envInGit.length === 0) passingChecks.push('No env files tracked by git');
|
|
318
|
+
if (hardcodedSecrets.length === 0) passingChecks.push('No hardcoded secrets found');
|
|
319
|
+
if (!lockfileMissing) passingChecks.push('Lockfile present');
|
|
311
320
|
if (!drift || (!drift.notInstalled && drift.missing.length === 0)) passingChecks.push('Dependencies installed');
|
|
312
321
|
if (duplicates.length === 0) passingChecks.push('No duplicate variables');
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
322
|
+
if (hasBuildScript) passingChecks.push('Build script found');
|
|
323
|
+
if (!gitStatus.uncommitted) passingChecks.push('No uncommitted changes');
|
|
324
|
+
if (!gitStatus.unpushed) passingChecks.push('No unpushed commits');
|
|
325
|
+
for (const key of present) passingChecks.push(key + ' configured');
|
|
316
326
|
|
|
317
327
|
const totalIssues = criticalIssues.length + warningIssues.length;
|
|
318
328
|
const hasFailed = totalIssues > 0;
|
|
319
329
|
|
|
320
|
-
|
|
330
|
+
if (found.size === 0 && totalIssues === 0) {
|
|
331
|
+
console.log('\uD83D\uDEE1\uFE0F Safelaunch Scan Report');
|
|
332
|
+
console.log('');
|
|
333
|
+
console.log('No environment variables found in your codebase.');
|
|
334
|
+
console.log('');
|
|
335
|
+
console.log(DIVIDER);
|
|
336
|
+
console.log('');
|
|
337
|
+
console.log('\u2705 Nothing to check');
|
|
338
|
+
console.log('');
|
|
339
|
+
console.log(' Your project does not appear to use process.env');
|
|
340
|
+
console.log('');
|
|
341
|
+
await track('safelaunch_scan_run', { project_type: projectType, vars_found: 0 });
|
|
342
|
+
await shutdown();
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
321
345
|
|
|
322
346
|
if (hasFailed) {
|
|
323
|
-
const critCount = criticalIssues.length;
|
|
324
|
-
const warnCount = warningIssues.length;
|
|
325
|
-
|
|
326
|
-
let summary = totalIssues + ' issue' + (totalIssues !== 1 ? 's' : '') + ' found';
|
|
327
347
|
const parts = [];
|
|
328
|
-
if (
|
|
329
|
-
if (
|
|
330
|
-
summary += '\n' + parts.join(' · ');
|
|
348
|
+
if (criticalIssues.length > 0) parts.push(criticalIssues.length + ' critical');
|
|
349
|
+
if (warningIssues.length > 0) parts.push(warningIssues.length + ' warning' + (warningIssues.length !== 1 ? 's' : ''));
|
|
331
350
|
|
|
332
|
-
console.log('
|
|
351
|
+
console.log('\uD83D\uDEA8 Safelaunch Scan Report');
|
|
333
352
|
console.log('');
|
|
334
353
|
console.log('This project is NOT safe to deploy');
|
|
335
354
|
console.log('');
|
|
336
|
-
console.log(
|
|
355
|
+
console.log(totalIssues + ' issue' + (totalIssues !== 1 ? 's' : '') + ' found');
|
|
356
|
+
console.log(parts.join(' \u00B7 '));
|
|
337
357
|
console.log('');
|
|
338
358
|
console.log(DIVIDER);
|
|
339
359
|
console.log('');
|
|
340
360
|
|
|
341
361
|
if (criticalIssues.length > 0) {
|
|
342
|
-
console.log('
|
|
362
|
+
console.log('\uD83D\uDEA8 CRITICAL (will break your app)');
|
|
343
363
|
console.log('');
|
|
344
364
|
criticalIssues.forEach((issue, i) => {
|
|
345
365
|
console.log(renderIssue(i + 1, issue.title, issue.impact));
|
|
346
|
-
if (i < criticalIssues.length - 1) {
|
|
347
|
-
console.log('---');
|
|
348
|
-
console.log('');
|
|
349
|
-
}
|
|
366
|
+
if (i < criticalIssues.length - 1) { console.log('---'); console.log(''); }
|
|
350
367
|
});
|
|
351
368
|
console.log('');
|
|
352
369
|
console.log(DIVIDER);
|
|
@@ -354,15 +371,12 @@ async function scan() {
|
|
|
354
371
|
}
|
|
355
372
|
|
|
356
373
|
if (warningIssues.length > 0) {
|
|
357
|
-
console.log('
|
|
374
|
+
console.log('\u26A0\uFE0F WARNINGS (may cause issues)');
|
|
358
375
|
console.log('');
|
|
359
376
|
const offset = criticalIssues.length;
|
|
360
377
|
warningIssues.forEach((issue, i) => {
|
|
361
378
|
console.log(renderIssue(offset + i + 1, issue.title, issue.impact));
|
|
362
|
-
if (i < warningIssues.length - 1) {
|
|
363
|
-
console.log('---');
|
|
364
|
-
console.log('');
|
|
365
|
-
}
|
|
379
|
+
if (i < warningIssues.length - 1) { console.log('---'); console.log(''); }
|
|
366
380
|
});
|
|
367
381
|
console.log('');
|
|
368
382
|
console.log(DIVIDER);
|
|
@@ -370,17 +384,15 @@ async function scan() {
|
|
|
370
384
|
}
|
|
371
385
|
|
|
372
386
|
if (passingChecks.length > 0) {
|
|
373
|
-
console.log('
|
|
387
|
+
console.log('\u2705 What\'s working (' + passingChecks.length + ' checks passed)');
|
|
374
388
|
console.log('');
|
|
375
|
-
for (const check of passingChecks)
|
|
376
|
-
console.log('- ' + check);
|
|
377
|
-
}
|
|
389
|
+
for (const check of passingChecks) console.log('- ' + check);
|
|
378
390
|
console.log('');
|
|
379
391
|
console.log(DIVIDER);
|
|
380
392
|
console.log('');
|
|
381
393
|
}
|
|
382
394
|
|
|
383
|
-
console.log('
|
|
395
|
+
console.log('\uD83D\uDCA1 Next step');
|
|
384
396
|
console.log('');
|
|
385
397
|
console.log(' Run:');
|
|
386
398
|
console.log(' safelaunch init');
|
|
@@ -389,9 +401,7 @@ async function scan() {
|
|
|
389
401
|
console.log('');
|
|
390
402
|
|
|
391
403
|
} else {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
console.log('🛡️ Safelaunch Scan Report');
|
|
404
|
+
console.log('\uD83D\uDEE1\uFE0F Safelaunch Scan Report');
|
|
395
405
|
console.log('');
|
|
396
406
|
console.log('Your project is safe to deploy');
|
|
397
407
|
console.log('');
|
|
@@ -399,15 +409,13 @@ async function scan() {
|
|
|
399
409
|
console.log('');
|
|
400
410
|
console.log(DIVIDER);
|
|
401
411
|
console.log('');
|
|
402
|
-
console.log('
|
|
412
|
+
console.log('\u2705 All checks passed');
|
|
403
413
|
console.log('');
|
|
404
|
-
for (const check of passingChecks)
|
|
405
|
-
console.log('- ' + check);
|
|
406
|
-
}
|
|
414
|
+
for (const check of passingChecks) console.log('- ' + check);
|
|
407
415
|
console.log('');
|
|
408
416
|
console.log(DIVIDER);
|
|
409
417
|
console.log('');
|
|
410
|
-
console.log('
|
|
418
|
+
console.log('\uD83C\uDF89 You\'re good to go');
|
|
411
419
|
console.log('');
|
|
412
420
|
console.log(' Deploy with confidence');
|
|
413
421
|
console.log('');
|
|
@@ -421,7 +429,14 @@ async function scan() {
|
|
|
421
429
|
empty: empty.length,
|
|
422
430
|
duplicates: duplicates.length,
|
|
423
431
|
prefix_warnings: prefixWarnings.length,
|
|
424
|
-
dependency_drift: !!(drift && drift.missing.length > 0)
|
|
432
|
+
dependency_drift: !!(drift && drift.missing.length > 0),
|
|
433
|
+
env_in_git: envInGit.length,
|
|
434
|
+
hardcoded_secrets: hardcodedSecrets.length,
|
|
435
|
+
lockfile_missing: lockfileMissing,
|
|
436
|
+
env_not_ignored: !envIgnored,
|
|
437
|
+
build_script_missing: !hasBuildScript,
|
|
438
|
+
uncommitted: gitStatus.uncommitted,
|
|
439
|
+
unpushed: gitStatus.unpushed
|
|
425
440
|
});
|
|
426
441
|
|
|
427
442
|
await shutdown();
|