@checkstack/healthcheck-script-backend 0.3.0 → 0.3.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,49 @@
1
1
  # @checkstack/healthcheck-script-backend
2
2
 
3
+ ## 0.3.2
4
+
5
+ ### Patch Changes
6
+
7
+ - d73e33e: Security fix: Prevent environment variable leakage to child processes.
8
+ - 0ebbe56: Security Vulnerability Remediation completed:
9
+ - Refactored core authorization to Fail-Closed architecture with secure defaults.
10
+ - Implemented `assertTeamManagementAccess` to resolve BOLA in Teams Management.
11
+ - Protected internal S2S capabilities via explicit wildcard `serviceScope` definitions.
12
+ - Disarmed OS Command Injection in DiskCollector via strict regex validation and bash escaping.
13
+ - Re-architected inline script processing executing scripts in sandboxed Web Worker contexts.
14
+ - Isolated subprocess environment scopes in PingStrategy limiting variable leakage.
15
+ - Enforced strict token/API Key parsing with URLSearchParams checking.
16
+ - Explicitly fail-fast on missing DATABASE_URL configuration across independent backend clusters.
17
+ - Activated strict HTTP Security Headers (HSTS, CSP, X-Frame-Options) across the API automatically.
18
+ - Updated dependencies [0ebbe56]
19
+ - @checkstack/backend-api@0.8.1
20
+ - @checkstack/common@0.6.3
21
+ - @checkstack/healthcheck-common@0.8.3
22
+
23
+ ## 0.3.1
24
+
25
+ ### Patch Changes
26
+
27
+ - 869b4ab: ## Health Check Execution Improvements
28
+
29
+ ### Breaking Changes (backend-api)
30
+
31
+ - `HealthCheckStrategy.createClient()` now accepts `unknown` instead of `TConfig` due to TypeScript contravariance constraints. Implementations should use `this.config.validate(config)` to narrow the type.
32
+
33
+ ### Features
34
+
35
+ - **Platform-level hard timeout**: The executor now wraps the entire health check execution (connection + all collectors) in a single timeout, ensuring checks never hang indefinitely.
36
+ - **Parallel collector execution**: Collectors now run in parallel using `Promise.allSettled()`, improving performance while ensuring all collectors complete regardless of individual failures.
37
+ - **Base strategy config schema**: All strategy configs now extend `baseStrategyConfigSchema` which provides a standardized `timeout` field with sensible defaults (30s, min 100ms).
38
+
39
+ ### Fixes
40
+
41
+ - Fixed HTTP and Jenkins strategies clearing timeouts before reading the full response body.
42
+ - Simplified registry type signatures by using default type parameters.
43
+
44
+ - Updated dependencies [869b4ab]
45
+ - @checkstack/backend-api@0.8.0
46
+
3
47
  ## 0.3.0
4
48
 
5
49
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/healthcheck-script-backend",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "scripts": {
@@ -9,9 +9,9 @@
9
9
  "lint:code": "eslint . --max-warnings 0"
10
10
  },
11
11
  "dependencies": {
12
- "@checkstack/backend-api": "0.5.2",
13
- "@checkstack/common": "0.6.1",
14
- "@checkstack/healthcheck-common": "0.8.1"
12
+ "@checkstack/backend-api": "0.8.0",
13
+ "@checkstack/common": "0.6.2",
14
+ "@checkstack/healthcheck-common": "0.8.2"
15
15
  },
