safelaunch 1.0.33 → 1.0.37
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/src/scan.js +268 -0
package/package.json
CHANGED
package/src/scan.js
CHANGED
|
@@ -63,6 +63,56 @@ const IMPACTS = {
|
|
|
63
63
|
impact: "Different tools read different copies. Behaviour is unpredictable — one service may use the wrong value.",
|
|
64
64
|
fix: `Remove the duplicate ${name} entry from your .env file.`,
|
|
65
65
|
}),
|
|
66
|
+
WILDCARD_DEPENDENCY: (name) => ({
|
|
67
|
+
title: `Wildcard version for ${name} in package.json`,
|
|
68
|
+
impact: "Using * or latest means npm can install any version, including breaking changes. Your build may work today and fail tomorrow.",
|
|
69
|
+
fix: `Pin ${name} to a specific version, e.g. \"${name}\": \"1.2.3\".`,
|
|
70
|
+
}),
|
|
71
|
+
DEBUGGER_STATEMENT: (file, line) => ({
|
|
72
|
+
title: `debugger statement left in ${file}`,
|
|
73
|
+
impact: "A debugger statement pauses execution in some environments and signals unfinished code. It should never ship to production.",
|
|
74
|
+
fix: `Remove the debugger statement from ${file} line ${line}.`,
|
|
75
|
+
}),
|
|
76
|
+
WEAK_SECRET: (name, val) => ({
|
|
77
|
+
title: `Weak secret value for ${name}`,
|
|
78
|
+
impact: "Predictable secrets can be guessed or brute-forced. This is a serious security vulnerability in production.",
|
|
79
|
+
fix: `Replace the value of ${name} with a strong randomly generated secret.`,
|
|
80
|
+
}),
|
|
81
|
+
DEBUG_FLAG_ENABLED: (name) => ({
|
|
82
|
+
title: `${name} is enabled in .env`,
|
|
83
|
+
impact: "Debug mode in production exposes stack traces, internal errors, and verbose logs to end users.",
|
|
84
|
+
fix: `Set ${name}=false or remove it from your .env before deploying.`,
|
|
85
|
+
}),
|
|
86
|
+
MISSING_ENV_EXAMPLE: () => ({
|
|
87
|
+
title: ".env.example is missing",
|
|
88
|
+
impact: "Other developers cloning this repo will not know what environment variables are required.",
|
|
89
|
+
fix: "Create a .env.example file listing all required variables without their values.",
|
|
90
|
+
}),
|
|
91
|
+
MISSING_START_SCRIPT: () => ({
|
|
92
|
+
title: "No start script in package.json",
|
|
93
|
+
impact: "Deployment platforms like Railway, Heroku, and Render use the start script to run your app. Without it, your deploy will fail.",
|
|
94
|
+
fix: "Add a start script to package.json, e.g. \"start\": \"node src/index.js\".",
|
|
95
|
+
}),
|
|
96
|
+
MISSING_BUILD_SCRIPT: () => ({
|
|
97
|
+
title: "No build script in package.json",
|
|
98
|
+
impact: "CI and deployment platforms expect a build script. Your deploy will likely fail or skip the build step.",
|
|
99
|
+
fix: "Add a build script to package.json, e.g. \"build\": \"tsc\" or \"build\": \"vite build\".",
|
|
100
|
+
}),
|
|
101
|
+
CONSOLE_LOG_FOUND: (file, line) => ({
|
|
102
|
+
title: `console.log left in ${file}`,
|
|
103
|
+
impact: "Debug logs expose internal data and clutter production output. They may leak sensitive information.",
|
|
104
|
+
fix: `Remove or replace the console.log in ${file} line ${line} with a proper logger.`,
|
|
105
|
+
}),
|
|
106
|
+
HARDCODED_LOCALHOST: (file, line) => ({
|
|
107
|
+
title: `Hardcoded localhost URL in ${file}`,
|
|
108
|
+
impact: "This will break in production — localhost only works on your machine.",
|
|
109
|
+
fix: `Replace the localhost URL in ${file} line ${line} with an environment variable.`,
|
|
110
|
+
}),
|
|
111
|
+
ENV_NOT_GITIGNORED: () => ({
|
|
112
|
+
title: ".env is not in .gitignore",
|
|
113
|
+
impact: "Your .env file could be committed to git, exposing secrets to anyone with repo access.",
|
|
114
|
+
fix: "Add .env to your .gitignore file immediately.",
|
|
115
|
+
}),
|
|
66
116
|
NODE_MODULES_STALE: () => ({
|
|
67
117
|
title: "node_modules may be out of date",
|
|
68
118
|
impact: "package.json was modified after node_modules was last updated. You may be running old or missing dependencies.",
|
|
@@ -394,6 +444,214 @@ function renderInteractiveOutput(blockers, warnings, infos, elapsed) {
|
|
|
394
444
|
return lines.join("\n");
|
|
395
445
|
}
|
|
396
446
|
|
|
447
|
+
|
|
448
|
+
function checkEnvGitignored(cwd) {
|
|
449
|
+
const issues = [];
|
|
450
|
+
if (!fileExists(path.join(cwd, ".env"))) return issues;
|
|
451
|
+
const gitignore = readFileSafe(path.join(cwd, ".gitignore"));
|
|
452
|
+
if (!gitignore) {
|
|
453
|
+
issues.push({ severity: "block", ...IMPACTS.ENV_NOT_GITIGNORED() });
|
|
454
|
+
return issues;
|
|
455
|
+
}
|
|
456
|
+
const lines = gitignore.split("\n").map(l => l.trim());
|
|
457
|
+
const ignored = lines.some(l => l === ".env" || l === "*.env" || l === ".env*");
|
|
458
|
+
if (!ignored) {
|
|
459
|
+
issues.push({ severity: "block", ...IMPACTS.ENV_NOT_GITIGNORED() });
|
|
460
|
+
}
|
|
461
|
+
return issues;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function checkHardcodedLocalhost(cwd) {
|
|
465
|
+
const issues = [];
|
|
466
|
+
const extensions = ['.js', '.ts', '.jsx', '.tsx', '.env.example', '.json', '.html'];
|
|
467
|
+
const ignore = ['node_modules', '.git', 'dist', 'build', '.next', '.vscode'];
|
|
468
|
+
const localhostRe = /https?:\/\/localhost[:/]/;
|
|
469
|
+
|
|
470
|
+
function walk(dir) {
|
|
471
|
+
let entries;
|
|
472
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
473
|
+
for (const entry of entries) {
|
|
474
|
+
if (ignore.includes(entry.name)) continue;
|
|
475
|
+
const full = path.join(dir, entry.name);
|
|
476
|
+
if (entry.isDirectory()) { walk(full); continue; }
|
|
477
|
+
if (!extensions.some(ext => entry.name.endsWith(ext))) continue;
|
|
478
|
+
const content = readFileSafe(full);
|
|
479
|
+
if (!content) continue;
|
|
480
|
+
const lines = content.split('\n');
|
|
481
|
+
for (let i = 0; i < lines.length; i++) {
|
|
482
|
+
if (localhostRe.test(lines[i])) {
|
|
483
|
+
const rel = path.relative(cwd, full);
|
|
484
|
+
issues.push({ severity: "warn", ...IMPACTS.HARDCODED_LOCALHOST(rel, i + 1) });
|
|
485
|
+
break; // one issue per file
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
walk(cwd);
|
|
492
|
+
return issues;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function checkConsoleLogs(cwd) {
|
|
496
|
+
const issues = [];
|
|
497
|
+
const extensions = ['.js', '.ts', '.jsx', '.tsx'];
|
|
498
|
+
const ignore = ['node_modules', '.git', 'dist', 'build', '.next', '.vscode'];
|
|
499
|
+
const consoleRe = /console\.log\s*\(/;
|
|
500
|
+
|
|
501
|
+
function walk(dir) {
|
|
502
|
+
let entries;
|
|
503
|
+
try { entries = require('fs').readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
504
|
+
for (const entry of entries) {
|
|
505
|
+
if (ignore.includes(entry.name)) continue;
|
|
506
|
+
const full = require('path').join(dir, entry.name);
|
|
507
|
+
if (entry.isDirectory()) { walk(full); continue; }
|
|
508
|
+
if (!extensions.some(ext => entry.name.endsWith(ext))) continue;
|
|
509
|
+
const src = readFileSafe(full);
|
|
510
|
+
if (!src) continue;
|
|
511
|
+
const lines = src.split('\n');
|
|
512
|
+
for (let i = 0; i < lines.length; i++) {
|
|
513
|
+
if (consoleRe.test(lines[i])) {
|
|
514
|
+
const rel = require('path').relative(cwd, full);
|
|
515
|
+
issues.push({ severity: "warn", ...IMPACTS.CONSOLE_LOG_FOUND(rel, i + 1) });
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
walk(cwd);
|
|
523
|
+
return issues;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function checkBuildScript(cwd) {
|
|
527
|
+
const issues = [];
|
|
528
|
+
const pkgRaw = readFileSafe(require('path').join(cwd, "package.json"));
|
|
529
|
+
if (!pkgRaw) return issues;
|
|
530
|
+
try {
|
|
531
|
+
const pkg = JSON.parse(pkgRaw);
|
|
532
|
+
const scripts = pkg.scripts || {};
|
|
533
|
+
if (!scripts.build) {
|
|
534
|
+
issues.push({ severity: "warn", ...IMPACTS.MISSING_BUILD_SCRIPT() });
|
|
535
|
+
}
|
|
536
|
+
} catch {}
|
|
537
|
+
return issues;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function checkEnvExample(cwd) {
|
|
541
|
+
const issues = [];
|
|
542
|
+
const p = require('path');
|
|
543
|
+
if (!readFileSafe(p.join(cwd, ".env"))) return issues;
|
|
544
|
+
if (!readFileSafe(p.join(cwd, ".env.example")) && !readFileSafe(p.join(cwd, ".env.sample"))) {
|
|
545
|
+
issues.push({ severity: "info", ...IMPACTS.MISSING_ENV_EXAMPLE() });
|
|
546
|
+
}
|
|
547
|
+
return issues;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function checkDebugFlags(cwd) {
|
|
551
|
+
const issues = [];
|
|
552
|
+
const envVars = readFileSafe(require('path').join(cwd, ".env"));
|
|
553
|
+
if (!envVars) return issues;
|
|
554
|
+
const debugKeys = ['DEBUG', 'NODE_ENV'];
|
|
555
|
+
for (const line of envVars.split('\n')) {
|
|
556
|
+
const trimmed = line.trim();
|
|
557
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
558
|
+
const [key, val] = trimmed.split('=');
|
|
559
|
+
if (!key || !val) continue;
|
|
560
|
+
const k = key.trim().toUpperCase();
|
|
561
|
+
const v = val.trim().toLowerCase();
|
|
562
|
+
if (k === 'DEBUG' && (v === 'true' || v === '1' || v === '*')) {
|
|
563
|
+
issues.push({ severity: "warn", ...IMPACTS.DEBUG_FLAG_ENABLED(key.trim()) });
|
|
564
|
+
}
|
|
565
|
+
if (k === 'NODE_ENV' && v === 'development') {
|
|
566
|
+
issues.push({ severity: "warn", ...IMPACTS.DEBUG_FLAG_ENABLED(key.trim()) });
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return issues;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function checkStartScript(cwd) {
|
|
573
|
+
const issues = [];
|
|
574
|
+
const pkgRaw = readFileSafe(require('path').join(cwd, "package.json"));
|
|
575
|
+
if (!pkgRaw) return issues;
|
|
576
|
+
try {
|
|
577
|
+
const pkg = JSON.parse(pkgRaw);
|
|
578
|
+
const scripts = pkg.scripts || {};
|
|
579
|
+
if (!scripts.start) {
|
|
580
|
+
issues.push({ severity: "warn", ...IMPACTS.MISSING_START_SCRIPT() });
|
|
581
|
+
}
|
|
582
|
+
} catch {}
|
|
583
|
+
return issues;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function checkWeakSecrets(cwd) {
|
|
587
|
+
const issues = [];
|
|
588
|
+
const envVars = readFileSafe(require('path').join(cwd, ".env"));
|
|
589
|
+
if (!envVars) return issues;
|
|
590
|
+
const weakValues = ['secret', 'password', 'changeme', 'example', 'test', '123456', 'qwerty', 'abc123', 'letmein', 'admin', 'root', 'default'];
|
|
591
|
+
const secretKeys = ['secret', 'password', 'key', 'token', 'jwt', 'api_key', 'apikey', 'auth', 'pass'];
|
|
592
|
+
for (const line of envVars.split('\n')) {
|
|
593
|
+
const trimmed = line.trim();
|
|
594
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
595
|
+
const eqIdx = trimmed.indexOf('=');
|
|
596
|
+
if (eqIdx === -1) continue;
|
|
597
|
+
const key = trimmed.slice(0, eqIdx).trim().toLowerCase();
|
|
598
|
+
const val = trimmed.slice(eqIdx + 1).trim().toLowerCase();
|
|
599
|
+
if (!val) continue;
|
|
600
|
+
const isSecretKey = secretKeys.some(k => key.includes(k));
|
|
601
|
+
const isWeakVal = weakValues.includes(val);
|
|
602
|
+
if (isSecretKey && isWeakVal) {
|
|
603
|
+
issues.push({ severity: "block", ...IMPACTS.WEAK_SECRET(trimmed.slice(0, eqIdx).trim(), val) });
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return issues;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function checkDebuggerStatements(cwd) {
|
|
610
|
+
const issues = [];
|
|
611
|
+
const extensions = ['.js', '.ts', '.jsx', '.tsx'];
|
|
612
|
+
const ignore = ['node_modules', '.git', 'dist', 'build', '.next', '.vscode'];
|
|
613
|
+
const debuggerRe = /^\s*debugger\s*;?\s*$/;
|
|
614
|
+
|
|
615
|
+
function walk(dir) {
|
|
616
|
+
let entries;
|
|
617
|
+
try { entries = require('fs').readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
618
|
+
for (const entry of entries) {
|
|
619
|
+
if (ignore.includes(entry.name)) continue;
|
|
620
|
+
const full = require('path').join(dir, entry.name);
|
|
621
|
+
if (entry.isDirectory()) { walk(full); continue; }
|
|
622
|
+
if (!extensions.some(ext => entry.name.endsWith(ext))) continue;
|
|
623
|
+
const src = readFileSafe(full);
|
|
624
|
+
if (!src) continue;
|
|
625
|
+
const lines = src.split('\n');
|
|
626
|
+
for (let i = 0; i < lines.length; i++) {
|
|
627
|
+
if (debuggerRe.test(lines[i])) {
|
|
628
|
+
const rel = require('path').relative(cwd, full);
|
|
629
|
+
issues.push({ severity: "block", ...IMPACTS.DEBUGGER_STATEMENT(rel, i + 1) });
|
|
630
|
+
break;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
walk(cwd);
|
|
637
|
+
return issues;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function checkWildcardDeps(cwd) {
|
|
641
|
+
const issues = [];
|
|
642
|
+
const pkgRaw = readFileSafe(require('path').join(cwd, "package.json"));
|
|
643
|
+
if (!pkgRaw) return issues;
|
|
644
|
+
try {
|
|
645
|
+
const pkg = JSON.parse(pkgRaw);
|
|
646
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
647
|
+
for (const [name, version] of Object.entries(allDeps)) {
|
|
648
|
+
if (version === '*' || version === 'latest') {
|
|
649
|
+
issues.push({ severity: "warn", ...IMPACTS.WILDCARD_DEPENDENCY(name) });
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
} catch {}
|
|
653
|
+
return issues;
|
|
654
|
+
}
|
|
397
655
|
async function runScan(options = {}) {
|
|
398
656
|
const { hookMode = false, quiet = false, cwd = process.cwd() } = options;
|
|
399
657
|
const start = Date.now();
|
|
@@ -408,6 +666,16 @@ async function runScan(options = {}) {
|
|
|
408
666
|
...checkLockfileSync(cwd),
|
|
409
667
|
...checkTypeScript(cwd),
|
|
410
668
|
...checkNpmAudit(cwd),
|
|
669
|
+
...checkEnvGitignored(cwd),
|
|
670
|
+
...checkHardcodedLocalhost(cwd),
|
|
671
|
+
...checkConsoleLogs(cwd),
|
|
672
|
+
...checkDebuggerStatements(cwd),
|
|
673
|
+
...checkBuildScript(cwd),
|
|
674
|
+
...checkStartScript(cwd),
|
|
675
|
+
...checkWildcardDeps(cwd),
|
|
676
|
+
...checkEnvExample(cwd),
|
|
677
|
+
...checkDebugFlags(cwd),
|
|
678
|
+
...checkWeakSecrets(cwd),
|
|
411
679
|
...checkNodeVersion(cwd),
|
|
412
680
|
...checkPythonVersion(cwd),
|
|
413
681
|
];
|