Skip to content

Commit 660b786

Browse files
EPW80claude
andcommitted
security: persist HIPAA audit log to MongoDB (W1 #6)
createAuditLog() only console.logged with a "in a real HIPAA deployment we would persist this here" comment, and the API-access capture point was a TODO. For a HIPAA-framed project, an audit trail that vanishes on process exit is the most load-bearing gap in the repo. - Add the AuditLog Mongoose model: action/userId/address/requestId/ ip/userAgent/details/timestamp, immutable (no updatedAt), with compound indexes on (userId, timestamp), (address, timestamp), (action, timestamp). - Add auditLogService.write(): fire-and-forget, swallows its own errors so a DB hiccup never crashes request handling. - Wire createAuditLog() and the res.end API-access hook to it. Retention/tamper-evidence remain documented limitations — see SECURITY.md. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent b91a653 commit 660b786

3 files changed

Lines changed: 83 additions & 4 deletions

File tree

server/middleware/hipaaCompliance.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { createError } from "../errors/index.js";
33
import crypto from "crypto";
44
import validation from "../validation/index.js";
5+
import auditLogService from "../services/auditLogService.js";
56

67
// Function to create an audit log entry
78
const createAuditLog = async (action, details = {}) => {
@@ -12,9 +13,13 @@ const createAuditLog = async (action, details = {}) => {
1213
...details,
1314
};
1415

15-
// In a real HIPAA deployment we would persist this to a tamper-evident
16-
// audit store here. For now just log it.
17-
console.log(`[HIPAA AUDIT] ${action}:`, auditEntry);
16+
// Persist to MongoDB. auditLogService.write() swallows its own errors, so
17+
// a DB hiccup will never throw here.
18+
await auditLogService.write(action, auditEntry);
19+
20+
if (process.env.NODE_ENV !== "production") {
21+
console.log(`[HIPAA AUDIT] ${action}:`, auditEntry);
22+
}
1823

1924
return true;
2025
} catch (error) {
@@ -115,7 +120,8 @@ const hipaaCompliance = {
115120
);
116121
}
117122

118-
// TODO: persist every API access to a real audit store.
123+
// Persist every API access. Fire-and-forget — res.end must not block.
124+
auditLogService.write("API_ACCESS", auditEntry);
119125

120126
// Call the original end method
121127
originalEnd.apply(res, arguments);

server/models/AuditLog.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import mongoose from "mongoose";
2+
3+
const auditLogSchema = new mongoose.Schema(
4+
{
5+
action: {
6+
type: String,
7+
required: true,
8+
index: true,
9+
},
10+
userId: {
11+
type: String,
12+
index: true,
13+
},
14+
address: {
15+
type: String,
16+
lowercase: true,
17+
trim: true,
18+
index: true,
19+
},
20+
requestId: {
21+
type: String,
22+
},
23+
ipAddress: {
24+
type: String,
25+
},
26+
userAgent: {
27+
type: String,
28+
},
29+
details: {
30+
type: mongoose.Schema.Types.Mixed,
31+
},
32+
timestamp: {
33+
type: Date,
34+
default: Date.now,
35+
index: true,
36+
},
37+
},
38+
{
39+
// Audit logs are immutable — no updatedAt needed.
40+
timestamps: false,
41+
}
42+
);
43+
44+
// Compound indexes for the most common query patterns.
45+
auditLogSchema.index({ userId: 1, timestamp: -1 });
46+
auditLogSchema.index({ address: 1, timestamp: -1 });
47+
auditLogSchema.index({ action: 1, timestamp: -1 });
48+
49+
const AuditLog = mongoose.model("AuditLog", auditLogSchema);
50+
export default AuditLog;

server/services/auditLogService.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import AuditLog from "../models/AuditLog.js";
2+
import { logger } from "../config/loggerConfig.js";
3+
4+
// Audit failures must NEVER crash the application. Every write is fire-and-
5+
// forget from the caller's perspective; errors are logged but not rethrown.
6+
async function write(action, details = {}) {
7+
try {
8+
await AuditLog.create({
9+
action,
10+
userId: details.userId,
11+
address: details.address,
12+
requestId: details.requestId,
13+
ipAddress: details.ipAddress || details.ip,
14+
userAgent: details.userAgent,
15+
details,
16+
timestamp: details.timestamp ? new Date(details.timestamp) : new Date(),
17+
});
18+
} catch (err) {
19+
logger.error("Audit log write failed", { action, error: err.message });
20+
}
21+
}
22+
23+
export default { write };

0 commit comments

Comments
 (0)