@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 +44 -0
- package/package.json +4 -4
- package/src/inline-script-collector.ts +60 -14
- package/src/security.test.ts +30 -0
- package/src/strategy.ts +23 -8
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.
|
|
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.
|
|
13
|
-
"@checkstack/common": "0.6.
|
|
14
|
-
"@checkstack/healthcheck-common": "0.8.
|
|
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
|
-
//
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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(
|
|
109
|
-
message:
|
|
110
|
-
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 =
|
|
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: { ...
|
|
170
|
+
env: { ...safeEnv, ...config.env },
|
|
156
171
|
stdout: "pipe",
|
|
157
172
|
stderr: "pipe",
|
|
158
173
|
});
|