fix: 加强运行时证据脱敏
This commit is contained in:
parent
0892fd8048
commit
ce20c350cd
@ -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 = '<redacted>';
|
||||
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=<redacted>'],
|
||||
[/\b(Cookie)\s*[:=]\s*[^\s,;]+/gi, '$1=<redacted>'],
|
||||
[/\b(ticket|randstr|token|replyText)\s*=\s*[^\s,;&]+/gi, '$1=<redacted>'],
|
||||
];
|
||||
|
||||
@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<T extends Record<string, unknown> | 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<string, unknown>).map(([key, item]) => [
|
||||
Object.entries(value).map(([key, item]) => [
|
||||
key,
|
||||
this.isSensitiveKey(key) ? '<redacted>' : 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<string, unknown> {
|
||||
return (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
!Array.isArray(value) &&
|
||||
!(value instanceof Date)
|
||||
);
|
||||
}
|
||||
|
||||
private isSensitiveKey(key: string) {
|
||||
return SENSITIVE_KEY_PATTERN.test(key);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@ -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 = '<redacted>';
|
||||
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=<redacted>'],
|
||||
[/\b(Cookie)\s*[:=]\s*[^\s,;]+/gi, '$1=<redacted>'],
|
||||
[/\b(ticket|randstr|token|replyText)\s*=\s*[^\s,;&]+/gi, '$1=<redacted>'],
|
||||
];
|
||||
|
||||
@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<string, unknown>,
|
||||
): Record<string, unknown> | undefined {
|
||||
if (!value) return undefined;
|
||||
return this.sanitizeValue(value) as Record<string, unknown>;
|
||||
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<string, unknown> {
|
||||
return (
|
||||
typeof value === 'object' &&
|
||||
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user