@erosolaraijs/cure 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +120 -0
- package/dist/bin/cure.d.ts +10 -0
- package/dist/bin/cure.d.ts.map +1 -0
- package/dist/bin/cure.js +169 -0
- package/dist/bin/cure.js.map +1 -0
- package/dist/capabilities/cancerTreatmentCapability.d.ts +167 -0
- package/dist/capabilities/cancerTreatmentCapability.d.ts.map +1 -0
- package/dist/capabilities/cancerTreatmentCapability.js +912 -0
- package/dist/capabilities/cancerTreatmentCapability.js.map +1 -0
- package/dist/capabilities/index.d.ts +2 -0
- package/dist/capabilities/index.d.ts.map +1 -0
- package/dist/capabilities/index.js +3 -0
- package/dist/capabilities/index.js.map +1 -0
- package/dist/compliance/hipaa.d.ts +337 -0
- package/dist/compliance/hipaa.d.ts.map +1 -0
- package/dist/compliance/hipaa.js +929 -0
- package/dist/compliance/hipaa.js.map +1 -0
- package/dist/examples/cancerTreatmentDemo.d.ts +21 -0
- package/dist/examples/cancerTreatmentDemo.d.ts.map +1 -0
- package/dist/examples/cancerTreatmentDemo.js +216 -0
- package/dist/examples/cancerTreatmentDemo.js.map +1 -0
- package/dist/integrations/clinicalTrials/clinicalTrialsGov.d.ts +265 -0
- package/dist/integrations/clinicalTrials/clinicalTrialsGov.d.ts.map +1 -0
- package/dist/integrations/clinicalTrials/clinicalTrialsGov.js +808 -0
- package/dist/integrations/clinicalTrials/clinicalTrialsGov.js.map +1 -0
- package/dist/integrations/ehr/fhir.d.ts +455 -0
- package/dist/integrations/ehr/fhir.d.ts.map +1 -0
- package/dist/integrations/ehr/fhir.js +859 -0
- package/dist/integrations/ehr/fhir.js.map +1 -0
- package/dist/integrations/genomics/genomicPlatforms.d.ts +362 -0
- package/dist/integrations/genomics/genomicPlatforms.d.ts.map +1 -0
- package/dist/integrations/genomics/genomicPlatforms.js +1079 -0
- package/dist/integrations/genomics/genomicPlatforms.js.map +1 -0
- package/package.json +52 -0
- package/src/bin/cure.ts +182 -0
- package/src/capabilities/cancerTreatmentCapability.ts +1161 -0
- package/src/capabilities/index.ts +2 -0
- package/src/compliance/hipaa.ts +1365 -0
- package/src/examples/cancerTreatmentDemo.ts +241 -0
- package/src/integrations/clinicalTrials/clinicalTrialsGov.ts +1143 -0
- package/src/integrations/ehr/fhir.ts +1304 -0
- package/src/integrations/genomics/genomicPlatforms.ts +1480 -0
- package/src/ml/outcomePredictor.ts +1301 -0
- package/src/safety/drugInteractions.ts +942 -0
- package/src/validation/retrospectiveValidator.ts +887 -0
|
@@ -0,0 +1,929 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HIPAA Compliance Layer
|
|
3
|
+
*
|
|
4
|
+
* Provides comprehensive HIPAA compliance features:
|
|
5
|
+
* - Audit logging (access, modification, disclosure)
|
|
6
|
+
* - Patient consent management
|
|
7
|
+
* - Data encryption (at rest and in transit)
|
|
8
|
+
* - Access control and authentication
|
|
9
|
+
* - Break-the-glass emergency access
|
|
10
|
+
* - Minimum necessary standard enforcement
|
|
11
|
+
* - Business Associate Agreement tracking
|
|
12
|
+
*
|
|
13
|
+
* IMPORTANT: This module implements the technical safeguards required by HIPAA.
|
|
14
|
+
* Administrative and physical safeguards must be implemented organizationally.
|
|
15
|
+
*/
|
|
16
|
+
import { createHash, createCipheriv, createDecipheriv, randomBytes, scrypt } from 'crypto';
|
|
17
|
+
import { EventEmitter } from 'events';
|
|
18
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
19
|
+
// HIPAA COMPLIANCE SERVICE
|
|
20
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
21
|
+
export class HIPAAComplianceService extends EventEmitter {
|
|
22
|
+
auditStore = [];
|
|
23
|
+
consents = new Map();
|
|
24
|
+
accessPolicies = [];
|
|
25
|
+
encryptionKeys = new Map();
|
|
26
|
+
activeKeyId;
|
|
27
|
+
config;
|
|
28
|
+
lastEntryHash;
|
|
29
|
+
constructor(config) {
|
|
30
|
+
super();
|
|
31
|
+
this.config = {
|
|
32
|
+
encryption: {
|
|
33
|
+
algorithm: 'aes-256-gcm',
|
|
34
|
+
keyDerivation: 'scrypt',
|
|
35
|
+
keyRotationDays: 90,
|
|
36
|
+
fieldLevelEncryption: true,
|
|
37
|
+
encryptedFields: ['ssn', 'dob', 'address', 'phone', 'genomics', 'mentalHealth'],
|
|
38
|
+
...config?.encryption
|
|
39
|
+
},
|
|
40
|
+
masking: {
|
|
41
|
+
ssnPattern: 'last4',
|
|
42
|
+
dobPattern: 'age-only',
|
|
43
|
+
phonePattern: 'last4',
|
|
44
|
+
addressPattern: 'city-state',
|
|
45
|
+
mrnPattern: 'full',
|
|
46
|
+
genomicPattern: 'summary',
|
|
47
|
+
...config?.masking
|
|
48
|
+
},
|
|
49
|
+
auditRetentionDays: config?.auditRetentionDays || 2190, // 6 years per HIPAA
|
|
50
|
+
emergencyAccessEnabled: config?.emergencyAccessEnabled ?? true
|
|
51
|
+
};
|
|
52
|
+
// Initialize encryption key
|
|
53
|
+
this.activeKeyId = this.generateKeyId();
|
|
54
|
+
this.initializeEncryptionKey();
|
|
55
|
+
}
|
|
56
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
57
|
+
// AUDIT LOGGING
|
|
58
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
59
|
+
/**
|
|
60
|
+
* Log an audit event
|
|
61
|
+
*/
|
|
62
|
+
async logAuditEvent(entry) {
|
|
63
|
+
const id = this.generateAuditId();
|
|
64
|
+
const timestamp = new Date();
|
|
65
|
+
// Create the entry
|
|
66
|
+
const fullEntry = {
|
|
67
|
+
id,
|
|
68
|
+
timestamp,
|
|
69
|
+
...entry,
|
|
70
|
+
previousEntryHash: this.lastEntryHash
|
|
71
|
+
};
|
|
72
|
+
// Calculate hash for tamper detection
|
|
73
|
+
fullEntry.entryHash = this.calculateEntryHash(fullEntry);
|
|
74
|
+
this.lastEntryHash = fullEntry.entryHash;
|
|
75
|
+
// Store the entry
|
|
76
|
+
this.auditStore.push(fullEntry);
|
|
77
|
+
// Emit event for external handlers
|
|
78
|
+
this.emit('audit-event', fullEntry);
|
|
79
|
+
// Check for suspicious patterns
|
|
80
|
+
await this.detectSuspiciousActivity(fullEntry);
|
|
81
|
+
return id;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Log PHI access event
|
|
85
|
+
*/
|
|
86
|
+
async logPHIAccess(params) {
|
|
87
|
+
return this.logAuditEvent({
|
|
88
|
+
eventType: params.emergencyAccess ? 'emergency-access' : 'access',
|
|
89
|
+
action: params.action,
|
|
90
|
+
outcome: 'success',
|
|
91
|
+
actor: {
|
|
92
|
+
userId: params.userId,
|
|
93
|
+
userName: params.userName,
|
|
94
|
+
role: params.role,
|
|
95
|
+
ipAddress: params.ipAddress
|
|
96
|
+
},
|
|
97
|
+
resource: {
|
|
98
|
+
type: params.resourceType,
|
|
99
|
+
id: params.resourceId,
|
|
100
|
+
patientId: params.patientId
|
|
101
|
+
},
|
|
102
|
+
details: {
|
|
103
|
+
fieldsAccessed: params.fieldsAccessed,
|
|
104
|
+
reason: params.reason,
|
|
105
|
+
emergencyAccess: params.emergencyAccess,
|
|
106
|
+
consentId: params.consentId
|
|
107
|
+
},
|
|
108
|
+
security: {
|
|
109
|
+
authMethod: 'mfa', // Assuming MFA for PHI access
|
|
110
|
+
encryptionUsed: true,
|
|
111
|
+
integrityVerified: true,
|
|
112
|
+
accessLevel: params.emergencyAccess ? 'emergency' : 'normal'
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Log PHI modification event
|
|
118
|
+
*/
|
|
119
|
+
async logPHIModification(params) {
|
|
120
|
+
return this.logAuditEvent({
|
|
121
|
+
eventType: 'modification',
|
|
122
|
+
action: params.action,
|
|
123
|
+
outcome: 'success',
|
|
124
|
+
actor: {
|
|
125
|
+
userId: params.userId,
|
|
126
|
+
role: params.role,
|
|
127
|
+
ipAddress: params.ipAddress
|
|
128
|
+
},
|
|
129
|
+
resource: {
|
|
130
|
+
type: params.resourceType,
|
|
131
|
+
id: params.resourceId,
|
|
132
|
+
patientId: params.patientId
|
|
133
|
+
},
|
|
134
|
+
details: {
|
|
135
|
+
fieldsModified: params.fieldsModified,
|
|
136
|
+
previousValues: params.previousValues,
|
|
137
|
+
newValues: params.newValues,
|
|
138
|
+
reason: params.reason
|
|
139
|
+
},
|
|
140
|
+
security: {
|
|
141
|
+
authMethod: 'mfa',
|
|
142
|
+
encryptionUsed: true,
|
|
143
|
+
integrityVerified: true,
|
|
144
|
+
accessLevel: 'normal'
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Log authentication event
|
|
150
|
+
*/
|
|
151
|
+
async logAuthentication(params) {
|
|
152
|
+
return this.logAuditEvent({
|
|
153
|
+
eventType: 'authentication',
|
|
154
|
+
action: params.action,
|
|
155
|
+
outcome: params.success ? 'success' : 'failure',
|
|
156
|
+
actor: {
|
|
157
|
+
userId: params.userId,
|
|
158
|
+
role: 'unknown',
|
|
159
|
+
ipAddress: params.ipAddress
|
|
160
|
+
},
|
|
161
|
+
resource: {
|
|
162
|
+
type: 'system'
|
|
163
|
+
},
|
|
164
|
+
details: params.failureReason ? { reason: params.failureReason } : undefined,
|
|
165
|
+
security: {
|
|
166
|
+
authMethod: params.authMethod,
|
|
167
|
+
encryptionUsed: true,
|
|
168
|
+
integrityVerified: true,
|
|
169
|
+
accessLevel: 'normal'
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Query audit logs
|
|
175
|
+
*/
|
|
176
|
+
queryAuditLogs(params) {
|
|
177
|
+
let filtered = [...this.auditStore];
|
|
178
|
+
if (params.startDate) {
|
|
179
|
+
filtered = filtered.filter(e => e.timestamp >= params.startDate);
|
|
180
|
+
}
|
|
181
|
+
if (params.endDate) {
|
|
182
|
+
filtered = filtered.filter(e => e.timestamp <= params.endDate);
|
|
183
|
+
}
|
|
184
|
+
if (params.patientId) {
|
|
185
|
+
filtered = filtered.filter(e => e.resource.patientId === params.patientId);
|
|
186
|
+
}
|
|
187
|
+
if (params.userId) {
|
|
188
|
+
filtered = filtered.filter(e => e.actor.userId === params.userId);
|
|
189
|
+
}
|
|
190
|
+
if (params.eventType) {
|
|
191
|
+
filtered = filtered.filter(e => e.eventType === params.eventType);
|
|
192
|
+
}
|
|
193
|
+
if (params.action) {
|
|
194
|
+
filtered = filtered.filter(e => e.action === params.action);
|
|
195
|
+
}
|
|
196
|
+
if (params.outcome) {
|
|
197
|
+
filtered = filtered.filter(e => e.outcome === params.outcome);
|
|
198
|
+
}
|
|
199
|
+
// Sort by timestamp descending
|
|
200
|
+
filtered.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
|
201
|
+
const total = filtered.length;
|
|
202
|
+
const offset = params.offset || 0;
|
|
203
|
+
const limit = params.limit || 100;
|
|
204
|
+
return {
|
|
205
|
+
entries: filtered.slice(offset, offset + limit),
|
|
206
|
+
total
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Get accounting of disclosures for a patient (HIPAA requirement)
|
|
211
|
+
*/
|
|
212
|
+
getAccountingOfDisclosures(patientId, startDate, endDate) {
|
|
213
|
+
return this.auditStore.filter(entry => entry.resource.patientId === patientId &&
|
|
214
|
+
entry.eventType === 'disclosure' &&
|
|
215
|
+
(!startDate || entry.timestamp >= startDate) &&
|
|
216
|
+
(!endDate || entry.timestamp <= endDate)).sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Verify audit log integrity
|
|
220
|
+
*/
|
|
221
|
+
verifyAuditLogIntegrity() {
|
|
222
|
+
const errors = [];
|
|
223
|
+
for (let i = 0; i < this.auditStore.length; i++) {
|
|
224
|
+
const entry = this.auditStore[i];
|
|
225
|
+
// Verify hash
|
|
226
|
+
const { entryHash: _hash, ...entryWithoutHash } = entry;
|
|
227
|
+
const calculatedHash = this.calculateEntryHash(entryWithoutHash);
|
|
228
|
+
if (calculatedHash !== entry.entryHash) {
|
|
229
|
+
errors.push(`Entry ${entry.id} has been tampered with (hash mismatch)`);
|
|
230
|
+
}
|
|
231
|
+
// Verify chain
|
|
232
|
+
if (i > 0 && entry.previousEntryHash !== this.auditStore[i - 1].entryHash) {
|
|
233
|
+
errors.push(`Entry ${entry.id} has broken chain link`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
valid: errors.length === 0,
|
|
238
|
+
errors
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
242
|
+
// CONSENT MANAGEMENT
|
|
243
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
244
|
+
/**
|
|
245
|
+
* Record a patient consent
|
|
246
|
+
*/
|
|
247
|
+
async recordConsent(consent) {
|
|
248
|
+
const id = this.generateConsentId();
|
|
249
|
+
const now = new Date();
|
|
250
|
+
const fullConsent = {
|
|
251
|
+
id,
|
|
252
|
+
...consent,
|
|
253
|
+
audit: {
|
|
254
|
+
createdAt: now,
|
|
255
|
+
createdBy: consent.capture.method === 'electronic' ? 'system' : 'staff'
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
// Store consent
|
|
259
|
+
const patientConsents = this.consents.get(consent.patientId) || [];
|
|
260
|
+
patientConsents.push(fullConsent);
|
|
261
|
+
this.consents.set(consent.patientId, patientConsents);
|
|
262
|
+
// Log the consent capture
|
|
263
|
+
await this.logAuditEvent({
|
|
264
|
+
eventType: 'consent-change',
|
|
265
|
+
action: 'consent-obtained',
|
|
266
|
+
outcome: 'success',
|
|
267
|
+
actor: {
|
|
268
|
+
userId: fullConsent.audit.createdBy,
|
|
269
|
+
role: 'system',
|
|
270
|
+
ipAddress: consent.capture.ipAddress || 'unknown'
|
|
271
|
+
},
|
|
272
|
+
resource: {
|
|
273
|
+
type: 'patient',
|
|
274
|
+
patientId: consent.patientId,
|
|
275
|
+
description: `${consent.consentType} consent`
|
|
276
|
+
},
|
|
277
|
+
details: {
|
|
278
|
+
consentId: id
|
|
279
|
+
},
|
|
280
|
+
security: {
|
|
281
|
+
authMethod: 'password',
|
|
282
|
+
encryptionUsed: true,
|
|
283
|
+
integrityVerified: true,
|
|
284
|
+
accessLevel: 'normal'
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
this.emit('consent-recorded', fullConsent);
|
|
288
|
+
return id;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Revoke a patient consent
|
|
292
|
+
*/
|
|
293
|
+
async revokeConsent(consentId, revokedBy, reason) {
|
|
294
|
+
for (const [patientId, consents] of this.consents) {
|
|
295
|
+
const consent = consents.find(c => c.id === consentId);
|
|
296
|
+
if (consent) {
|
|
297
|
+
consent.status = 'revoked';
|
|
298
|
+
consent.audit.revokedAt = new Date();
|
|
299
|
+
consent.audit.revokedBy = revokedBy;
|
|
300
|
+
consent.audit.revocationReason = reason;
|
|
301
|
+
// Log the revocation
|
|
302
|
+
await this.logAuditEvent({
|
|
303
|
+
eventType: 'consent-change',
|
|
304
|
+
action: 'consent-revoked',
|
|
305
|
+
outcome: 'success',
|
|
306
|
+
actor: {
|
|
307
|
+
userId: revokedBy,
|
|
308
|
+
role: 'patient',
|
|
309
|
+
ipAddress: 'unknown'
|
|
310
|
+
},
|
|
311
|
+
resource: {
|
|
312
|
+
type: 'patient',
|
|
313
|
+
patientId,
|
|
314
|
+
description: `${consent.consentType} consent revoked`
|
|
315
|
+
},
|
|
316
|
+
details: {
|
|
317
|
+
consentId,
|
|
318
|
+
reason
|
|
319
|
+
},
|
|
320
|
+
security: {
|
|
321
|
+
authMethod: 'password',
|
|
322
|
+
encryptionUsed: true,
|
|
323
|
+
integrityVerified: true,
|
|
324
|
+
accessLevel: 'normal'
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
this.emit('consent-revoked', consent);
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Check if a patient has consented to a specific use
|
|
335
|
+
*/
|
|
336
|
+
checkConsent(patientId, purpose, dataCategory) {
|
|
337
|
+
const patientConsents = this.consents.get(patientId) || [];
|
|
338
|
+
// Find active consent that covers the purpose
|
|
339
|
+
const validConsent = patientConsents.find(c => c.status === 'active' &&
|
|
340
|
+
c.scope.purposes.includes(purpose) &&
|
|
341
|
+
(!dataCategory || c.scope.dataCategories.includes(dataCategory)) &&
|
|
342
|
+
(!c.validity.expirationDate || c.validity.expirationDate > new Date()));
|
|
343
|
+
if (validConsent) {
|
|
344
|
+
return {
|
|
345
|
+
hasConsent: true,
|
|
346
|
+
consentId: validConsent.id,
|
|
347
|
+
restrictions: validConsent.scope.excludedData
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
// Check if TPO (Treatment, Payment, Operations) - may not require explicit consent
|
|
351
|
+
if (['treatment', 'payment', 'healthcare-operations'].includes(purpose)) {
|
|
352
|
+
// TPO typically covered by Notice of Privacy Practices
|
|
353
|
+
return {
|
|
354
|
+
hasConsent: true,
|
|
355
|
+
restrictions: []
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
hasConsent: false
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Get all consents for a patient
|
|
364
|
+
*/
|
|
365
|
+
getPatientConsents(patientId) {
|
|
366
|
+
return this.consents.get(patientId) || [];
|
|
367
|
+
}
|
|
368
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
369
|
+
// ACCESS CONTROL
|
|
370
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
371
|
+
/**
|
|
372
|
+
* Add an access policy
|
|
373
|
+
*/
|
|
374
|
+
addAccessPolicy(policy) {
|
|
375
|
+
const id = this.generatePolicyId();
|
|
376
|
+
const fullPolicy = { id, ...policy };
|
|
377
|
+
this.accessPolicies.push(fullPolicy);
|
|
378
|
+
this.accessPolicies.sort((a, b) => b.priority - a.priority);
|
|
379
|
+
return id;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Check if access should be allowed
|
|
383
|
+
*/
|
|
384
|
+
checkAccess(request) {
|
|
385
|
+
const time = request.time || new Date();
|
|
386
|
+
// Emergency access bypass (break-the-glass)
|
|
387
|
+
if (request.emergencyAccess && this.config.emergencyAccessEnabled) {
|
|
388
|
+
return {
|
|
389
|
+
allowed: true,
|
|
390
|
+
reason: 'Emergency access granted (break-the-glass)',
|
|
391
|
+
conditions: ['Must document emergency reason', 'Subject to post-hoc review'],
|
|
392
|
+
auditRequired: true
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
// Check consent if accessing patient data
|
|
396
|
+
if (request.patientId && request.purpose) {
|
|
397
|
+
const consentCheck = this.checkConsent(request.patientId, request.purpose, request.dataCategory);
|
|
398
|
+
if (!consentCheck.hasConsent) {
|
|
399
|
+
return {
|
|
400
|
+
allowed: false,
|
|
401
|
+
reason: `No consent for ${request.purpose} purpose`,
|
|
402
|
+
auditRequired: true
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
// Evaluate policies in priority order
|
|
407
|
+
for (const policy of this.accessPolicies) {
|
|
408
|
+
if (!policy.enabled)
|
|
409
|
+
continue;
|
|
410
|
+
// Check if policy applies to this subject
|
|
411
|
+
const subjectMatch = (!policy.subjects.roles || policy.subjects.roles.includes(request.role)) &&
|
|
412
|
+
(!policy.subjects.users || policy.subjects.users.includes(request.userId)) &&
|
|
413
|
+
(!policy.subjects.organizations || policy.subjects.organizations.includes(request.organization || ''));
|
|
414
|
+
if (!subjectMatch)
|
|
415
|
+
continue;
|
|
416
|
+
// Check if policy applies to this resource
|
|
417
|
+
const resourceMatch = (!policy.resources.types || policy.resources.types.includes(request.resourceType)) &&
|
|
418
|
+
(!policy.resources.patientIds || !request.patientId || policy.resources.patientIds.includes(request.patientId)) &&
|
|
419
|
+
(!policy.resources.dataCategories || !request.dataCategory || policy.resources.dataCategories.includes(request.dataCategory));
|
|
420
|
+
if (!resourceMatch)
|
|
421
|
+
continue;
|
|
422
|
+
// Check if action is permitted
|
|
423
|
+
if (!policy.permissions.actions.includes(request.action)) {
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
// Evaluate conditions
|
|
427
|
+
const conditionResults = this.evaluateConditions(policy.permissions.conditions || [], request, time);
|
|
428
|
+
if (conditionResults.allMet) {
|
|
429
|
+
return {
|
|
430
|
+
allowed: true,
|
|
431
|
+
policy: policy.id,
|
|
432
|
+
reason: `Access granted by policy: ${policy.name}`,
|
|
433
|
+
conditions: conditionResults.messages,
|
|
434
|
+
auditRequired: true
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
else if (conditionResults.requiresElevation) {
|
|
438
|
+
return {
|
|
439
|
+
allowed: false,
|
|
440
|
+
policy: policy.id,
|
|
441
|
+
reason: 'Access requires additional verification',
|
|
442
|
+
requiresElevation: true,
|
|
443
|
+
conditions: conditionResults.messages,
|
|
444
|
+
auditRequired: true
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
// Default deny
|
|
449
|
+
return {
|
|
450
|
+
allowed: false,
|
|
451
|
+
reason: 'No matching policy found - access denied by default',
|
|
452
|
+
auditRequired: true
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
evaluateConditions(conditions, request, time) {
|
|
456
|
+
const messages = [];
|
|
457
|
+
let requiresElevation = false;
|
|
458
|
+
for (const condition of conditions) {
|
|
459
|
+
switch (condition.type) {
|
|
460
|
+
case 'time-of-day':
|
|
461
|
+
const hour = time.getHours();
|
|
462
|
+
const startHour = condition.parameters.startHour || 0;
|
|
463
|
+
const endHour = condition.parameters.endHour || 24;
|
|
464
|
+
if (hour < startHour || hour >= endHour) {
|
|
465
|
+
messages.push(`Access outside permitted hours (${startHour}:00 - ${endHour}:00)`);
|
|
466
|
+
return { allMet: false, requiresElevation: false, messages };
|
|
467
|
+
}
|
|
468
|
+
break;
|
|
469
|
+
case 'ip-range':
|
|
470
|
+
// Simplified IP check - would need proper CIDR matching in production
|
|
471
|
+
const allowedRanges = condition.parameters.ranges;
|
|
472
|
+
if (request.ipAddress && !allowedRanges.some(r => request.ipAddress.startsWith(r))) {
|
|
473
|
+
messages.push('Access from unauthorized network location');
|
|
474
|
+
return { allMet: false, requiresElevation: false, messages };
|
|
475
|
+
}
|
|
476
|
+
break;
|
|
477
|
+
case 'mfa-required':
|
|
478
|
+
messages.push('Multi-factor authentication required');
|
|
479
|
+
requiresElevation = true;
|
|
480
|
+
break;
|
|
481
|
+
case 'purpose':
|
|
482
|
+
const allowedPurposes = condition.parameters.purposes;
|
|
483
|
+
if (!request.purpose || !allowedPurposes.includes(request.purpose)) {
|
|
484
|
+
messages.push(`Purpose '${request.purpose}' not permitted`);
|
|
485
|
+
return { allMet: false, requiresElevation: false, messages };
|
|
486
|
+
}
|
|
487
|
+
break;
|
|
488
|
+
case 'emergency-only':
|
|
489
|
+
if (!request.emergencyAccess) {
|
|
490
|
+
messages.push('Access restricted to emergency situations');
|
|
491
|
+
return { allMet: false, requiresElevation: false, messages };
|
|
492
|
+
}
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
allMet: !requiresElevation,
|
|
498
|
+
requiresElevation,
|
|
499
|
+
messages
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
503
|
+
// ENCRYPTION
|
|
504
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
505
|
+
/**
|
|
506
|
+
* Encrypt sensitive data
|
|
507
|
+
*/
|
|
508
|
+
async encryptData(data, context) {
|
|
509
|
+
const keyEntry = this.encryptionKeys.get(this.activeKeyId);
|
|
510
|
+
if (!keyEntry) {
|
|
511
|
+
throw new Error('No active encryption key');
|
|
512
|
+
}
|
|
513
|
+
const iv = randomBytes(16);
|
|
514
|
+
const algorithm = this.config.encryption.algorithm;
|
|
515
|
+
if (algorithm === 'aes-256-gcm') {
|
|
516
|
+
const cipher = createCipheriv('aes-256-gcm', keyEntry.key, iv);
|
|
517
|
+
if (context) {
|
|
518
|
+
cipher.setAAD(Buffer.from(context));
|
|
519
|
+
}
|
|
520
|
+
const dataBuffer = typeof data === 'string' ? Buffer.from(data, 'utf8') : data;
|
|
521
|
+
const encrypted = Buffer.concat([cipher.update(dataBuffer), cipher.final()]);
|
|
522
|
+
const authTag = cipher.getAuthTag();
|
|
523
|
+
return {
|
|
524
|
+
ciphertext: encrypted.toString('base64'),
|
|
525
|
+
iv: iv.toString('base64'),
|
|
526
|
+
authTag: authTag.toString('base64'),
|
|
527
|
+
keyId: this.activeKeyId,
|
|
528
|
+
algorithm,
|
|
529
|
+
encryptedAt: new Date()
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
// AES-256-CBC fallback
|
|
534
|
+
const cipher = createCipheriv('aes-256-cbc', keyEntry.key, iv);
|
|
535
|
+
const dataBuffer = typeof data === 'string' ? Buffer.from(data, 'utf8') : data;
|
|
536
|
+
const encrypted = Buffer.concat([cipher.update(dataBuffer), cipher.final()]);
|
|
537
|
+
return {
|
|
538
|
+
ciphertext: encrypted.toString('base64'),
|
|
539
|
+
iv: iv.toString('base64'),
|
|
540
|
+
keyId: this.activeKeyId,
|
|
541
|
+
algorithm: 'aes-256-cbc',
|
|
542
|
+
encryptedAt: new Date()
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Decrypt data
|
|
548
|
+
*/
|
|
549
|
+
async decryptData(encrypted, context) {
|
|
550
|
+
const keyEntry = this.encryptionKeys.get(encrypted.keyId);
|
|
551
|
+
if (!keyEntry) {
|
|
552
|
+
throw new Error(`Encryption key ${encrypted.keyId} not found`);
|
|
553
|
+
}
|
|
554
|
+
const iv = Buffer.from(encrypted.iv, 'base64');
|
|
555
|
+
const ciphertext = Buffer.from(encrypted.ciphertext, 'base64');
|
|
556
|
+
if (encrypted.algorithm === 'aes-256-gcm') {
|
|
557
|
+
const decipher = createDecipheriv('aes-256-gcm', keyEntry.key, iv);
|
|
558
|
+
if (encrypted.authTag) {
|
|
559
|
+
decipher.setAuthTag(Buffer.from(encrypted.authTag, 'base64'));
|
|
560
|
+
}
|
|
561
|
+
if (context) {
|
|
562
|
+
decipher.setAAD(Buffer.from(context));
|
|
563
|
+
}
|
|
564
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
const decipher = createDecipheriv('aes-256-cbc', keyEntry.key, iv);
|
|
568
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Encrypt specific PHI fields in an object
|
|
573
|
+
*/
|
|
574
|
+
async encryptPHI(data) {
|
|
575
|
+
const encryptedFields = {};
|
|
576
|
+
const result = { ...data };
|
|
577
|
+
for (const field of this.config.encryption.encryptedFields || []) {
|
|
578
|
+
if (data[field] !== undefined) {
|
|
579
|
+
encryptedFields[field] = await this.encryptData(JSON.stringify(data[field]), field);
|
|
580
|
+
result[field] = '[ENCRYPTED]';
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
result._encrypted = encryptedFields;
|
|
584
|
+
return result;
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Decrypt PHI fields in an object
|
|
588
|
+
*/
|
|
589
|
+
async decryptPHI(data) {
|
|
590
|
+
if (!data._encrypted)
|
|
591
|
+
return data;
|
|
592
|
+
const result = { ...data };
|
|
593
|
+
for (const [field, encrypted] of Object.entries(data._encrypted)) {
|
|
594
|
+
const decrypted = await this.decryptData(encrypted, field);
|
|
595
|
+
result[field] = JSON.parse(decrypted.toString('utf8'));
|
|
596
|
+
}
|
|
597
|
+
delete result._encrypted;
|
|
598
|
+
return result;
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Rotate encryption keys
|
|
602
|
+
*/
|
|
603
|
+
async rotateEncryptionKey() {
|
|
604
|
+
// Mark current key as inactive
|
|
605
|
+
const currentKey = this.encryptionKeys.get(this.activeKeyId);
|
|
606
|
+
if (currentKey) {
|
|
607
|
+
currentKey.active = false;
|
|
608
|
+
}
|
|
609
|
+
// Generate new key
|
|
610
|
+
const newKeyId = this.generateKeyId();
|
|
611
|
+
this.initializeEncryptionKey(newKeyId);
|
|
612
|
+
this.activeKeyId = newKeyId;
|
|
613
|
+
// Log key rotation
|
|
614
|
+
await this.logAuditEvent({
|
|
615
|
+
eventType: 'system-event',
|
|
616
|
+
action: 'update',
|
|
617
|
+
outcome: 'success',
|
|
618
|
+
actor: {
|
|
619
|
+
userId: 'system',
|
|
620
|
+
role: 'system',
|
|
621
|
+
ipAddress: 'localhost'
|
|
622
|
+
},
|
|
623
|
+
resource: {
|
|
624
|
+
type: 'configuration',
|
|
625
|
+
description: 'Encryption key rotation'
|
|
626
|
+
},
|
|
627
|
+
details: {
|
|
628
|
+
previousValues: { keyId: currentKey ? this.activeKeyId : 'none' },
|
|
629
|
+
newValues: { keyId: newKeyId }
|
|
630
|
+
},
|
|
631
|
+
security: {
|
|
632
|
+
authMethod: 'certificate',
|
|
633
|
+
encryptionUsed: true,
|
|
634
|
+
integrityVerified: true,
|
|
635
|
+
accessLevel: 'elevated'
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
return newKeyId;
|
|
639
|
+
}
|
|
640
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
641
|
+
// DATA MASKING (Minimum Necessary)
|
|
642
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
643
|
+
/**
|
|
644
|
+
* Mask PHI according to minimum necessary standard
|
|
645
|
+
*/
|
|
646
|
+
maskPHI(data, accessLevel) {
|
|
647
|
+
const masked = { ...data };
|
|
648
|
+
// Determine masking rules based on access level
|
|
649
|
+
const maskingRules = this.getMaskingRulesForLevel(accessLevel);
|
|
650
|
+
// Apply masking
|
|
651
|
+
if (masked.ssn && maskingRules.ssnPattern !== 'full') {
|
|
652
|
+
masked.ssn = this.maskSSN(masked.ssn, maskingRules.ssnPattern);
|
|
653
|
+
}
|
|
654
|
+
if (masked.dob && maskingRules.dobPattern !== 'full') {
|
|
655
|
+
masked.dob = this.maskDOB(masked.dob, maskingRules.dobPattern);
|
|
656
|
+
}
|
|
657
|
+
if (masked.phone && maskingRules.phonePattern !== 'full') {
|
|
658
|
+
masked.phone = this.maskPhone(masked.phone, maskingRules.phonePattern);
|
|
659
|
+
}
|
|
660
|
+
if (masked.address && maskingRules.addressPattern !== 'full') {
|
|
661
|
+
masked.address = this.maskAddress(masked.address, maskingRules.addressPattern);
|
|
662
|
+
}
|
|
663
|
+
if (masked.mrn && maskingRules.mrnPattern !== 'full') {
|
|
664
|
+
masked.mrn = this.maskMRN(masked.mrn, maskingRules.mrnPattern);
|
|
665
|
+
}
|
|
666
|
+
if (masked.genomics && maskingRules.genomicPattern !== 'full') {
|
|
667
|
+
masked.genomics = this.maskGenomics(masked.genomics, maskingRules.genomicPattern);
|
|
668
|
+
}
|
|
669
|
+
return masked;
|
|
670
|
+
}
|
|
671
|
+
getMaskingRulesForLevel(level) {
|
|
672
|
+
switch (level) {
|
|
673
|
+
case 'full':
|
|
674
|
+
return {
|
|
675
|
+
ssnPattern: 'full',
|
|
676
|
+
dobPattern: 'full',
|
|
677
|
+
phonePattern: 'full',
|
|
678
|
+
addressPattern: 'full',
|
|
679
|
+
mrnPattern: 'full',
|
|
680
|
+
genomicPattern: 'full'
|
|
681
|
+
};
|
|
682
|
+
case 'clinical':
|
|
683
|
+
return {
|
|
684
|
+
ssnPattern: 'last4',
|
|
685
|
+
dobPattern: 'full',
|
|
686
|
+
phonePattern: 'full',
|
|
687
|
+
addressPattern: 'full',
|
|
688
|
+
mrnPattern: 'full',
|
|
689
|
+
genomicPattern: 'full'
|
|
690
|
+
};
|
|
691
|
+
case 'billing':
|
|
692
|
+
return {
|
|
693
|
+
ssnPattern: 'last4',
|
|
694
|
+
dobPattern: 'full',
|
|
695
|
+
phonePattern: 'last4',
|
|
696
|
+
addressPattern: 'full',
|
|
697
|
+
mrnPattern: 'full',
|
|
698
|
+
genomicPattern: 'hidden'
|
|
699
|
+
};
|
|
700
|
+
case 'research':
|
|
701
|
+
return {
|
|
702
|
+
ssnPattern: 'hidden',
|
|
703
|
+
dobPattern: 'year-only',
|
|
704
|
+
phonePattern: 'hidden',
|
|
705
|
+
addressPattern: 'zip-only',
|
|
706
|
+
mrnPattern: 'hidden',
|
|
707
|
+
genomicPattern: 'summary'
|
|
708
|
+
};
|
|
709
|
+
case 'minimal':
|
|
710
|
+
default:
|
|
711
|
+
return {
|
|
712
|
+
ssnPattern: 'hidden',
|
|
713
|
+
dobPattern: 'age-only',
|
|
714
|
+
phonePattern: 'hidden',
|
|
715
|
+
addressPattern: 'hidden',
|
|
716
|
+
mrnPattern: 'hidden',
|
|
717
|
+
genomicPattern: 'hidden'
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
maskSSN(ssn, pattern) {
|
|
722
|
+
const cleaned = ssn.replace(/\D/g, '');
|
|
723
|
+
switch (pattern) {
|
|
724
|
+
case 'last4':
|
|
725
|
+
return `***-**-${cleaned.slice(-4)}`;
|
|
726
|
+
case 'hidden':
|
|
727
|
+
return '***-**-****';
|
|
728
|
+
default:
|
|
729
|
+
return ssn;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
maskDOB(dob, pattern) {
|
|
733
|
+
const date = typeof dob === 'string' ? new Date(dob) : dob;
|
|
734
|
+
switch (pattern) {
|
|
735
|
+
case 'year-only':
|
|
736
|
+
return date.getFullYear().toString();
|
|
737
|
+
case 'age-only':
|
|
738
|
+
const age = Math.floor((Date.now() - date.getTime()) / (365.25 * 24 * 60 * 60 * 1000));
|
|
739
|
+
return `Age: ${age}`;
|
|
740
|
+
case 'hidden':
|
|
741
|
+
return '[REDACTED]';
|
|
742
|
+
default:
|
|
743
|
+
return typeof dob === 'string' ? dob : date.toISOString().split('T')[0];
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
maskPhone(phone, pattern) {
|
|
747
|
+
const cleaned = phone.replace(/\D/g, '');
|
|
748
|
+
switch (pattern) {
|
|
749
|
+
case 'last4':
|
|
750
|
+
return `(***) ***-${cleaned.slice(-4)}`;
|
|
751
|
+
case 'hidden':
|
|
752
|
+
return '(***) ***-****';
|
|
753
|
+
default:
|
|
754
|
+
return phone;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
maskAddress(address, pattern) {
|
|
758
|
+
if (typeof address === 'string') {
|
|
759
|
+
switch (pattern) {
|
|
760
|
+
case 'city-state':
|
|
761
|
+
return '[City, State]';
|
|
762
|
+
case 'zip-only':
|
|
763
|
+
const zipMatch = address.match(/\d{5}(-\d{4})?/);
|
|
764
|
+
return zipMatch ? zipMatch[0] : '[ZIP]';
|
|
765
|
+
case 'hidden':
|
|
766
|
+
return '[REDACTED]';
|
|
767
|
+
default:
|
|
768
|
+
return address;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
// Object address
|
|
772
|
+
switch (pattern) {
|
|
773
|
+
case 'city-state':
|
|
774
|
+
return { city: address.city, state: address.state };
|
|
775
|
+
case 'zip-only':
|
|
776
|
+
return { zip: address.zip || address.postalCode };
|
|
777
|
+
case 'hidden':
|
|
778
|
+
return '[REDACTED]';
|
|
779
|
+
default:
|
|
780
|
+
return address;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
maskMRN(mrn, pattern) {
|
|
784
|
+
switch (pattern) {
|
|
785
|
+
case 'last4':
|
|
786
|
+
return `****${mrn.slice(-4)}`;
|
|
787
|
+
case 'hidden':
|
|
788
|
+
return '[REDACTED]';
|
|
789
|
+
default:
|
|
790
|
+
return mrn;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
maskGenomics(genomics, pattern) {
|
|
794
|
+
switch (pattern) {
|
|
795
|
+
case 'summary':
|
|
796
|
+
if (Array.isArray(genomics)) {
|
|
797
|
+
return { count: genomics.length, summary: 'Genomic data available' };
|
|
798
|
+
}
|
|
799
|
+
return { summary: 'Genomic data available' };
|
|
800
|
+
case 'hidden':
|
|
801
|
+
return '[GENOMIC DATA REDACTED]';
|
|
802
|
+
default:
|
|
803
|
+
return genomics;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
807
|
+
// HELPER METHODS
|
|
808
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
809
|
+
generateAuditId() {
|
|
810
|
+
return `AUD-${Date.now()}-${randomBytes(4).toString('hex')}`;
|
|
811
|
+
}
|
|
812
|
+
generateConsentId() {
|
|
813
|
+
return `CON-${Date.now()}-${randomBytes(4).toString('hex')}`;
|
|
814
|
+
}
|
|
815
|
+
generatePolicyId() {
|
|
816
|
+
return `POL-${Date.now()}-${randomBytes(4).toString('hex')}`;
|
|
817
|
+
}
|
|
818
|
+
generateKeyId() {
|
|
819
|
+
return `KEY-${Date.now()}-${randomBytes(4).toString('hex')}`;
|
|
820
|
+
}
|
|
821
|
+
async initializeEncryptionKey(keyId) {
|
|
822
|
+
const id = keyId || this.activeKeyId;
|
|
823
|
+
// In production, this would use a proper key management system (KMS)
|
|
824
|
+
// For now, derive from a master secret
|
|
825
|
+
const masterSecret = process.env.HIPAA_MASTER_KEY || 'DEFAULT_DEV_KEY_CHANGE_IN_PRODUCTION';
|
|
826
|
+
return new Promise((resolve, reject) => {
|
|
827
|
+
scrypt(masterSecret, id, 32, (err, derivedKey) => {
|
|
828
|
+
if (err) {
|
|
829
|
+
reject(err);
|
|
830
|
+
}
|
|
831
|
+
else {
|
|
832
|
+
this.encryptionKeys.set(id, {
|
|
833
|
+
key: derivedKey,
|
|
834
|
+
createdAt: new Date(),
|
|
835
|
+
active: true
|
|
836
|
+
});
|
|
837
|
+
resolve();
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
calculateEntryHash(entry) {
|
|
843
|
+
const content = JSON.stringify({
|
|
844
|
+
...entry,
|
|
845
|
+
timestamp: entry.timestamp.toISOString()
|
|
846
|
+
});
|
|
847
|
+
return createHash('sha256').update(content).digest('hex');
|
|
848
|
+
}
|
|
849
|
+
async detectSuspiciousActivity(entry) {
|
|
850
|
+
// Check for patterns that might indicate security issues
|
|
851
|
+
const suspiciousPatterns = [
|
|
852
|
+
{
|
|
853
|
+
pattern: () => entry.action === 'login-failed',
|
|
854
|
+
alert: 'Failed login attempt'
|
|
855
|
+
},
|
|
856
|
+
{
|
|
857
|
+
pattern: () => entry.security.accessLevel === 'emergency',
|
|
858
|
+
alert: 'Emergency access used'
|
|
859
|
+
},
|
|
860
|
+
{
|
|
861
|
+
pattern: () => {
|
|
862
|
+
// Check for after-hours access
|
|
863
|
+
const hour = entry.timestamp.getHours();
|
|
864
|
+
return (hour < 6 || hour > 22) && entry.eventType === 'access';
|
|
865
|
+
},
|
|
866
|
+
alert: 'After-hours PHI access'
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
pattern: () => {
|
|
870
|
+
// Check for excessive access
|
|
871
|
+
const recentAccess = this.auditStore.filter(e => e.actor.userId === entry.actor.userId &&
|
|
872
|
+
e.eventType === 'access' &&
|
|
873
|
+
e.timestamp.getTime() > Date.now() - 60000 // Last minute
|
|
874
|
+
);
|
|
875
|
+
return recentAccess.length > 50;
|
|
876
|
+
},
|
|
877
|
+
alert: 'Excessive PHI access rate detected'
|
|
878
|
+
}
|
|
879
|
+
];
|
|
880
|
+
for (const { pattern, alert } of suspiciousPatterns) {
|
|
881
|
+
if (pattern()) {
|
|
882
|
+
this.emit('security-alert', {
|
|
883
|
+
timestamp: new Date(),
|
|
884
|
+
alert,
|
|
885
|
+
auditEntryId: entry.id,
|
|
886
|
+
actor: entry.actor,
|
|
887
|
+
severity: 'warning'
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Export audit logs for compliance reporting
|
|
894
|
+
*/
|
|
895
|
+
exportAuditLogs(format, params) {
|
|
896
|
+
const { entries } = this.queryAuditLogs({
|
|
897
|
+
...params,
|
|
898
|
+
limit: 100000
|
|
899
|
+
});
|
|
900
|
+
if (format === 'json') {
|
|
901
|
+
return JSON.stringify(entries, null, 2);
|
|
902
|
+
}
|
|
903
|
+
// CSV format
|
|
904
|
+
const headers = [
|
|
905
|
+
'ID', 'Timestamp', 'Event Type', 'Action', 'Outcome',
|
|
906
|
+
'User ID', 'User Role', 'IP Address',
|
|
907
|
+
'Resource Type', 'Patient ID', 'Access Level'
|
|
908
|
+
];
|
|
909
|
+
const rows = entries.map(e => [
|
|
910
|
+
e.id,
|
|
911
|
+
e.timestamp.toISOString(),
|
|
912
|
+
e.eventType,
|
|
913
|
+
e.action,
|
|
914
|
+
e.outcome,
|
|
915
|
+
e.actor.userId,
|
|
916
|
+
e.actor.role,
|
|
917
|
+
e.actor.ipAddress,
|
|
918
|
+
e.resource.type,
|
|
919
|
+
e.resource.patientId || '',
|
|
920
|
+
e.security.accessLevel
|
|
921
|
+
]);
|
|
922
|
+
return [
|
|
923
|
+
headers.join(','),
|
|
924
|
+
...rows.map(r => r.map(v => `"${v}"`).join(','))
|
|
925
|
+
].join('\n');
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
export default HIPAAComplianceService;
|
|
929
|
+
//# sourceMappingURL=hipaa.js.map
|