safelaunch 1.0.31 → 1.0.33

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 +96 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "safelaunch",
3
- "version": "1.0.31",
3
+ "version": "1.0.33",
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,16 +63,41 @@ 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
+ NODE_MODULES_STALE: () => ({
67
+ title: "node_modules may be out of date",
68
+ impact: "package.json was modified after node_modules was last updated. You may be running old or missing dependencies.",
69
+ fix: "Run npm install to sync your dependencies.",
70
+ }),
66
71
  MISSING_NODE_MODULES: () => ({
67
72
  title: "node_modules is missing",
68
73
  impact: "The app won't start. Nothing is installed.",
69
74
  fix: "Run npm install.",
70
75
  }),
76
+ AUDIT_HIGH: (count) => ({
77
+ title: `${count} high-severity vulnerability${count > 1 ? "ies" : "y"} in dependencies`,
78
+ impact: "Known vulnerabilities exist that could be exploited. These should be fixed before deploying.",
79
+ fix: "Run npm audit fix or check npm audit for manual fixes.",
80
+ }),
81
+ AUDIT_HIGH: (count) => ({
82
+ title: `${count} high-severity vulnerability${count > 1 ? "ies" : "y"} in dependencies`,
83
+ impact: "Known vulnerabilities exist that could be exploited. These should be fixed before deploying.",
84
+ fix: "Run npm audit fix or check npm audit for manual fixes.",
85
+ }),
71
86
  AUDIT_CRITICAL: (count) => ({
72
87
  title: `${count} critical vulnerability${count > 1 ? "ies" : "y"} in dependencies`,
73
88
  impact: "Known exploits exist for these packages. Shipping them puts your users and infrastructure at risk.",
74
89
  fix: "Run npm audit fix or check npm audit for manual fixes.",
75
90
  }),
