fix: 修复运行时审查问题
This commit is contained in:
parent
c7c5822f14
commit
7f4ff45938
@ -258,6 +258,19 @@ function applyOperationResponseExamples(
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRuntimeHealthPath(path)) {
|
||||
const plainResponse = buildPlainJsonResponse(
|
||||
runtimeHealthExample(),
|
||||
'API 运行时健康检查',
|
||||
);
|
||||
operation.responses['200'] = mergeJsonResponse(
|
||||
operation.responses['200'],
|
||||
plainResponse,
|
||||
);
|
||||
applyErrorResponses(operation);
|
||||
return;
|
||||
}
|
||||
|
||||
const dataExample = getOperationDataExample(path, method, operation);
|
||||
const successSchema = createOperationSuccessSchema(
|
||||
document,
|
||||
@ -313,6 +326,24 @@ function buildSuccessResponse(dataExample: any, schema: SwaggerSchema) {
|
||||
};
|
||||
}
|
||||
|
||||
function buildPlainJsonResponse(example: any, description: string) {
|
||||
return {
|
||||
description,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: schemaFromExample(example),
|
||||
example,
|
||||
examples: {
|
||||
success: {
|
||||
summary: '成功响应',
|
||||
value: example,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildErrorResponse(status: number, summary: string, message: string) {
|
||||
return {
|
||||
description: message,
|
||||
@ -705,6 +736,10 @@ function isBinaryResponsePath(path: string) {
|
||||
return path.includes('/download') || path.includes('/resource-proxy');
|
||||
}
|
||||
|
||||
function isRuntimeHealthPath(path: string) {
|
||||
return path.toLowerCase() === '/health/runtime';
|
||||
}
|
||||
|
||||
function itemExampleByPath(path: string) {
|
||||
if (path.includes('/qqbot/account')) return qqbotAccountExample();
|
||||
if (path.includes('/qqbot/command')) return qqbotCommandExample();
|
||||
@ -946,6 +981,95 @@ function pluginHealthExample() {
|
||||
};
|
||||
}
|
||||
|
||||
function runtimeHealthExample() {
|
||||
return {
|
||||
service: 'kt-template-online-api',
|
||||
checkedAt: '2026-06-13T00:00:00.000Z',
|
||||
status: 'degraded',
|
||||
checks: [
|
||||
{
|
||||
name: 'process',
|
||||
status: 'live',
|
||||
critical: true,
|
||||
message: 'NestJS process is responding',
|
||||
},
|
||||
{
|
||||
name: 'config:QQBOT_NAPCAT_IMAGE',
|
||||
status: 'degraded',
|
||||
critical: false,
|
||||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function wordpressArticleExample() {
|
||||
return {
|
||||
id: 1,
|
||||
|
||||
@ -51,6 +51,7 @@ const OPTIONAL_CONFIG_CHECKS: ReadonlyArray<string | readonly string[]> = [
|
||||
'QQBOT_REVERSE_WS_PATH',
|
||||
'QQBOT_REVERSE_WS_TOKEN',
|
||||
'QQBOT_NAPCAT_ROOT',
|
||||
'QQBOT_NAPCAT_IMAGE',
|
||||
'QQBOT_NAPCAT_CONTAINER_MODE',
|
||||
'QQBOT_NAPCAT_SSH_TARGET',
|
||||
'QQBOT_NAPCAT_SSH_PORT',
|
||||
@ -152,6 +153,7 @@ export class RuntimeConfigService {
|
||||
'QQBOT_NAPCAT_ROOT',
|
||||
'/vol1/docker/kt-qqbot/napcat-instances',
|
||||
),
|
||||
napcatImage: this.getString('QQBOT_NAPCAT_IMAGE'),
|
||||
napcatContainerMode: this.getString('QQBOT_NAPCAT_CONTAINER_MODE'),
|
||||
napcatSshTarget: this.getString('QQBOT_NAPCAT_SSH_TARGET', 'nas'),
|
||||
napcatSshPort: this.getPositiveNumber('QQBOT_NAPCAT_SSH_PORT', 22),
|
||||
|
||||
@ -55,6 +55,7 @@ export interface RuntimeQqbotConfig {
|
||||
reverseWsPath: string;
|
||||
reverseWsToken: string;
|
||||
napcatRoot: string;
|
||||
napcatImage: string;
|
||||
napcatContainerMode: string;
|
||||
napcatSshTarget: string;
|
||||
napcatSshPort: number;
|
||||
|
||||
@ -5,12 +5,30 @@ import {
|
||||
} from './runtime-evidence.types';
|
||||
|
||||
const REDACTED_VALUE = '<redacted>';
|
||||
const REDACTED_BASE64_VALUE = '<redacted-base64>';
|
||||
const SENSITIVE_KEY_PATTERN =
|
||||
/password|secret|token|authorization|cookie|privateKey|sshKey|ticket|randstr|replyText/i;
|
||||
/password|secret|token|authorization|cookie|privatekey|sshkey|ticket|randstr|replytext|base64/i;
|
||||
const SENSITIVE_TEXT_KEY_PATTERN =
|
||||
'password|secret|token|authorization|cookie|private[_-]?key|ssh[_-]?key|ticket|randstr|replyText|sid';
|
||||
const SENSITIVE_TEXT_REPLACEMENTS: Array<[RegExp, string]> = [
|
||||
[
|
||||
/data:[a-z0-9.+-]+\/[a-z0-9.+-]+;base64,[a-z0-9+/=\r\n]+/gi,
|
||||
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(Cookie)\s*[:=]\s*[^\s,;]+/gi, '$1=<redacted>'],
|
||||
[/\b(ticket|randstr|token|replyText)\s*=\s*[^\s,;&]+/gi, '$1=<redacted>'],
|
||||
[
|
||||
new RegExp(
|
||||
`(["'])(${SENSITIVE_TEXT_KEY_PATTERN})\\1\\s*:\\s*(["'])[^"']*\\3`,
|
||||
'gi',
|
||||
),
|
||||
'$1$2$1:$3<redacted>$3',
|
||||
],
|
||||
[
|
||||
new RegExp(`\\b(${SENSITIVE_TEXT_KEY_PATTERN})\\s*=\\s*[^\\s,;&]+`, 'gi'),
|
||||
'$1=<redacted>',
|
||||
],
|
||||
];
|
||||
|
||||
@Injectable()
|
||||
@ -63,6 +81,7 @@ export class RuntimeEvidenceService {
|
||||
}
|
||||
|
||||
private isSensitiveKey(key: string) {
|
||||
return SENSITIVE_KEY_PATTERN.test(key);
|
||||
const normalizedKey = key.replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
|
||||
return normalizedKey === 'sid' || SENSITIVE_KEY_PATTERN.test(normalizedKey);
|
||||
}
|
||||
}
|
||||
|
||||
49
test/common/swagger-response.spec.ts
Normal file
49
test/common/swagger-response.spec.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import type { OpenAPIObject } from '@nestjs/swagger';
|
||||
import { applySwaggerResponseExamples } from '../../src/common/swagger/swagger-response';
|
||||
|
||||
describe('applySwaggerResponseExamples', () => {
|
||||
it('keeps runtime health swagger response as plain JSON instead of Vben success wrapper', () => {
|
||||
const document = {
|
||||
openapi: '3.0.0',
|
||||
info: {
|
||||
title: 'KT API',
|
||||
version: 'test',
|
||||
},
|
||||
paths: {
|
||||
'/health/runtime': {
|
||||
get: {
|
||||
summary: 'Get machine-readable API runtime health',
|
||||
responses: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenAPIObject;
|
||||
|
||||
applySwaggerResponseExamples(document);
|
||||
|
||||
const operation = (document.paths['/health/runtime'] as any).get;
|
||||
const jsonContent = operation.responses['200'].content['application/json'];
|
||||
|
||||
expect(jsonContent.example).toEqual(
|
||||
expect.objectContaining({
|
||||
service: 'kt-template-online-api',
|
||||
status: 'degraded',
|
||||
checks: expect.any(Array),
|
||||
config: expect.any(Object),
|
||||
}),
|
||||
);
|
||||
expect(jsonContent.example).not.toHaveProperty('code');
|
||||
expect(jsonContent.example).not.toHaveProperty('msg');
|
||||
expect(jsonContent.example).not.toHaveProperty('data');
|
||||
expect(jsonContent.schema).toEqual(
|
||||
expect.objectContaining({
|
||||
type: 'object',
|
||||
properties: expect.objectContaining({
|
||||
service: expect.any(Object),
|
||||
checks: expect.any(Object),
|
||||
config: expect.any(Object),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -99,6 +99,7 @@ describe('RuntimeConfigService', () => {
|
||||
LOKI_USERNAME: 'loki-user',
|
||||
LOKI_PASSWORD: 'loki-password',
|
||||
QQBOT_NAPCAT_ROOT: '/vol1/docker/napcat',
|
||||
QQBOT_NAPCAT_IMAGE: 'mlikiowa/napcat-docker:latest',
|
||||
QQBOT_NAPCAT_CONTAINER_MODE: 'ssh',
|
||||
QQBOT_NAPCAT_SSH_TARGET: 'nas',
|
||||
QQBOT_NAPCAT_SSH_PORT: '2202',
|
||||
@ -134,6 +135,7 @@ describe('RuntimeConfigService', () => {
|
||||
reverseWsPath: '/qqbot/reverse',
|
||||
reverseWsToken: 'qq***en',
|
||||
napcatRoot: '/vol1/docker/napcat',
|
||||
napcatImage: 'mlikiowa/napcat-docker:latest',
|
||||
napcatContainerMode: 'ssh',
|
||||
napcatSshTarget: 'nas',
|
||||
napcatSshPort: 2202,
|
||||
@ -151,6 +153,13 @@ describe('RuntimeConfigService', () => {
|
||||
present: true,
|
||||
}),
|
||||
);
|
||||
expect(checks).toContainEqual(
|
||||
expect.objectContaining({
|
||||
key: 'QQBOT_NAPCAT_IMAGE',
|
||||
level: 'optional',
|
||||
present: true,
|
||||
}),
|
||||
);
|
||||
expect(checks).toContainEqual(
|
||||
expect.objectContaining({
|
||||
key: 'QQBOT_NAPCAT_REVERSE_WS_URL|QQBOT_NAPCAT_REVERSE_WS_BASE',
|
||||
|
||||
@ -179,4 +179,41 @@ describe('RuntimeEvidenceService', () => {
|
||||
expect(input.assertions[0].message).toContain('raw-reply-text');
|
||||
expect(input.cleanup.message).toContain('raw-cookie');
|
||||
});
|
||||
|
||||
it('redacts captcha sid, snake_case secret keys, JSON secret text, and base64 payloads', () => {
|
||||
const service = new RuntimeEvidenceService();
|
||||
const rawBase64Payload = `data:image/png;base64,${'A'.repeat(160)}`;
|
||||
|
||||
const record = service.createRecord({
|
||||
title: 'captcha evidence',
|
||||
taskType: 'api',
|
||||
project: 'kt-template-online-api',
|
||||
environment: 'local',
|
||||
operation: 'qqbot-login',
|
||||
status: 'blocked',
|
||||
details: {
|
||||
sid: 'raw-captcha-sid',
|
||||
private_key: 'raw-private-key',
|
||||
ssh_key: 'raw-ssh-key',
|
||||
imageBase64: rawBase64Payload,
|
||||
text: 'sid=raw-text-sid private_key=raw-text-private-key ssh_key=raw-text-ssh-key',
|
||||
jsonText:
|
||||
'{"sid":"raw-json-sid","token":"raw-json-token","safe":"kept"}',
|
||||
},
|
||||
});
|
||||
|
||||
const serialized = JSON.stringify(record);
|
||||
|
||||
expect(serialized).not.toContain('raw-captcha-sid');
|
||||
expect(serialized).not.toContain('raw-private-key');
|
||||
expect(serialized).not.toContain('raw-ssh-key');
|
||||
expect(serialized).not.toContain('raw-text-sid');
|
||||
expect(serialized).not.toContain('raw-text-private-key');
|
||||
expect(serialized).not.toContain('raw-text-ssh-key');
|
||||
expect(serialized).not.toContain('raw-json-sid');
|
||||
expect(serialized).not.toContain('raw-json-token');
|
||||
expect(serialized).not.toContain(rawBase64Payload);
|
||||
expect(serialized).toContain('captcha evidence');
|
||||
expect(serialized).toContain('kept');
|
||||
});
|
||||
});
|
||||
|
||||
@ -48,6 +48,7 @@ describe('RuntimeHealthController', () => {
|
||||
reverseWsPath: '/qqbot/onebot/reverse',
|
||||
reverseWsToken: 'qq***en',
|
||||
napcatRoot: '/vol1/docker/napcat',
|
||||
napcatImage: 'mlikiowa/napcat-docker:latest',
|
||||
napcatContainerMode: 'ssh',
|
||||
napcatSshTarget: 'nas',
|
||||
napcatSshPort: 2202,
|
||||
|
||||
@ -44,6 +44,7 @@ function createSnapshot(
|
||||
reverseWsPath: '/qqbot/onebot/reverse',
|
||||
reverseWsToken: 'qq***en',
|
||||
napcatRoot: '/vol1/docker/napcat',
|
||||
napcatImage: 'mlikiowa/napcat-docker:latest',
|
||||
napcatContainerMode: 'ssh',
|
||||
napcatSshTarget: 'nas',
|
||||
napcatSshPort: 2202,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user