feat: 添加API运行时证据模型
This commit is contained in:
parent
7e2c153c01
commit
0892fd8048
75
src/runtime/evidence/runtime-evidence.service.ts
Normal file
75
src/runtime/evidence/runtime-evidence.service.ts
Normal file
@ -0,0 +1,75 @@
|
||||
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;
|
||||
|
||||
@Injectable()
|
||||
export class RuntimeEvidenceService {
|
||||
createRecord(input: RuntimeEvidenceInput): RuntimeEvidenceRecord {
|
||||
const startedAt = input.startedAt ?? new Date();
|
||||
const endedAt = input.endedAt ?? new Date();
|
||||
|
||||
return {
|
||||
...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>;
|
||||
}
|
||||
|
||||
private sanitizeValue(value: unknown): unknown {
|
||||
if (value instanceof Date) return value;
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => this.sanitizeValue(item));
|
||||
}
|
||||
if (this.isPlainRecord(value)) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([key, entry]) => [
|
||||
key,
|
||||
this.isSensitiveKey(key) ? REDACTED_VALUE : this.sanitizeValue(entry),
|
||||
]),
|
||||
);
|
||||
}
|
||||
return 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);
|
||||
}
|
||||
}
|
||||
42
src/runtime/evidence/runtime-evidence.types.ts
Normal file
42
src/runtime/evidence/runtime-evidence.types.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import type { RuntimeClassifiedError } from '../errors/runtime-error.types';
|
||||
|
||||
export type RuntimeEvidenceStatus =
|
||||
| 'passed'
|
||||
| 'failed'
|
||||
| 'blocked'
|
||||
| 'skipped';
|
||||
|
||||
export interface RuntimeEvidenceCleanupResult {
|
||||
status: RuntimeEvidenceStatus;
|
||||
message: string;
|
||||
details?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface RuntimeEvidenceAssertion {
|
||||
name: string;
|
||||
passed: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface RuntimeEvidenceInput {
|
||||
title: string;
|
||||
taskType: string;
|
||||
project: string;
|
||||
environment: string;
|
||||
operation: string;
|
||||
target?: string;
|
||||
status: RuntimeEvidenceStatus;
|
||||
startedAt?: Date;
|
||||
endedAt?: Date;
|
||||
details?: Record<string, unknown>;
|
||||
assertions?: RuntimeEvidenceAssertion[];
|
||||
cleanup?: RuntimeEvidenceCleanupResult;
|
||||
error?: RuntimeClassifiedError;
|
||||
}
|
||||
|
||||
export interface RuntimeEvidenceRecord extends RuntimeEvidenceInput {
|
||||
startedAt: Date;
|
||||
endedAt: Date;
|
||||
durationMs: number;
|
||||
schemaVersion: 1;
|
||||
}
|
||||
112
test/runtime/runtime-evidence.service.spec.ts
Normal file
112
test/runtime/runtime-evidence.service.spec.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import { RuntimeEvidenceService } from '../../src/runtime/evidence/runtime-evidence.service';
|
||||
|
||||
describe('RuntimeEvidenceService', () => {
|
||||
it('creates records with explicit timing, duration, and schema version', () => {
|
||||
const service = new RuntimeEvidenceService();
|
||||
const startedAt = new Date('2026-06-13T00:00:00.000Z');
|
||||
const endedAt = new Date('2026-06-13T00:00:01.250Z');
|
||||
|
||||
const record = service.createRecord({
|
||||
title: 'runtime config smoke',
|
||||
taskType: 'backend',
|
||||
project: 'kt-template-online-api',
|
||||
environment: 'local',
|
||||
operation: 'runtime-config',
|
||||
status: 'passed',
|
||||
startedAt,
|
||||
endedAt,
|
||||
});
|
||||
|
||||
expect(record.startedAt).toBe(startedAt);
|
||||
expect(record.endedAt).toBe(endedAt);
|
||||
expect(record.durationMs).toBe(1250);
|
||||
expect(record.schemaVersion).toBe(1);
|
||||
});
|
||||
|
||||
it('redacts nested sensitive fields in details and cleanup details', () => {
|
||||
const service = new RuntimeEvidenceService();
|
||||
|
||||
const record = service.createRecord({
|
||||
title: 'qqbot captcha smoke',
|
||||
taskType: 'api',
|
||||
project: 'kt-template-online-api',
|
||||
environment: 'local',
|
||||
operation: 'qqbot-login',
|
||||
target: 'account:2637330537',
|
||||
status: 'blocked',
|
||||
details: {
|
||||
username: 'kept-user',
|
||||
password: 1,
|
||||
nested: {
|
||||
token: true,
|
||||
message: 'kept-message',
|
||||
replyText: 'raw-reply-text',
|
||||
},
|
||||
},
|
||||
cleanup: {
|
||||
status: 'failed',
|
||||
message: 'cleanup failed',
|
||||
details: {
|
||||
ticket: 'raw-ticket',
|
||||
randstr: 'raw-randstr',
|
||||
removed: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(record.details).toEqual({
|
||||
username: 'kept-user',
|
||||
password: '<redacted>',
|
||||
nested: {
|
||||
token: '<redacted>',
|
||||
message: 'kept-message',
|
||||
replyText: '<redacted>',
|
||||
},
|
||||
});
|
||||
expect(record.cleanup?.details).toEqual({
|
||||
ticket: '<redacted>',
|
||||
randstr: '<redacted>',
|
||||
removed: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('sanitizes arrays and case-insensitive sensitive keys while preserving dates', () => {
|
||||
const service = new RuntimeEvidenceService();
|
||||
const createdAt = new Date('2026-06-13T00:00:00.000Z');
|
||||
|
||||
const record = service.createRecord({
|
||||
title: 'array evidence',
|
||||
taskType: 'backend',
|
||||
project: 'kt-template-online-api',
|
||||
environment: 'local',
|
||||
operation: 'runtime-evidence',
|
||||
status: 'failed',
|
||||
details: {
|
||||
events: [
|
||||
{ Authorization: 'Bearer raw', name: 'kept' },
|
||||
{ nested: { Cookie: 'session=raw', count: 2 } },
|
||||
],
|
||||
createdAt,
|
||||
},
|
||||
});
|
||||
|
||||
const details = record.details as {
|
||||
events: Array<Record<string, unknown>>;
|
||||
createdAt: Date;
|
||||
};
|
||||
|
||||
expect(details.createdAt).toBe(createdAt);
|
||||
expect(details.createdAt).toBeInstanceOf(Date);
|
||||
expect(details.events).toHaveLength(2);
|
||||
expect(details.events[0]).toEqual({
|
||||
Authorization: '<redacted>',
|
||||
name: 'kept',
|
||||
});
|
||||
expect(details.events[1]).toEqual({
|
||||
nested: {
|
||||
Cookie: '<redacted>',
|
||||
count: 2,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user