safelaunch 1.0.34 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/scan.js +246 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "safelaunch",
3
- "version": "1.0.34",
3
+ "version": "1.0.37",
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.",
@@ -415,6 +460,198 @@ function checkEnvGitignored(cwd) {
415
460
  }
416
461
  return issues;
417
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
+ }
418
655
  async function runScan(options = {}) {
419
656
  const { hookMode = false, quiet = false, cwd = process.cwd() } = options;
420
657
  const start = Date.now();
@@ -430,6 +667,15 @@ async function runScan(options = {}) {
430
667
  ...checkTypeScript(cwd),
431
668
  ...checkNpmAudit(cwd),
432
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),
433
679
  ...checkNodeVersion(cwd),
434
680
  ...checkPythonVersion(cwd),
435
681
  ];