From ce20c350cde08b52c57fa006f70c47b3c024e480 Mon Sep 17 00:00:00 2001 From: sunlei Date: Sat, 13 Jun 2026 17:31:58 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=8A=A0=E5=BC=BA=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E6=97=B6=E8=AF=81=E6=8D=AE=E8=84=B1=E6=95=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-06-13-api-runtime-foundation.md | 63 +++++++++-------- .../evidence/runtime-evidence.service.ts | 37 ++++------ test/runtime/runtime-evidence.service.spec.ts | 70 +++++++++++++++++++ 3 files changed, 118 insertions(+), 52 deletions(-) diff --git a/docs/superpowers/plans/2026-06-13-api-runtime-foundation.md b/docs/superpowers/plans/2026-06-13-api-runtime-foundation.md index ecc9443..f6d6c3d 100644 --- a/docs/superpowers/plans/2026-06-13-api-runtime-foundation.md +++ b/docs/superpowers/plans/2026-06-13-api-runtime-foundation.md @@ -666,69 +666,72 @@ export interface RuntimeEvidenceRecord extends RuntimeEvidenceInput { ```ts import { Injectable } from '@nestjs/common'; import { - RuntimeEvidenceCleanupResult, RuntimeEvidenceInput, RuntimeEvidenceRecord, } from './runtime-evidence.types'; +const REDACTED_VALUE = ''; +const SENSITIVE_KEY_PATTERN = + /password|secret|token|authorization|cookie|privateKey|sshKey|ticket|randstr|replyText/i; +const SENSITIVE_TEXT_REPLACEMENTS: Array<[RegExp, string]> = [ + [/\b(Authorization)\s*[:=]\s*Bearer\s+[^\s,;]+/gi, '$1='], + [/\b(Cookie)\s*[:=]\s*[^\s,;]+/gi, '$1='], + [/\b(ticket|randstr|token|replyText)\s*=\s*[^\s,;&]+/gi, '$1='], +]; + @Injectable() export class RuntimeEvidenceService { createRecord(input: RuntimeEvidenceInput): RuntimeEvidenceRecord { const startedAt = input.startedAt ?? new Date(); const endedAt = input.endedAt ?? new Date(); - - return { + const record: RuntimeEvidenceRecord = { ...input, - details: this.sanitizeRecord(input.details), - cleanup: this.sanitizeCleanup(input.cleanup), startedAt, endedAt, durationMs: Math.max(0, endedAt.getTime() - startedAt.getTime()), schemaVersion: 1, }; - } - sanitizeRecord | undefined>(value: T): T { - if (!value) return value; - return this.sanitizeValue(value) as T; - } - - private sanitizeCleanup( - cleanup: RuntimeEvidenceCleanupResult | undefined, - ): RuntimeEvidenceCleanupResult | undefined { - if (!cleanup) return undefined; - return { - ...cleanup, - details: this.sanitizeRecord(cleanup.details), - }; + return this.sanitizeValue(record) as RuntimeEvidenceRecord; } private sanitizeValue(value: unknown): unknown { + if (value instanceof Date) return value; + if (typeof value === 'string') return this.sanitizeText(value); if (Array.isArray(value)) { return value.map((item) => this.sanitizeValue(item)); } - - if (value instanceof Date) { - return value; - } - - if (!value || typeof value !== 'object') { + if (!this.isPlainRecord(value)) { return value; } return Object.fromEntries( - Object.entries(value as Record).map(([key, item]) => [ + Object.entries(value).map(([key, item]) => [ key, - this.isSensitiveKey(key) ? '' : this.sanitizeValue(item), + this.isSensitiveKey(key) ? REDACTED_VALUE : this.sanitizeValue(item), ]), ); } - private isSensitiveKey(key: string) { - return /password|secret|token|authorization|cookie|privateKey|sshKey|ticket|randstr|replyText/i.test( - key, + private sanitizeText(value: string) { + return SENSITIVE_TEXT_REPLACEMENTS.reduce( + (text, [pattern, replacement]) => text.replace(pattern, replacement), + value, ); } + + private isPlainRecord(value: unknown): value is Record { + return ( + typeof value === 'object' && + value !== null && + !Array.isArray(value) && + !(value instanceof Date) + ); + } + + private isSensitiveKey(key: string) { + return SENSITIVE_KEY_PATTERN.test(key); + } } ``` diff --git a/src/runtime/evidence/runtime-evidence.service.ts b/src/runtime/evidence/runtime-evidence.service.ts index a02ce74..fa3046d 100644 --- a/src/runtime/evidence/runtime-evidence.service.ts +++ b/src/runtime/evidence/runtime-evidence.service.ts @@ -1,6 +1,5 @@ import { Injectable } from '@nestjs/common'; import { - RuntimeEvidenceCleanupResult, RuntimeEvidenceInput, RuntimeEvidenceRecord, } from './runtime-evidence.types'; @@ -8,44 +7,31 @@ import { const REDACTED_VALUE = ''; const SENSITIVE_KEY_PATTERN = /password|secret|token|authorization|cookie|privateKey|sshKey|ticket|randstr|replyText/i; +const SENSITIVE_TEXT_REPLACEMENTS: Array<[RegExp, string]> = [ + [/\b(Authorization)\s*[:=]\s*Bearer\s+[^\s,;]+/gi, '$1='], + [/\b(Cookie)\s*[:=]\s*[^\s,;]+/gi, '$1='], + [/\b(ticket|randstr|token|replyText)\s*=\s*[^\s,;&]+/gi, '$1='], +]; @Injectable() export class RuntimeEvidenceService { createRecord(input: RuntimeEvidenceInput): RuntimeEvidenceRecord { const startedAt = input.startedAt ?? new Date(); const endedAt = input.endedAt ?? new Date(); - - return { + const record: RuntimeEvidenceRecord = { ...input, startedAt, endedAt, durationMs: Math.max(0, endedAt.getTime() - startedAt.getTime()), schemaVersion: 1, - details: this.sanitizeRecord(input.details), - cleanup: this.sanitizeCleanup(input.cleanup), }; - } - private sanitizeCleanup( - cleanup?: RuntimeEvidenceCleanupResult, - ): RuntimeEvidenceCleanupResult | undefined { - if (!cleanup) return undefined; - - return { - ...cleanup, - details: this.sanitizeRecord(cleanup.details), - }; - } - - private sanitizeRecord( - value?: Record, - ): Record | undefined { - if (!value) return undefined; - return this.sanitizeValue(value) as Record; + return this.sanitizeValue(record) as RuntimeEvidenceRecord; } private sanitizeValue(value: unknown): unknown { if (value instanceof Date) return value; + if (typeof value === 'string') return this.sanitizeText(value); if (Array.isArray(value)) { return value.map((item) => this.sanitizeValue(item)); } @@ -60,6 +46,13 @@ export class RuntimeEvidenceService { return value; } + private sanitizeText(value: string) { + return SENSITIVE_TEXT_REPLACEMENTS.reduce( + (text, [pattern, replacement]) => text.replace(pattern, replacement), + value, + ); + } + private isPlainRecord(value: unknown): value is Record { return ( typeof value === 'object' && diff --git a/test/runtime/runtime-evidence.service.spec.ts b/test/runtime/runtime-evidence.service.spec.ts index 0ab9dd7..2f97a00 100644 --- a/test/runtime/runtime-evidence.service.spec.ts +++ b/test/runtime/runtime-evidence.service.spec.ts @@ -23,6 +23,25 @@ describe('RuntimeEvidenceService', () => { expect(record.schemaVersion).toBe(1); }); + it('keeps duration non-negative when evidence ends before it starts', () => { + const service = new RuntimeEvidenceService(); + const startedAt = new Date('2026-06-13T00:00:01.250Z'); + const endedAt = new Date('2026-06-13T00:00:00.000Z'); + + const record = service.createRecord({ + title: 'runtime config smoke', + taskType: 'backend', + project: 'kt-template-online-api', + environment: 'local', + operation: 'runtime-config', + status: 'failed', + startedAt, + endedAt, + }); + + expect(record.durationMs).toBe(0); + }); + it('redacts nested sensitive fields in details and cleanup details', () => { const service = new RuntimeEvidenceService(); @@ -109,4 +128,55 @@ describe('RuntimeEvidenceService', () => { }, }); }); + + it('sanitizes sensitive key-value text across the whole evidence record without mutating input', () => { + const service = new RuntimeEvidenceService(); + const input = { + title: 'safe title token=raw-token', + taskType: 'api', + project: 'kt-template-online-api', + environment: 'local', + operation: 'qqbot-login', + target: 'account safe text Authorization=Bearer raw-token', + status: 'failed' as const, + error: { + category: 'operation_failed' as const, + operation: 'captcha', + message: 'safe error message ticket=raw-ticket', + cause: + 'safe cause randstr=raw-randstr Authorization: Bearer raw-token Cookie=session=raw-cookie', + retryable: false, + }, + assertions: [ + { + name: 'reply text check', + passed: false, + message: 'safe assertion replyText=raw-reply-text', + }, + ], + cleanup: { + status: 'failed' as const, + message: 'safe cleanup Cookie=session=raw-cookie', + }, + }; + + const record = service.createRecord(input); + const serialized = JSON.stringify(record); + + expect(serialized).not.toContain('raw-ticket'); + expect(serialized).not.toContain('raw-randstr'); + expect(serialized).not.toContain('raw-token'); + expect(serialized).not.toContain('raw-cookie'); + expect(serialized).not.toContain('raw-reply-text'); + expect(serialized).toContain('safe title'); + expect(serialized).toContain('safe error message'); + expect(serialized).toContain('safe cause'); + expect(serialized).toContain('safe assertion'); + expect(serialized).toContain('safe cleanup'); + expect(input.title).toContain('raw-token'); + expect(input.error.message).toContain('raw-ticket'); + expect(input.error.cause).toContain('raw-randstr'); + expect(input.assertions[0].message).toContain('raw-reply-text'); + expect(input.cleanup.message).toContain('raw-cookie'); + }); });