safelaunch 1.0.34 → 1.0.39

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/scan.js +252 -10
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "safelaunch",
3
- "version": "1.0.34",
3
+ "version": "1.0.39",
4
4
  "description": "Backend Reliability Infrastructure - catch what breaks production before it breaks",
5
5
  "main": "index.js",
6
6
  "bin": {
package/src/scan.js CHANGED
@@ -63,6 +63,51 @@ 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
+ }),
66
111
  ENV_NOT_GITIGNORED: () => ({
67
112
  title: ".env is not in .gitignore",
68
113
  impact: "Your .env file could be committed to git, exposing secrets to anyone with repo access.",
@@ -83,12 +128,7 @@ const IMPACTS = {
83
128
  impact: "Known vulnerabilities exist that could be exploited. These should be fixed before deploying.",
84
129
  fix: "Run npm audit fix or check npm audit for manual fixes.",
85
130
  }),
86
- AUDIT_HIGH: (count) => ({
87
- title: `${count} high-severity vulnerability${count > 1 ? "ies" : "y"} in dependencies`,
88
- impact: "Known vulnerabilities exist that could be exploited. These should be fixed before deploying.",
89
- fix: "Run npm audit fix or check npm audit for manual fixes.",
90
- }),
91
- AUDIT_CRITICAL: (count) => ({
131
+ AUDIT_CRITICAL: (count) => ({
92
132
  title: `${count} critical vulnerability${count > 1 ? "ies" : "y"} in dependencies`,
93
133
  impact: "Known exploits exist for these packages. Shipping them puts your users and infrastructure at risk.",
94
134
  fix: "Run npm audit fix or check npm audit for manual fixes.",
@@ -132,8 +172,9 @@ function loadManifest(cwd) {
132
172
 
133
173
  function checkMissingEnvVars(cwd, envVars, manifest) {
134
174
  const issues = [];
135
- if (!manifest || !manifest.required) return issues;
136
- for (const name of manifest.required) {
175
+ if (!manifest || !manifest.envs) return issues;
176
+ for (const [name, meta] of Object.entries(manifest.envs)) {
177
+ if (!meta.required) continue;
137
178
  if (!(name in envVars) && !process.env[name]) {
138
179
  issues.push({ severity: "block", ...IMPACTS.MISSING_ENV_VAR(name) });
139
180
  }
@@ -207,7 +248,7 @@ function checkTypeScript(cwd) {
207
248
  const issues = [];
208
249
  if (!fileExists(path.join(cwd, "tsconfig.json"))) return issues;
209
250
  try {
210
- execSync("npx tsc --noEmit", { cwd, encoding: "utf8", stdio: ["pipe","pipe","pipe"] });
251
+ execSync("npx tsc --noEmit", { cwd, encoding: "utf8", stdio: ["pipe","pipe","pipe"], timeout: 30000 });
211
252
  } catch {
212
253
  issues.push({ severity: "block", ...IMPACTS.TS_ERRORS() });
213
254
  }
@@ -242,7 +283,7 @@ function checkNpmAudit(cwd) {
242
283
  const issues = [];
243
284
  if (!fileExists(path.join(cwd, "package.json"))) return issues;
244
285
  try {
245
- execSync("npm audit --json", { cwd, encoding: "utf8", stdio: ["pipe","pipe","pipe"] });
286
+ execSync("npm audit --json", { cwd, encoding: "utf8", stdio: ["pipe","pipe","pipe"], timeout: 15000 });
246
287
  } catch (e) {
247
288
  try {
248
289
  const data = JSON.parse(e.stdout || "");
@@ -415,6 +456,198 @@ function checkEnvGitignored(cwd) {
415
456
  }
416
457
  return issues;
417
458
  }
459
+
460
+ function checkHardcodedLocalhost(cwd) {
461
+ const issues = [];
462
+ const extensions = ['.js', '.ts', '.jsx', '.tsx', '.env.example', '.json', '.html'];
463
+ const ignore = ['node_modules', '.git', 'dist', 'build', '.next', '.vscode'];
464
+ const localhostRe = /https?:\/\/localhost[:/]/;
465
+
466
+ function walk(dir) {
467
+ let entries;
468
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
469
+ for (const entry of entries) {
470
+ if (ignore.includes(entry.name)) continue;
471
+ const full = path.join(dir, entry.name);
472
+ if (entry.isDirectory()) { walk(full); continue; }
473
+ if (!extensions.some(ext => entry.name.endsWith(ext))) continue;
474
+ const content = readFileSafe(full);
475
+ if (!content) continue;
476
+ const lines = content.split('\n');
477
+ for (let i = 0; i < lines.length; i++) {
478
+ if (localhostRe.test(lines[i])) {
479
+ const rel = path.relative(cwd, full);
480
+ issues.push({ severity: "warn", ...IMPACTS.HARDCODED_LOCALHOST(rel, i + 1) });
481
+ break; // one issue per file
482
+ }
483
+ }
484
+ }
485
+ }
486
+
487
+ walk(cwd);
488
+ return issues;
489
+ }
490
+
491
+ function checkConsoleLogs(cwd) {
492
+ const issues = [];
493
+ const extensions = ['.js', '.ts', '.jsx', '.tsx'];
494
+ const ignore = ['node_modules', '.git', 'dist', 'build', '.next', '.vscode'];
495
+ const consoleRe = /console\.log\s*\(/;
496
+
497
+ function walk(dir) {
498
+ let entries;
499
+ try { entries = require('fs').readdirSync(dir, { withFileTypes: true }); } catch { return; }
500
+ for (const entry of entries) {
501
+ if (ignore.includes(entry.name)) continue;
502
+ const full = require('path').join(dir, entry.name);
503
+ if (entry.isDirectory()) { walk(full); continue; }
504
+ if (!extensions.some(ext => entry.name.endsWith(ext))) continue;
505
+ const src = readFileSafe(full);
506
+ if (!src) continue;
507
+ const lines = src.split('\n');
508
+ for (let i = 0; i < lines.length; i++) {
509
+ if (consoleRe.test(lines[i])) {
510
+ const rel = require('path').relative(cwd, full);
511
+ issues.push({ severity: "warn", ...IMPACTS.CONSOLE_LOG_FOUND(rel, i + 1) });
512
+ break;
513
+ }
514
+ }
515
+ }
516
+ }
517
+
518
+ walk(cwd);
519
+ return issues;
520
+ }
521
+
522
+ function checkBuildScript(cwd) {
523
+ const issues = [];
524
+ const pkgRaw = readFileSafe(require('path').join(cwd, "package.json"));
525
+ if (!pkgRaw) return issues;
526
+ try {
527
+ const pkg = JSON.parse(pkgRaw);
528
+ const scripts = pkg.scripts || {};
529
+ if (!scripts.build) {
530
+ issues.push({ severity: "warn", ...IMPACTS.MISSING_BUILD_SCRIPT() });
531
+ }
532
+ } catch {}
533
+ return issues;
534
+ }
535
+
536
+ function checkEnvExample(cwd) {
537
+ const issues = [];
538
+ const p = require('path');
539
+ if (!readFileSafe(p.join(cwd, ".env"))) return issues;
540
+ if (!readFileSafe(p.join(cwd, ".env.example")) && !readFileSafe(p.join(cwd, ".env.sample"))) {
541
+ issues.push({ severity: "info", ...IMPACTS.MISSING_ENV_EXAMPLE() });
542
+ }
543
+ return issues;
544
+ }
545
+
546
+ function checkDebugFlags(cwd) {
547
+ const issues = [];
548
+ const envVars = readFileSafe(require('path').join(cwd, ".env"));
549
+ if (!envVars) return issues;
550
+ const debugKeys = ['DEBUG', 'NODE_ENV'];
551
+ for (const line of envVars.split('\n')) {
552
+ const trimmed = line.trim();
553
+ if (!trimmed || trimmed.startsWith('#')) continue;
554
+ const [key, val] = trimmed.split('=');
555
+ if (!key || !val) continue;
556
+ const k = key.trim().toUpperCase();
557
+ const v = val.trim().toLowerCase();
558
+ if (k === 'DEBUG' && (v === 'true' || v === '1' || v === '*')) {
559
+ issues.push({ severity: "warn", ...IMPACTS.DEBUG_FLAG_ENABLED(key.trim()) });
560
+ }
561
+ if (k === 'NODE_ENV' && v === 'development') {
562
+ issues.push({ severity: "warn", ...IMPACTS.DEBUG_FLAG_ENABLED(key.trim()) });
563
+ }
564
+ }
565
+ return issues;
566
+ }
567
+
568
+ function checkStartScript(cwd) {
569
+ const issues = [];
570
+ const pkgRaw = readFileSafe(require('path').join(cwd, "package.json"));
571
+ if (!pkgRaw) return issues;
572
+ try {
573
+ const pkg = JSON.parse(pkgRaw);
574
+ const scripts = pkg.scripts || {};
575
+ if (!scripts.start) {
576
+ issues.push({ severity: "warn", ...IMPACTS.MISSING_START_SCRIPT() });
577
+ }
578
+ } catch {}
579
+ return issues;
580
+ }
581
+
582
+ function checkWeakSecrets(cwd) {
583
+ const issues = [];
584
+ const envVars = readFileSafe(require('path').join(cwd, ".env"));
585
+ if (!envVars) return issues;
586
+ const weakValues = ['secret', 'password', 'changeme', 'example', 'test', '123456', 'qwerty', 'abc123', 'letmein', 'admin', 'root', 'default'];
587
+ const secretKeys = ['secret', 'password', 'key', 'token', 'jwt', 'api_key', 'apikey', 'auth', 'pass'];
588
+ for (const line of envVars.split('\n')) {
589
+ const trimmed = line.trim();
590
+ if (!trimmed || trimmed.startsWith('#')) continue;
591
+ const eqIdx = trimmed.indexOf('=');
592
+ if (eqIdx === -1) continue;
593
+ const key = trimmed.slice(0, eqIdx).trim().toLowerCase();
594
+ const val = trimmed.slice(eqIdx + 1).trim().toLowerCase();
595
+ if (!val) continue;
596
+ const isSecretKey = secretKeys.some(k => key.includes(k));
597
+ const isWeakVal = weakValues.includes(val);
598
+ if (isSecretKey && isWeakVal) {
599
+ issues.push({ severity: "block", ...IMPACTS.WEAK_SECRET(trimmed.slice(0, eqIdx).trim(), val) });
600
+ }
601
+ }
602
+ return issues;
603
+ }
604
+
605
+ function checkDebuggerStatements(cwd) {
606
+ const issues = [];
607
+ const extensions = ['.js', '.ts', '.jsx', '.tsx'];
608
+ const ignore = ['node_modules', '.git', 'dist', 'build', '.next', '.vscode'];
609
+ const debuggerRe = /^\s*debugger\s*;?\s*$/;
610
+
611
+ function walk(dir) {
612
+ let entries;
613
+ try { entries = require('fs').readdirSync(dir, { withFileTypes: true }); } catch { return; }
614
+ for (const entry of entries) {
615
+ if (ignore.includes(entry.name)) continue;
616
+ const full = require('path').join(dir, entry.name);
617
+ if (entry.isDirectory()) { walk(full); continue; }
618
+ if (!extensions.some(ext => entry.name.endsWith(ext))) continue;
619
+ const src = readFileSafe(full);
620
+ if (!src) continue;
621
+ const lines = src.split('\n');
622
+ for (let i = 0; i < lines.length; i++) {
623
+ if (debuggerRe.test(lines[i])) {
624
+ const rel = require('path').relative(cwd, full);
625
+ issues.push({ severity: "block", ...IMPACTS.DEBUGGER_STATEMENT(rel, i + 1) });
626
+ break;
627
+ }
628
+ }
629
+ }
630
+ }
631
+
632
+ walk(cwd);
633
+ return issues;
634
+ }
635
+
636
+ function checkWildcardDeps(cwd) {
637
+ const issues = [];
638
+ const pkgRaw = readFileSafe(require('path').join(cwd, "package.json"));
639
+ if (!pkgRaw) return issues;
640
+ try {
641
+ const pkg = JSON.parse(pkgRaw);
642
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
643
+ for (const [name, version] of Object.entries(allDeps)) {
644
+ if (version === '*' || version === 'latest') {
645
+ issues.push({ severity: "warn", ...IMPACTS.WILDCARD_DEPENDENCY(name) });
646
+ }
647
+ }
648
+ } catch {}
649
+ return issues;
650
+ }
418
651
  async function runScan(options = {}) {
419
652
  const { hookMode = false, quiet = false, cwd = process.cwd() } = options;
420
653
  const start = Date.now();
@@ -430,6 +663,15 @@ async function runScan(options = {}) {
430
663
  ...checkTypeScript(cwd),
431
664
  ...checkNpmAudit(cwd),
432
665
  ...checkEnvGitignored(cwd),
666
+ ...checkHardcodedLocalhost(cwd),
667
+ ...checkConsoleLogs(cwd),
668
+ ...checkDebuggerStatements(cwd),
669
+ ...checkBuildScript(cwd),
670
+ ...checkStartScript(cwd),
671
+ ...checkWildcardDeps(cwd),
672
+ ...checkEnvExample(cwd),
673
+ ...checkDebugFlags(cwd),
674
+ ...checkWeakSecrets(cwd),
433
675
  ...checkNodeVersion(cwd),
434
676
  ...checkPythonVersion(cwd),
435
677
  ];