feat(audit): add correlation ids and redaction
This commit is contained in:
@@ -7,6 +7,7 @@ import type {
|
|||||||
ToolSuccessEvent,
|
ToolSuccessEvent,
|
||||||
ToolErrorEvent,
|
ToolErrorEvent,
|
||||||
ToolDeniedEvent,
|
ToolDeniedEvent,
|
||||||
|
ToolApprovalEvent,
|
||||||
SkillsInstallerExecutionBlockedEvent,
|
SkillsInstallerExecutionBlockedEvent,
|
||||||
SkillsInstallerCommandResultEvent,
|
SkillsInstallerCommandResultEvent,
|
||||||
SessionCreateEvent,
|
SessionCreateEvent,
|
||||||
@@ -88,6 +89,11 @@ export class AuditLogger {
|
|||||||
this.write({ level: 'warn', event_type: 'tool.denied', event: event as unknown as Record<string, unknown> });
|
this.write({ level: 'warn', event_type: 'tool.denied', event: event as unknown as Record<string, unknown> });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toolApproval(event: ToolApprovalEvent): void {
|
||||||
|
if (!this.shouldLog('tools', 'debug')) {return;}
|
||||||
|
this.write({ level: 'debug', event_type: 'tool.approval', event: event as unknown as Record<string, unknown> });
|
||||||
|
}
|
||||||
|
|
||||||
skillsInstallerExecutionBlocked(event: SkillsInstallerExecutionBlockedEvent): void {
|
skillsInstallerExecutionBlocked(event: SkillsInstallerExecutionBlockedEvent): void {
|
||||||
if (!this.shouldLog('tools', 'warn')) {return;}
|
if (!this.shouldLog('tools', 'warn')) {return;}
|
||||||
this.write({
|
this.write({
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
const SECRET_KEY_REGEX = /(api[_-]?key|auth|authorization|token|secret|password|cookie)/i;
|
||||||
|
|
||||||
|
export interface RedactionResult<T = unknown> {
|
||||||
|
value: T;
|
||||||
|
redactions: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function redactString(input: string): RedactionResult<string> {
|
||||||
|
let redactions = 0;
|
||||||
|
let output = input;
|
||||||
|
|
||||||
|
// Redact common Authorization header patterns.
|
||||||
|
const beforeAuth = output;
|
||||||
|
output = output.replace(/\bBearer\s+[A-Za-z0-9._\-~=+/]+\b/g, '[REDACTED_BEARER]');
|
||||||
|
if (output !== beforeAuth) {
|
||||||
|
redactions += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redact very long high-entropy tokens (heuristic).
|
||||||
|
const beforeLong = output;
|
||||||
|
output = output.replace(/\b[A-Za-z0-9_\-]{32,}\b/g, '[REDACTED_TOKEN]');
|
||||||
|
if (output !== beforeLong) {
|
||||||
|
redactions += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { value: output, redactions };
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldRedactKey(key: string): boolean {
|
||||||
|
return SECRET_KEY_REGEX.test(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function redactForAudit(value: unknown, opts?: { maxDepth?: number }): RedactionResult {
|
||||||
|
const maxDepth = opts?.maxDepth ?? 6;
|
||||||
|
|
||||||
|
const seen = new WeakSet<object>();
|
||||||
|
|
||||||
|
const walk = (v: unknown, depth: number): RedactionResult => {
|
||||||
|
if (depth > maxDepth) {
|
||||||
|
return { value: '[TRUNCATED_DEPTH]', redactions: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof v === 'string') {
|
||||||
|
return redactString(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (v === null || v === undefined) {
|
||||||
|
return { value: v, redactions: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof v === 'number' || typeof v === 'boolean') {
|
||||||
|
return { value: v, redactions: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(v)) {
|
||||||
|
let redactions = 0;
|
||||||
|
const arr: unknown[] = [];
|
||||||
|
for (const item of v) {
|
||||||
|
const r = walk(item, depth + 1);
|
||||||
|
arr.push(r.value);
|
||||||
|
redactions += r.redactions;
|
||||||
|
}
|
||||||
|
return { value: arr, redactions };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof v === 'object') {
|
||||||
|
const obj = v as Record<string, unknown>;
|
||||||
|
if (seen.has(obj)) {
|
||||||
|
return { value: '[CYCLE]', redactions: 0 };
|
||||||
|
}
|
||||||
|
seen.add(obj);
|
||||||
|
|
||||||
|
let redactions = 0;
|
||||||
|
const out: Record<string, unknown> = {};
|
||||||
|
for (const [k, val] of Object.entries(obj)) {
|
||||||
|
if (shouldRedactKey(k)) {
|
||||||
|
out[k] = '[REDACTED]';
|
||||||
|
redactions += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const r = walk(val, depth + 1);
|
||||||
|
out[k] = r.value;
|
||||||
|
redactions += r.redactions;
|
||||||
|
}
|
||||||
|
return { value: out, redactions };
|
||||||
|
}
|
||||||
|
|
||||||
|
// bigint/symbol/function - stringify to be safe.
|
||||||
|
return { value: String(v), redactions: 0 };
|
||||||
|
};
|
||||||
|
|
||||||
|
return walk(value, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function containsSecretLikeKeys(value: unknown, opts?: { maxDepth?: number }): boolean {
|
||||||
|
const maxDepth = opts?.maxDepth ?? 6;
|
||||||
|
const seen = new WeakSet<object>();
|
||||||
|
|
||||||
|
const walk = (v: unknown, depth: number): boolean => {
|
||||||
|
if (depth > maxDepth) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (v === null || v === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (Array.isArray(v)) {
|
||||||
|
return v.some((item) => walk(item, depth + 1));
|
||||||
|
}
|
||||||
|
if (typeof v === 'object') {
|
||||||
|
const obj = v as Record<string, unknown>;
|
||||||
|
if (seen.has(obj)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
seen.add(obj);
|
||||||
|
|
||||||
|
for (const [k, val] of Object.entries(obj)) {
|
||||||
|
if (shouldRedactKey(k)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (walk(val, depth + 1)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
return walk(value, 0);
|
||||||
|
}
|
||||||
+28
-1
@@ -2,7 +2,7 @@ export type AuditLevel = 'debug' | 'info' | 'warn' | 'error';
|
|||||||
|
|
||||||
export type AuditEventType =
|
export type AuditEventType =
|
||||||
// Tool execution
|
// Tool execution
|
||||||
| 'tool.start' | 'tool.success' | 'tool.error' | 'tool.denied'
|
| 'tool.start' | 'tool.success' | 'tool.error' | 'tool.denied' | 'tool.approval'
|
||||||
// Skills installer
|
// Skills installer
|
||||||
| 'skills.installer.execution_blocked' | 'skills.installer.command_result'
|
| 'skills.installer.execution_blocked' | 'skills.installer.command_result'
|
||||||
// Session lifecycle
|
// Session lifecycle
|
||||||
@@ -49,6 +49,10 @@ export interface AuditQuery {
|
|||||||
export interface ToolStartEvent {
|
export interface ToolStartEvent {
|
||||||
tool_name: string;
|
tool_name: string;
|
||||||
tool_args: unknown;
|
tool_args: unknown;
|
||||||
|
execution_id?: string;
|
||||||
|
execution_environment?: 'host' | 'sandbox';
|
||||||
|
skill_name?: string;
|
||||||
|
redactions_applied?: number;
|
||||||
session_id?: string;
|
session_id?: string;
|
||||||
channel?: string;
|
channel?: string;
|
||||||
sender?: string;
|
sender?: string;
|
||||||
@@ -59,6 +63,10 @@ export interface ToolSuccessEvent {
|
|||||||
tool_name: string;
|
tool_name: string;
|
||||||
result: { success: boolean; output: string; error?: string };
|
result: { success: boolean; output: string; error?: string };
|
||||||
duration_ms: number;
|
duration_ms: number;
|
||||||
|
execution_id?: string;
|
||||||
|
execution_environment?: 'host' | 'sandbox';
|
||||||
|
skill_name?: string;
|
||||||
|
redactions_applied?: number;
|
||||||
session_id?: string;
|
session_id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,16 +75,35 @@ export interface ToolErrorEvent {
|
|||||||
error: string;
|
error: string;
|
||||||
duration_ms: number;
|
duration_ms: number;
|
||||||
reason?: string;
|
reason?: string;
|
||||||
|
execution_id?: string;
|
||||||
|
execution_environment?: 'host' | 'sandbox';
|
||||||
|
skill_name?: string;
|
||||||
|
redactions_applied?: number;
|
||||||
session_id?: string;
|
session_id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ToolDeniedEvent {
|
export interface ToolDeniedEvent {
|
||||||
tool_name: string;
|
tool_name: string;
|
||||||
reason: string;
|
reason: string;
|
||||||
|
execution_id?: string;
|
||||||
|
execution_environment?: 'host' | 'sandbox';
|
||||||
|
skill_name?: string;
|
||||||
|
redactions_applied?: number;
|
||||||
session_id?: string;
|
session_id?: string;
|
||||||
denial_type: 'policy' | 'hook' | 'not_found' | 'autonomy_override';
|
denial_type: 'policy' | 'hook' | 'not_found' | 'autonomy_override';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ToolApprovalEvent {
|
||||||
|
tool_name: string;
|
||||||
|
approved: boolean;
|
||||||
|
reason?: string;
|
||||||
|
execution_id?: string;
|
||||||
|
execution_environment?: 'host' | 'sandbox';
|
||||||
|
skill_name?: string;
|
||||||
|
redactions_applied?: number;
|
||||||
|
session_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SkillsInstallerExecutionBlockedEvent {
|
export interface SkillsInstallerExecutionBlockedEvent {
|
||||||
skill_name: string;
|
skill_name: string;
|
||||||
phase: 'install' | 'execute';
|
phase: 'install' | 'execute';
|
||||||
|
|||||||
Reference in New Issue
Block a user