91
+ PYTHON_MISMATCH: (expected, actual) => ({
92
+ title: `Python version mismatch (expected: ${expected}, running: ${actual})`,
93
+ impact: "Code that works locally may fail in CI or production if they use the expected version.",
94
+ fix: `Switch to Python ${expected} or update .python-version to match your runtime.`,
95
+ }),
96
+ NODE_ENGINES_MISMATCH: (expected, actual) => ({
97
+ title: `Node version mismatch (package.json engines: ${expected}, running: ${actual})`,
98
+ impact: "Your package.json declares a required Node version that differs from what is running.",
99
+ fix: `Switch to Node ${expected} or update the engines field in package.json.`,
100
+ }),
76
101
  NODE_MISMATCH: (expected, actual) => ({
77
102
  title: `Node version mismatch (.nvmrc: ${expected}, running: ${actual})`,
78
103
  impact: "Code that works locally may fail in CI or production if they use the expected version.",
@@ -190,12 +215,21 @@ function checkNodeModules(cwd) {
190
215
  if (!fileExists(pkgPath)) return issues;
191
216
  try {
192
217
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
193
- const deps = Object.keys(pkg.dependencies || {});
218
+ const deps = Object.keys({ ...pkg.dependencies, ...pkg.devDependencies });
194
219
  if (deps.length === 0) return issues;
195
220
  } catch { return issues; }
196
- if (!fileExists(path.join(cwd, "node_modules"))) {
221
+ const nmPath = path.join(cwd, "node_modules");
222
+ if (!fileExists(nmPath)) {
197
223
  issues.push({ severity: "block", ...IMPACTS.MISSING_NODE_MODULES() });
224
+ return issues;
198
225
  }
226
+ try {
227
+ const pkgStat = fs.statSync(pkgPath);
228
+ const nmStat = fs.statSync(nmPath);
229
+ if (pkgStat.mtimeMs > nmStat.mtimeMs) {
230
+ issues.push({ severity: "warn", ...IMPACTS.NODE_MODULES_STALE() });
231
+ }
232
+ } catch {}
199
233
  return issues;
200
234
  }
201
235
 
@@ -203,12 +237,15 @@ function checkNpmAudit(cwd) {
203
237
  const issues = [];
204
238
  if (!fileExists(path.join(cwd, "package.json"))) return issues;
205
239
  try {
206
- execSync("npm audit --audit-level=critical --json", { cwd, encoding: "utf8", stdio: ["pipe","pipe","pipe"] });
240
+ execSync("npm audit --json", { cwd, encoding: "utf8", stdio: ["pipe","pipe","pipe"] });
207
241
  } catch (e) {
208
242
  try {
209
243
  const data = JSON.parse(e.stdout || "");
210
- const critCount = (data.metadata && data.metadata.vulnerabilities && data.metadata.vulnerabilities.critical) || 0;
211
- if (critCount > 0) issues.push({ severity: "warn", ...IMPACTS.AUDIT_CRITICAL(critCount) });
244
+ const vulns = (data.metadata && data.metadata.vulnerabilities) || {};
245
+ const critCount = vulns.critical || 0;
246
+ const highCount = vulns.high || 0;
247
+ if (critCount > 0) issues.push({ severity: "block", ...IMPACTS.AUDIT_CRITICAL(critCount) });
248
+ if (highCount > 0) issues.push({ severity: "warn", ...IMPACTS.AUDIT_HIGH(highCount) });
212
249
  } catch {}
213
250
  }
214
251
  return issues;
@@ -216,16 +253,63 @@ function checkNpmAudit(cwd) {
216
253
 
217
254
  function checkNodeVersion(cwd) {
218
255
  const issues = [];
219
- const content = readFileSafe(path.join(cwd, ".nvmrc"));
220
- if (!content) return issues;
221
- const expected = content.trim().replace(/^v/, "");
222
- const actual = process.version.replace(/^v/, "");
223
- if (expected.split(".")[0] !== actual.split(".")[0]) {
224
- issues.push({ severity: "info", ...IMPACTS.NODE_MISMATCH(expected, actual) });
256
+ const actual = process.version.replace(/^v/, "");
257
+
258
+ // Check .nvmrc
259
+ const nvmrc = readFileSafe(path.join(cwd, ".nvmrc"));
260
+ if (nvmrc) {
261
+ const expected = nvmrc.trim().replace(/^v/, "");
262
+ if (expected.split(".")[0] !== actual.split(".")[0]) {
263
+ issues.push({ severity: "warn", ...IMPACTS.NODE_MISMATCH(expected, actual) });
264
+ }
225
265
  }
266
+
267
+ // Check package.json engines.node
268
+ const pkgRaw = readFileSafe(path.join(cwd, "package.json"));
269
+ if (pkgRaw) {
270
+ try {
271
+ const pkg = JSON.parse(pkgRaw);
272
+ const enginesNode = pkg.engines && pkg.engines.node;
273
+ if (enginesNode) {
274
+ const match = enginesNode.match(/(\d+)/);
275
+ if (match && match[1] !== actual.split(".")[0]) {
276
+ issues.push({ severity: "warn", ...IMPACTS.NODE_ENGINES_MISMATCH(enginesNode, actual) });
277
+ }
278
+ }
279
+ } catch {}
280
+ }
281
+
226
282
  return issues;
227
283
  }
228
284
 
285
+ function checkPythonVersion(cwd) {
286
+ const issues = [];
287
+ const actual = exec("python3 --version") || exec("python --version");
288
+ if (!actual) return issues;
289
+ const actualVer = actual.replace(/^Python\s+/i, "").trim();
290
+
291
+ // Check .python-version
292
+ const pyVer = readFileSafe(path.join(cwd, ".python-version"));
293
+ if (pyVer) {
294
+ const expected = pyVer.trim();
295
+ if (expected.split(".")[0] !== actualVer.split(".")[0]) {
296
+ issues.push({ severity: "warn", ...IMPACTS.PYTHON_MISMATCH(expected, actualVer) });
297
+ }
298
+ }
299
+
300
+ // Check runtime.txt (Heroku/Render style)
301
+ const runtimeTxt = readFileSafe(path.join(cwd, "runtime.txt"));
302
+ if (runtimeTxt && runtimeTxt.startsWith("python-")) {
303
+ const expected = runtimeTxt.replace("python-", "").trim();
304
+ if (expected.split(".")[0] !== actualVer.split(".")[0]) {
305
+ issues.push({ severity: "warn", ...IMPACTS.PYTHON_MISMATCH(expected, actualVer) });
306
+ }
307
+ }
308
+
309
+ return issues;
310
+ }
311
+
312
+
229
313
  function renderHookOutput(blockers, warnings, infos, elapsed) {
230
314
  const lines = [];
231
315
  const hr = gray("─".repeat(49));
@@ -325,6 +409,7 @@ async function runScan(options = {}) {
325
409
  ...checkTypeScript(cwd),
326
410
  ...checkNpmAudit(cwd),
327
411
  ...checkNodeVersion(cwd),
412
+ ...checkPythonVersion(cwd),
328
413
  ];
329
414
  const blockers = allIssues.filter((i) => i.severity === "block");
330
415
  const warnings = allIssues.filter((i) => i.severity === "warn");