-
-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathaudit.ts
More file actions
190 lines (174 loc) · 4.85 KB
/
Copy pathaudit.ts
File metadata and controls
190 lines (174 loc) · 4.85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
import { db } from '$lib/server/db';
import type { RequestEvent } from '@sveltejs/kit';
export type AuditAction =
// Authentication Events
| 'USER_LOGIN_SUCCESS'
| 'USER_LOGIN_FAILED'
| 'USER_LOGOUT'
| 'USER_PASSWORD_CHANGED'
| 'USER_PASSWORD_RESET_REQUESTED'
| 'USER_PASSWORD_RESET_COMPLETED'
| 'USER_EMAIL_VERIFIED'
| 'SESSION_CREATED'
| 'SESSION_EXPIRED'
// User Management
| 'USER_CREATED'
| 'USER_UPDATED'
| 'USER_DELETED'
| 'USER_ROLE_CHANGED'
| 'USER_PROFILE_UPDATED'
// Team Management
| 'TEAM_CREATED'
| 'TEAM_UPDATED'
| 'TEAM_DELETED'
| 'TEAM_PLAN_CHANGED'
| 'TEAM_MEMBER_ADDED'
| 'TEAM_MEMBER_REMOVED'
| 'TEAM_INVITATION_SENT'
| 'TEAM_INVITATION_ACCEPTED'
| 'TEAM_INVITATION_REVOKED'
// API Keys
| 'API_KEY_CREATED'
| 'API_KEY_DELETED'
| 'API_KEY_USED'
// Authenticator Tokens
| 'AUTHENTICATOR_TOKEN_CREATED'
| 'AUTHENTICATOR_TOKEN_UPDATED'
| 'AUTHENTICATOR_TOKEN_DELETED'
| 'AUTHENTICATOR_TOKEN_VIEWED'
| 'AUTHENTICATOR_TOKEN_CODE_GENERATED'
// Data Access (GDPR)
| 'USER_DATA_EXPORTED'
| 'USER_DATA_DELETED'
| 'SENSITIVE_DATA_ACCESSED'
// Project & Test Data
| 'PROJECT_CREATED'
| 'PROJECT_UPDATED'
| 'PROJECT_DELETED'
| 'PROJECT_ACCESSED'
| 'TEST_CASE_CREATED'
| 'TEST_CASE_UPDATED'
| 'TEST_CASE_DELETED'
| 'TEST_SUITE_CREATED'
| 'TEST_SUITE_UPDATED'
| 'TEST_SUITE_DELETED'
| 'TEST_RUN_CREATED'
| 'TEST_RUN_UPDATED'
| 'TEST_RUN_DELETED'
// Subscription & Billing
| 'SUBSCRIPTION_CREATED'
| 'SUBSCRIPTION_UPDATED'
| 'SUBSCRIPTION_CANCELLED'
| 'PAYMENT_METHOD_ADDED'
| 'PAYMENT_METHOD_REMOVED'
// Enterprise Inquiries
| 'ENTERPRISE_INQUIRY_CREATED'
| 'ENTERPRISE_INQUIRY_UPDATED'
| 'ENTERPRISE_INQUIRY_CONVERTED'
// Security Events
| 'UNAUTHORIZED_ACCESS_ATTEMPT'
| 'RATE_LIMIT_EXCEEDED'
| 'SUSPICIOUS_ACTIVITY_DETECTED'
| 'SETTINGS_CHANGED'
// System Operations
| 'DATABASE_BACKUP_CREATED'
| 'DATABASE_BACKUP_FAILED'
| 'DATABASE_BACKUP_DELETION_FAILED';
interface AuditLogParams {
userId?: string | null; // Optional for anonymous events
teamId?: string;
action: AuditAction;
resourceType: string;
resourceId?: string;
metadata?: Record<string, any>;
event?: RequestEvent;
}
/**
* Create an audit log entry
* @param params - Audit log parameters
*/
export async function createAuditLog(params: AuditLogParams): Promise<void> {
const { userId, teamId, action, resourceType, resourceId, metadata, event } = params;
// Skip creating audit log if userId is null (not a valid user)
if (!userId) {
return;
}
// Skip anonymous users (not authenticated), but allow 'system' for automated operations
if (userId === 'anonymous') {
console.warn(`Skipping audit log for anonymous ${action} on ${resourceType}`, {
ipAddress: event?.request.headers.get('x-forwarded-for') || event?.getClientAddress(),
metadata
});
return;
}
// 'system' and regular user IDs continue here
// Extract IP address and user agent from event if available
const ipAddress = event?.request.headers.get('x-forwarded-for') || event?.getClientAddress();
const userAgent = event?.request.headers.get('user-agent');
try {
await db.auditLog.create({
data: {
userId,
teamId: teamId ?? undefined,
action,
resourceType,
resourceId: resourceId ?? undefined,
metadata: metadata ?? undefined,
ipAddress: ipAddress ?? undefined,
userAgent: userAgent ?? undefined
}
});
} catch (error) {
// Log but don't throw - audit failures shouldn't break the main operation
console.error('Failed to create audit log:', error);
}
}
/**
* Sanitize metadata to ensure no sensitive data is logged
* Removes fields like 'secret', 'password', 'token', etc.
* Uses exact field name matching and common patterns to avoid false positives
*/
export function sanitizeMetadata(data: Record<string, any>): Record<string, any> {
// Exact field names to filter
const exactSensitiveKeys = [
'secret',
'password',
'token',
'key',
'hash',
'apikey',
'accesstoken',
'refreshtoken',
'csrftoken',
'sessiontoken'
];
// Patterns for common sensitive field naming conventions
const sensitivePatterns = [
/^.*secret$/i, // ends with "secret"
/^.*password$/i, // ends with "password"
/^.*token$/i, // ends with "token"
/^.*_key$/i, // ends with "_key"
/^.*hash$/i, // ends with "hash"
/^api.*key$/i, // API key variations
/^encrypted.*/i // starts with "encrypted"
];
const sanitized: Record<string, any> = {};
for (const [key, value] of Object.entries(data)) {
const lowerKey = key.toLowerCase();
// Check exact matches first
if (exactSensitiveKeys.includes(lowerKey)) {
continue;
}
// Check regex patterns
if (sensitivePatterns.some((pattern) => pattern.test(key))) {
continue;
}
// Recursively sanitize nested objects
if (value && typeof value === 'object' && !Array.isArray(value)) {
sanitized[key] = sanitizeMetadata(value);
} else {
sanitized[key] = value;
}
}
return sanitized;
}