fix: 收敛运行时健康公开响应
This commit is contained in:
parent
340cb073be
commit
e480c0edcb
5
API.md
5
API.md
@ -54,7 +54,7 @@ Admin、Component、Dict、MinIO、Blog 管理、WordPress 管理和 QQBot 管
|
|||||||
|
|
||||||
| 方法 | 路径 | 认证 | 说明 |
|
| 方法 | 路径 | 认证 | 说明 |
|
||||||
| ----- | ----------------- | ---- | ---------------------------------- |
|
| ----- | ----------------- | ---- | ---------------------------------- |
|
||||||
| `GET` | `/health/runtime` | 否 | API 运行时健康和脱敏配置快照 |
|
| `GET` | `/health/runtime` | 否 | API 运行时健康和配置检查状态 |
|
||||||
|
|
||||||
该接口返回 plain JSON,不使用 Vben 响应包装,供本地 smoke、Jenkins/K8s 和 ktWorkflow 观测脚本直接读取。接口位于 Swagger 基础能力分组 `/api/basic`。
|
该接口返回 plain JSON,不使用 Vben 响应包装,供本地 smoke、Jenkins/K8s 和 ktWorkflow 观测脚本直接读取。接口位于 Swagger 基础能力分组 `/api/basic`。
|
||||||
|
|
||||||
@ -66,7 +66,8 @@ Admin、Component、Dict、MinIO、Blog 管理、WordPress 管理和 QQBot 管
|
|||||||
| `checkedAt` | ISO 时间字符串 |
|
| `checkedAt` | ISO 时间字符串 |
|
||||||
| `status` | `live`、`ready`、`degraded` 或 `blocked` |
|
| `status` | `live`、`ready`、`degraded` 或 `blocked` |
|
||||||
| `checks` | 进程和配置检查列表 |
|
| `checks` | 进程和配置检查列表 |
|
||||||
| `config` | 脱敏后的运行时配置快照,不包含原始 secret/token |
|
|
||||||
|
公开响应不返回数据库、WordPress、Loki、NapCat SSH 等运行拓扑配置快照;配置检查只暴露 key 级别、是否存在和缺失说明。
|
||||||
|
|
||||||
状态含义:
|
状态含义:
|
||||||
|
|
||||||
|
|||||||
@ -128,10 +128,9 @@ API 暴露 `GET /health/runtime` 作为本地 smoke、Jenkins/K8s 和 ktWorkflow
|
|||||||
返回内容包括:
|
返回内容包括:
|
||||||
|
|
||||||
- `status`:`live`、`ready`、`degraded` 或 `blocked`。
|
- `status`:`live`、`ready`、`degraded` 或 `blocked`。
|
||||||
- `checks`:进程存活和运行时配置检查。
|
- `checks`:进程存活和运行时配置检查状态。
|
||||||
- `config`:已脱敏的运行时配置快照,不包含原始密码、Token、Cookie、SSH key 或验证码票据。
|
|
||||||
|
|
||||||
`blocked` 表示关键配置缺失;`degraded` 表示可选运行时配置缺失,核心 API 仍可继续工作。本地未配置 Loki、WordPress、NapCat 等可选依赖时,健康状态可能保持 `degraded`。
|
该公开入口不返回数据库、WordPress、Loki、NapCat SSH 等运行拓扑配置快照;配置检查只暴露 key 级别、是否存在和缺失说明。`blocked` 表示关键配置缺失;`degraded` 表示可选运行时配置缺失,核心 API 仍可继续工作。本地未配置 Loki、WordPress、NapCat 等可选依赖时,健康状态可能保持 `degraded`。
|
||||||
|
|
||||||
## 核心规则
|
## 核心规则
|
||||||
|
|
||||||
|
|||||||
@ -1000,73 +1000,6 @@ function runtimeHealthExample() {
|
|||||||
message: 'QQBOT_NAPCAT_IMAGE is not configured',
|
message: 'QQBOT_NAPCAT_IMAGE is not configured',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
config: {
|
|
||||||
app: {
|
|
||||||
nodeEnv: 'production',
|
|
||||||
port: 48085,
|
|
||||||
},
|
|
||||||
database: {
|
|
||||||
host: 'mysql',
|
|
||||||
port: 3306,
|
|
||||||
database: 'kt_template',
|
|
||||||
username: 'kt',
|
|
||||||
synchronize: false,
|
|
||||||
},
|
|
||||||
loki: {
|
|
||||||
transportEnabled: false,
|
|
||||||
httpRequestPushEnabled: false,
|
|
||||||
queryConfigured: false,
|
|
||||||
host: '',
|
|
||||||
queryHost: '',
|
|
||||||
environment: 'production',
|
|
||||||
tenantId: '',
|
|
||||||
username: '',
|
|
||||||
passwordConfigured: false,
|
|
||||||
},
|
|
||||||
minio: {
|
|
||||||
endpoint: 'minio',
|
|
||||||
port: 9000,
|
|
||||||
useSSL: false,
|
|
||||||
accessKey: 'mi***ey',
|
|
||||||
bucket: 'kt-template-online',
|
|
||||||
},
|
|
||||||
wordpress: {
|
|
||||||
baseUrl: 'https://blog.example.test',
|
|
||||||
hostHeader: 'blog.example.test',
|
|
||||||
adminUsername: 'wordpress-admin',
|
|
||||||
passwordConfigured: true,
|
|
||||||
timeoutMs: 15000,
|
|
||||||
loginTimeoutMs: 3000,
|
|
||||||
availabilityTtlMs: 60000,
|
|
||||||
},
|
|
||||||
qqbot: {
|
|
||||||
reverseWsPath: '/qqbot/onebot/reverse',
|
|
||||||
reverseWsToken: 'qq***en',
|
|
||||||
napcatRoot: '/vol1/docker/kt-qqbot/napcat-instances',
|
|
||||||
napcatImage: '',
|
|
||||||
napcatContainerMode: 'ssh',
|
|
||||||
napcatSshTarget: 'nas',
|
|
||||||
napcatSshPort: 2202,
|
|
||||||
napcatSshKeyPath: '/home/kt/.ssh/napcat',
|
|
||||||
napcatReverseWsBase: 'ws://api.example.test/qqbot/onebot/reverse',
|
|
||||||
napcatWebuiBaseUrl: 'http://127.0.0.1:6099',
|
|
||||||
napcatWebuiToken: 'na***en',
|
|
||||||
},
|
|
||||||
checks: [
|
|
||||||
{
|
|
||||||
key: 'DB_HOST',
|
|
||||||
level: 'required',
|
|
||||||
present: true,
|
|
||||||
maskedValue: 'my***ql',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'QQBOT_NAPCAT_IMAGE',
|
|
||||||
level: 'optional',
|
|
||||||
present: false,
|
|
||||||
message: 'QQBOT_NAPCAT_IMAGE is not configured',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -16,14 +16,21 @@ const SENSITIVE_TEXT_REPLACEMENTS: Array<[RegExp, string]> = [
|
|||||||
REDACTED_BASE64_VALUE,
|
REDACTED_BASE64_VALUE,
|
||||||
],
|
],
|
||||||
[/\b[A-Za-z0-9+/]{120,}={0,2}\b/g, REDACTED_BASE64_VALUE],
|
[/\b[A-Za-z0-9+/]{120,}={0,2}\b/g, REDACTED_BASE64_VALUE],
|
||||||
[/\b(Authorization)\s*[:=]\s*Bearer\s+[^\s,;]+/gi, '$1=<redacted>'],
|
[/\b(Authorization)\s*[:=]\s*[^\r\n]+/gi, '$1=<redacted>'],
|
||||||
[/\b(Cookie)\s*[:=]\s*[^\s,;]+/gi, '$1=<redacted>'],
|
[/\b(Cookie)\s*[:=]\s*[^\r\n]+/gi, '$1=<redacted>'],
|
||||||
[
|
[
|
||||||
new RegExp(
|
new RegExp(
|
||||||
`(["'])(${SENSITIVE_TEXT_KEY_PATTERN})\\1\\s*:\\s*(["'])[^"']*\\3`,
|
`(["'])(${SENSITIVE_TEXT_KEY_PATTERN})\\1\\s*:\\s*(?:(["'])[^"']*\\3|[-+]?\\d+(?:\\.\\d+)?|true|false|null)`,
|
||||||
'gi',
|
'gi',
|
||||||
),
|
),
|
||||||
'$1$2$1:$3<redacted>$3',
|
'$1$2$1:"<redacted>"',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
new RegExp(
|
||||||
|
`\\b(${SENSITIVE_TEXT_KEY_PATTERN})(\\s*[:=]\\s*)(["'])[^"']*\\3`,
|
||||||
|
'gi',
|
||||||
|
),
|
||||||
|
'$1$2$3<redacted>$3',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
new RegExp(
|
new RegExp(
|
||||||
|
|||||||
@ -34,7 +34,6 @@ export class RuntimeHealthService {
|
|||||||
checkedAt: new Date().toISOString(),
|
checkedAt: new Date().toISOString(),
|
||||||
status: this.aggregateStatus(checks),
|
status: this.aggregateStatus(checks),
|
||||||
checks,
|
checks,
|
||||||
config,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
import type { RuntimeSafeConfigSnapshot } from '../config/runtime-config.types';
|
|
||||||
|
|
||||||
export type RuntimeHealthStatus = 'live' | 'ready' | 'degraded' | 'blocked';
|
export type RuntimeHealthStatus = 'live' | 'ready' | 'degraded' | 'blocked';
|
||||||
|
|
||||||
export interface RuntimeHealthCheck {
|
export interface RuntimeHealthCheck {
|
||||||
@ -15,5 +13,4 @@ export interface RuntimeHealthReport {
|
|||||||
checkedAt: string;
|
checkedAt: string;
|
||||||
status: RuntimeHealthStatus;
|
status: RuntimeHealthStatus;
|
||||||
checks: RuntimeHealthCheck[];
|
checks: RuntimeHealthCheck[];
|
||||||
config: RuntimeSafeConfigSnapshot;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,19 +29,18 @@ describe('applySwaggerResponseExamples', () => {
|
|||||||
service: 'kt-template-online-api',
|
service: 'kt-template-online-api',
|
||||||
status: 'degraded',
|
status: 'degraded',
|
||||||
checks: expect.any(Array),
|
checks: expect.any(Array),
|
||||||
config: expect.any(Object),
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(jsonContent.example).not.toHaveProperty('code');
|
expect(jsonContent.example).not.toHaveProperty('code');
|
||||||
expect(jsonContent.example).not.toHaveProperty('msg');
|
expect(jsonContent.example).not.toHaveProperty('msg');
|
||||||
expect(jsonContent.example).not.toHaveProperty('data');
|
expect(jsonContent.example).not.toHaveProperty('data');
|
||||||
|
expect(jsonContent.example).not.toHaveProperty('config');
|
||||||
expect(jsonContent.schema).toEqual(
|
expect(jsonContent.schema).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: expect.objectContaining({
|
properties: expect.objectContaining({
|
||||||
service: expect.any(Object),
|
service: expect.any(Object),
|
||||||
checks: expect.any(Object),
|
checks: expect.any(Object),
|
||||||
config: expect.any(Object),
|
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -249,4 +249,34 @@ describe('RuntimeEvidenceService', () => {
|
|||||||
expect(serialized).not.toContain('raw-json-client-secret');
|
expect(serialized).not.toContain('raw-json-client-secret');
|
||||||
expect(serialized).toContain('KT_SCAN_SAFE');
|
expect(serialized).toContain('KT_SCAN_SAFE');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('redacts cookie headers, quoted private key values, and non-string JSON secrets', () => {
|
||||||
|
const service = new RuntimeEvidenceService();
|
||||||
|
|
||||||
|
const record = service.createRecord({
|
||||||
|
title: 'realistic secret text evidence',
|
||||||
|
taskType: 'api',
|
||||||
|
project: 'kt-template-online-api',
|
||||||
|
environment: 'local',
|
||||||
|
operation: 'runtime-evidence',
|
||||||
|
status: 'failed',
|
||||||
|
details: {
|
||||||
|
cookieHeader:
|
||||||
|
'Cookie: admin_access_token=raw-cookie-token; wordpress_logged_in=raw-wordpress-cookie; theme=light',
|
||||||
|
quotedPrivateKey:
|
||||||
|
'private_key="-----BEGIN PRIVATE KEY----- raw pem body -----END PRIVATE KEY-----"',
|
||||||
|
jsonText:
|
||||||
|
'{"accessToken":12345,"sid":67890,"client_secret":true,"sessionId":"KT_SCAN_SAFE"}',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const serialized = JSON.stringify(record);
|
||||||
|
|
||||||
|
expect(serialized).not.toContain('raw-cookie-token');
|
||||||
|
expect(serialized).not.toContain('raw-wordpress-cookie');
|
||||||
|
expect(serialized).not.toContain('raw pem body');
|
||||||
|
expect(serialized).not.toContain('12345');
|
||||||
|
expect(serialized).not.toContain('67890');
|
||||||
|
expect(serialized).toContain('KT_SCAN_SAFE');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -8,57 +8,6 @@ describe('RuntimeHealthController', () => {
|
|||||||
checkedAt: '2026-06-13T00:00:00.000Z',
|
checkedAt: '2026-06-13T00:00:00.000Z',
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
checks: [],
|
checks: [],
|
||||||
config: {
|
|
||||||
app: { nodeEnv: 'test', port: 48085 },
|
|
||||||
database: {
|
|
||||||
host: 'mysql',
|
|
||||||
port: 3306,
|
|
||||||
database: 'kt',
|
|
||||||
username: 'root',
|
|
||||||
synchronize: false,
|
|
||||||
},
|
|
||||||
loki: {
|
|
||||||
transportEnabled: false,
|
|
||||||
httpRequestPushEnabled: false,
|
|
||||||
queryConfigured: false,
|
|
||||||
host: '',
|
|
||||||
queryHost: '',
|
|
||||||
environment: 'test',
|
|
||||||
tenantId: '',
|
|
||||||
username: '',
|
|
||||||
passwordConfigured: false,
|
|
||||||
},
|
|
||||||
minio: {
|
|
||||||
endpoint: 'minio',
|
|
||||||
port: 9000,
|
|
||||||
useSSL: false,
|
|
||||||
accessKey: 'mi***ey',
|
|
||||||
bucket: 'kt-template-online',
|
|
||||||
},
|
|
||||||
wordpress: {
|
|
||||||
baseUrl: 'https://blog.example.test',
|
|
||||||
hostHeader: 'blog.example.test',
|
|
||||||
adminUsername: 'wordpress-admin',
|
|
||||||
passwordConfigured: true,
|
|
||||||
timeoutMs: 15000,
|
|
||||||
loginTimeoutMs: 3000,
|
|
||||||
availabilityTtlMs: 60000,
|
|
||||||
},
|
|
||||||
qqbot: {
|
|
||||||
reverseWsPath: '/qqbot/onebot/reverse',
|
|
||||||
reverseWsToken: 'qq***en',
|
|
||||||
napcatRoot: '/vol1/docker/napcat',
|
|
||||||
napcatImage: 'mlikiowa/napcat-docker:latest',
|
|
||||||
napcatContainerMode: 'ssh',
|
|
||||||
napcatSshTarget: 'nas',
|
|
||||||
napcatSshPort: 2202,
|
|
||||||
napcatSshKeyPath: '/home/kt/.ssh/napcat',
|
|
||||||
napcatReverseWsBase: 'ws://api.example.test/onebot',
|
|
||||||
napcatWebuiBaseUrl: 'http://127.0.0.1:6099',
|
|
||||||
napcatWebuiToken: 'na***en',
|
|
||||||
},
|
|
||||||
checks: [],
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
const service = { getRuntimeHealth: jest.fn(() => report) };
|
const service = { getRuntimeHealth: jest.fn(() => report) };
|
||||||
const controller = new RuntimeHealthController(service as any);
|
const controller = new RuntimeHealthController(service as any);
|
||||||
|
|||||||
@ -78,11 +78,11 @@ describe('RuntimeHealthService', () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
service: 'kt-template-online-api',
|
service: 'kt-template-online-api',
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
config: expect.objectContaining({
|
|
||||||
minio: expect.objectContaining({ bucket: 'kt-template-online' }),
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
expect(report).not.toHaveProperty('config');
|
||||||
|
expect(JSON.stringify(report)).not.toContain('mysql');
|
||||||
|
expect(JSON.stringify(report)).not.toContain('/vol1/docker/napcat');
|
||||||
expect(new Date(report.checkedAt).toISOString()).toBe(report.checkedAt);
|
expect(new Date(report.checkedAt).toISOString()).toBe(report.checkedAt);
|
||||||
expect(report.checks).toContainEqual({
|
expect(report.checks).toContainEqual({
|
||||||
name: 'process',
|
name: 'process',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user