16
16
  "devDependencies": {
17
17
  "@types/bun": "^1.0.0",
@@ -77,22 +77,67 @@ async function executeInlineScript({
77
77
  timedOut: boolean;
78
78
  }> {
79
79
  try {
80
- // Create an async function from the script
81
- const asyncFn = new Function(
82
- "context",
83
- "console",
84
- "fetch",
85
- `return (async () => { ${script} })();`,
86
- );
87
-
88
- // Execute with timeout
89
- const result = await Promise.race([
90
- asyncFn(context, safeConsole, fetch),
80
+ // SECURITY: Execute in isolated Worker to prevent access to process, require, Bun, etc.
81
+ const workerCode = `
82
+ self.onmessage = async (event) => {
83
+ const { script, config } = event.data;
84
+ const logs = [];
85
+ const safeConsole = {
86
+ log: (...args) => logs.push(args.map(a => typeof a === "object" ? JSON.stringify(a) : String(a)).join(" ")),
87
+ warn: (...args) => logs.push("[WARN] " + args.map(a => typeof a === "object" ? JSON.stringify(a) : String(a)).join(" ")),
88
+ error: (...args) => logs.push("[ERROR] " + args.map(a => typeof a === "object" ? JSON.stringify(a) : String(a)).join(" ")),
89
+ info: (...args) => logs.push("[INFO] " + args.map(a => typeof a === "object" ? JSON.stringify(a) : String(a)).join(" ")),
90
+ };
91
+ const context = { config, fetch: globalThis.fetch };
92
+ try {
93
+ const fn = new Function("context", "console", "fetch",
94
+ "return (async () => { " + script + " })();"
95
+ );
96
+ const result = await fn(context, safeConsole, globalThis.fetch);
97
+ self.postMessage({ result, logs });
98
+ } catch (error) {
99
+ self.postMessage({ error: error.message, logs });
100
+ }
101
+ };
102
+ `;
103
+ const blob = new Blob([workerCode], { type: "application/javascript" });
104
+ const workerUrl = URL.createObjectURL(blob);
105
+ const worker = new Worker(workerUrl);
106
+
107
+ const workerResult = await Promise.race([
108
+ new Promise<{ result?: unknown; error?: string; logs: string[] }>((resolve) => {
109
+ worker.addEventListener("message", (event: MessageEvent) => resolve(event.data));
110
+ worker.postMessage({ script, config: context.config });
111
+ }),
91
112
  new Promise<never>((_, reject) =>
92
113
  setTimeout(() => reject(new Error("__TIMEOUT__")), timeoutMs),
93
114
  ),
94
115
  ]);
95
116
 
117
+ worker.terminate();
118
+ URL.revokeObjectURL(workerUrl);
119
+
120
+ // Replay logs into the outer safeConsole
121
+ if (workerResult.logs && Array.isArray(workerResult.logs)) {
122
+ for (const logLine of workerResult.logs) {
123
+ if (logLine.startsWith("[WARN] ")) {
124
+ safeConsole.warn(logLine.slice(7));
125
+ } else if (logLine.startsWith("[ERROR] ")) {
126
+ safeConsole.error(logLine.slice(8));
127
+ } else if (logLine.startsWith("[INFO] ")) {
128
+ safeConsole.info(logLine.slice(7));
129
+ } else {
130
+ safeConsole.log(logLine);
131
+ }
132
+ }
133
+ }
134
+
135
+ if (workerResult.error) {
136
+ throw new Error(workerResult.error);
137
+ }
138
+
139
+ const { result } = workerResult;
140
+
96
141
  // Normalize result
97
142
  if (result === undefined || result === null) {
98
143
  return { result: { success: true }, timedOut: false };
@@ -103,11 +148,12 @@ async function executeInlineScript({
103
148
  }
104
149
 
105
150
  if (typeof result === "object") {
151
+ const resultObj = result as Record<string, unknown>;
106
152
  return {
107
153
  result: {
108
- success: Boolean(result.success ?? true),
109
- message: result.message,
110
- value: result.value,
154
+ success: Boolean(resultObj.success ?? true),
155
+ message: typeof resultObj.message === "string" ? resultObj.message : undefined,
156
+ value: typeof resultObj.value === "number" ? resultObj.value : undefined,
111
157
  },
112
158
  timedOut: false,
113
159
  };
@@ -0,0 +1,30 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { ScriptHealthCheckStrategy } from "./strategy";
3
+
4
+ describe("ScriptHealthCheckStrategy Security", () => {
5
+ it("should not leak sensitive environment variables to child process", async () => {
6
+ // Set a secret in the current process
7
+ process.env.TEST_SECRET_KEY = "SUPER_SECRET_KEY_DO_NOT_LEAK";
8
+
9
+ // Use the default strategy which uses the real Bun.spawn
10
+ const strategy = new ScriptHealthCheckStrategy();
11
+ const connectedClient = await strategy.createClient({ timeout: 5000 });
12
+
13
+ const result = await connectedClient.client.exec({
14
+ command: "env",
15
+ args: [],
16
+ timeout: 5000,
17
+ });
18
+
19
+ connectedClient.close();
20
+
21
+ // Cleanup before assertions to be safe
22
+ delete process.env.TEST_SECRET_KEY;
23
+
24
+ expect(result.exitCode).toBe(0);
25
+ expect(result.stdout).not.toContain("SUPER_SECRET_KEY_DO_NOT_LEAK");
26
+ // Ensure we still have some environment variables
27
+ // PATH is usually present, but let's check for something else generic or just ensure output is not empty
28
+ expect(result.stdout.length).toBeGreaterThan(0);
29
+ });
30
+ });
package/src/strategy.ts CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  z,
14
14
  type ConnectedClient,
15
15
  type InferAggregatedResult,
16
+ baseStrategyConfigSchema,
16
17
  } from "@checkstack/backend-api";
17
18
  import {
18
19
  healthResultBoolean,
@@ -34,13 +35,7 @@ import type {
34
35
  * Configuration schema for Script health checks.
35
36
  * Global defaults only - action params moved to ExecuteCollector.
36
37
  */
37
- export const scriptConfigSchema = z.object({
38
- timeout: z
39
- .number()
40
- .min(100)
41
- .default(30_000)
42
- .describe("Default execution timeout in milliseconds"),
43
- });
38
+ export const scriptConfigSchema = baseStrategyConfigSchema.extend({});
44
39
 
45
40
  export type ScriptConfig = z.infer<typeof scriptConfigSchema>;
46
41
  export type ScriptConfigInput = z.input<typeof scriptConfigSchema>;
@@ -134,6 +129,19 @@ export interface ScriptExecutor {
134
129
  }): Promise<ScriptExecutionResult>;
135
130
  }
136
131
 
132
+ const SAFE_ENV_VARS = [
133
+ "PATH",
134
+ "HOME",
135
+ "USER",
136
+ "LANG",
137
+ "LC_ALL",
138
+ "LC_CTYPE",
139
+ "TZ",
140
+ "TMPDIR",
141
+ "HOSTNAME",
142
+ "SHELL",
143
+ ];
144
+
137
145
  // Default executor using Bun.spawn
138
146
  const defaultScriptExecutor: ScriptExecutor = {
139
147
  async execute(config) {
@@ -148,11 +156,18 @@ const defaultScriptExecutor: ScriptExecutor = {
148
156
  }, config.timeout);
149
157
  });
150
158
 
159
+ const safeEnv: Record<string, string> = {};
160
+ for (const key of SAFE_ENV_VARS) {
161
+ if (process.env[key] !== undefined) {
162
+ safeEnv[key] = process.env[key]!;
163
+ }
164
+ }
165
+
151
166
  try {
152
167
  proc = spawn({
153
168
  cmd: [config.command, ...config.args],
154
169
  cwd: config.cwd,
155
- env: { ...process.env, ...config.env },
170
+ env: { ...safeEnv, ...config.env },
156
171
  stdout: "pipe",
157
172
  stderr: "pipe",
158
173
  });