Audit logging feature files

This commit is contained in:
2026-01-29 15:10:29 -07:00
parent 41c8a1ef27
commit 1a578b23c1
11 changed files with 964 additions and 21 deletions

View File

@@ -0,0 +1,21 @@
// Audit Log Helper
// Fire-and-forget insert - never blocks the response
function logAudit(db, { userId, username, action, entityType, entityId, details, ipAddress }) {
const detailsStr = details && typeof details === 'object'
? JSON.stringify(details)
: details || null;
db.run(
`INSERT INTO audit_logs (user_id, username, action, entity_type, entity_id, details, ip_address)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[userId || null, username || 'unknown', action, entityType, entityId || null, detailsStr, ipAddress || null],
(err) => {
if (err) {
console.error('Audit log error:', err.message);
}
}
);
}
module.exports = logAudit;

View File

@@ -0,0 +1,96 @@
#!/usr/bin/env node
// Migration script: Add audit_logs table
// Run: node migrate-audit-log.js
const sqlite3 = require('sqlite3').verbose();
const fs = require('fs');
const DB_FILE = './cve_database.db';
const BACKUP_FILE = `./cve_database_backup_${Date.now()}.db`;
function run(db, sql, params = []) {
return new Promise((resolve, reject) => {
db.run(sql, params, function(err) {
if (err) reject(err);
else resolve(this);
});
});
}
function get(db, sql, params = []) {
return new Promise((resolve, reject) => {
db.get(sql, params, (err, row) => {
if (err) reject(err);
else resolve(row);
});
});
}
async function migrate() {
console.log('╔════════════════════════════════════════════════════════╗');
console.log('║ CVE Database Migration: Add Audit Logs ║');
console.log('╚════════════════════════════════════════════════════════╝\n');
if (!fs.existsSync(DB_FILE)) {
console.log('❌ Database not found. Run setup.js for fresh install.');
process.exit(1);
}
// Backup database
console.log('📦 Creating backup...');
fs.copyFileSync(DB_FILE, BACKUP_FILE);
console.log(` ✓ Backup saved to: ${BACKUP_FILE}\n`);
const db = new sqlite3.Database(DB_FILE);
try {
// Check if table already exists
const exists = await get(db,
"SELECT name FROM sqlite_master WHERE type='table' AND name='audit_logs'"
);
if (exists) {
console.log('⏭️ audit_logs table already exists, nothing to do.');
} else {
console.log('1⃣ Creating audit_logs table...');
await run(db, `
CREATE TABLE audit_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
username VARCHAR(50) NOT NULL,
action VARCHAR(50) NOT NULL,
entity_type VARCHAR(50) NOT NULL,
entity_id VARCHAR(100),
details TEXT,
ip_address VARCHAR(45),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
console.log(' ✓ Table created');
console.log('2⃣ Creating indexes...');
await run(db, 'CREATE INDEX IF NOT EXISTS idx_audit_user_id ON audit_logs(user_id)');
await run(db, 'CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_logs(action)');
await run(db, 'CREATE INDEX IF NOT EXISTS idx_audit_entity_type ON audit_logs(entity_type)');
await run(db, 'CREATE INDEX IF NOT EXISTS idx_audit_created_at ON audit_logs(created_at)');
console.log(' ✓ Indexes created');
}
console.log('\n╔════════════════════════════════════════════════════════╗');
console.log('║ MIGRATION COMPLETE! ║');
console.log('╚════════════════════════════════════════════════════════╝');
console.log('\n📋 Summary:');
console.log(' ✓ audit_logs table ready');
console.log(`\n💾 Backup saved: ${BACKUP_FILE}`);
console.log('\n🚀 Restart your server to apply changes.\n');
} catch (error) {
console.error('\n❌ Migration failed:', error.message);
console.log(`\n🔄 To restore from backup: cp ${BACKUP_FILE} ${DB_FILE}`);
process.exit(1);
} finally {
db.close();
}
}
migrate();

114
backend/routes/auditLog.js Normal file
View File

@@ -0,0 +1,114 @@
// Audit Log Routes (Admin only)
const express = require('express');
function createAuditLogRouter(db, requireAuth, requireRole) {
const router = express.Router();
// All routes require admin role
router.use(requireAuth(db), requireRole('admin'));
// Get paginated audit logs with filters
router.get('/', async (req, res) => {
const {
page = 1,
limit = 25,
user,
action,
entityType,
startDate,
endDate
} = req.query;
const offset = (Math.max(1, parseInt(page)) - 1) * parseInt(limit);
const pageSize = Math.min(100, Math.max(1, parseInt(limit)));
let where = [];
let params = [];
if (user) {
where.push('username LIKE ?');
params.push(`%${user}%`);
}
if (action) {
where.push('action = ?');
params.push(action);
}
if (entityType) {
where.push('entity_type = ?');
params.push(entityType);
}
if (startDate) {
where.push('created_at >= ?');
params.push(startDate);
}
if (endDate) {
where.push('created_at <= ?');
params.push(endDate + ' 23:59:59');
}
const whereClause = where.length > 0 ? 'WHERE ' + where.join(' AND ') : '';
try {
// Get total count
const countRow = await new Promise((resolve, reject) => {
db.get(
`SELECT COUNT(*) as total FROM audit_logs ${whereClause}`,
params,
(err, row) => {
if (err) reject(err);
else resolve(row);
}
);
});
// Get paginated results
const rows = await new Promise((resolve, reject) => {
db.all(
`SELECT * FROM audit_logs ${whereClause} ORDER BY created_at DESC LIMIT ? OFFSET ?`,
[...params, pageSize, offset],
(err, rows) => {
if (err) reject(err);
else resolve(rows);
}
);
});
res.json({
logs: rows,
pagination: {
page: parseInt(page),
limit: pageSize,
total: countRow.total,
totalPages: Math.ceil(countRow.total / pageSize)
}
});
} catch (err) {
console.error('Audit log query error:', err);
res.status(500).json({ error: 'Failed to fetch audit logs' });
}
});
// Get distinct action types for filter dropdown
router.get('/actions', async (req, res) => {
try {
const rows = await new Promise((resolve, reject) => {
db.all(
'SELECT DISTINCT action FROM audit_logs ORDER BY action',
(err, rows) => {
if (err) reject(err);
else resolve(rows);
}
);
});
res.json(rows.map(r => r.action));
} catch (err) {
console.error('Audit log actions error:', err);
res.status(500).json({ error: 'Failed to fetch actions' });
}
});
return router;
}
module.exports = createAuditLogRouter;

View File

@@ -3,7 +3,7 @@ const express = require('express');
const bcrypt = require('bcryptjs');
const crypto = require('crypto');
function createAuthRouter(db) {
function createAuthRouter(db, logAudit) {
const router = express.Router();
// Login
@@ -28,16 +28,43 @@ function createAuthRouter(db) {
});
if (!user) {
logAudit(db, {
userId: null,
username: username,
action: 'login_failed',
entityType: 'auth',
entityId: null,
details: { reason: 'user_not_found' },
ipAddress: req.ip
});
return res.status(401).json({ error: 'Invalid username or password' });
}
if (!user.is_active) {
logAudit(db, {
userId: user.id,
username: username,
action: 'login_failed',
entityType: 'auth',
entityId: null,
details: { reason: 'account_disabled' },
ipAddress: req.ip
});
return res.status(401).json({ error: 'Account is disabled' });
}
// Verify password
const validPassword = await bcrypt.compare(password, user.password_hash);
if (!validPassword) {
logAudit(db, {
userId: user.id,
username: username,
action: 'login_failed',
entityType: 'auth',
entityId: null,
details: { reason: 'invalid_password' },
ipAddress: req.ip
});
return res.status(401).json({ error: 'Invalid username or password' });
}
@@ -77,6 +104,16 @@ function createAuthRouter(db) {
maxAge: 24 * 60 * 60 * 1000 // 24 hours
});
logAudit(db, {
userId: user.id,
username: user.username,
action: 'login',
entityType: 'auth',
entityId: null,
details: { role: user.role },
ipAddress: req.ip
});
res.json({
message: 'Login successful',
user: {
@@ -97,6 +134,17 @@ function createAuthRouter(db) {
const sessionId = req.cookies?.session_id;
if (sessionId) {
// Look up user before deleting session
const session = await new Promise((resolve) => {
db.get(
`SELECT u.id as user_id, u.username FROM sessions s
JOIN users u ON s.user_id = u.id
WHERE s.session_id = ?`,
[sessionId],
(err, row) => resolve(row || null)
);
});
// Delete session from database
await new Promise((resolve) => {
db.run(
@@ -105,6 +153,18 @@ function createAuthRouter(db) {
() => resolve()
);
});
if (session) {
logAudit(db, {
userId: session.user_id,
username: session.username,
action: 'logout',
entityType: 'auth',
entityId: null,
details: null,
ipAddress: req.ip
});
}
}
// Clear cookie

View File

@@ -2,7 +2,7 @@
const express = require('express');
const bcrypt = require('bcryptjs');
function createUsersRouter(db, requireAuth, requireRole) {
function createUsersRouter(db, requireAuth, requireRole, logAudit) {
const router = express.Router();
// All routes require admin role
@@ -81,6 +81,16 @@ function createUsersRouter(db, requireAuth, requireRole) {
);
});
logAudit(db, {
userId: req.user.id,
username: req.user.username,
action: 'user_create',
entityType: 'user',
entityId: String(result.id),
details: { created_username: username, role: role || 'viewer' },
ipAddress: req.ip
});
res.status(201).json({
message: 'User created successfully',
user: {
@@ -160,6 +170,23 @@ function createUsersRouter(db, requireAuth, requireRole) {
);
});
const updatedFields = {};
if (username) updatedFields.username = username;
if (email) updatedFields.email = email;
if (role) updatedFields.role = role;
if (typeof is_active === 'boolean') updatedFields.is_active = is_active;
if (password) updatedFields.password_changed = true;
logAudit(db, {
userId: req.user.id,
username: req.user.username,
action: 'user_update',
entityType: 'user',
entityId: String(userId),
details: updatedFields,
ipAddress: req.ip
});
// If user was deactivated, delete their sessions
if (is_active === false) {
await new Promise((resolve) => {
@@ -187,6 +214,14 @@ function createUsersRouter(db, requireAuth, requireRole) {
}
try {
// Look up the user before deleting
const targetUser = await new Promise((resolve, reject) => {
db.get('SELECT username FROM users WHERE id = ?', [userId], (err, row) => {
if (err) reject(err);
else resolve(row);
});
});
// Delete sessions first (foreign key)
await new Promise((resolve) => {
db.run('DELETE FROM sessions WHERE user_id = ?', [userId], () => resolve());
@@ -204,6 +239,16 @@ function createUsersRouter(db, requireAuth, requireRole) {
return res.status(404).json({ error: 'User not found' });
}
logAudit(db, {
userId: req.user.id,
username: req.user.username,
action: 'user_delete',
entityType: 'user',
entityId: String(userId),
details: { deleted_username: targetUser ? targetUser.username : 'unknown' },
ipAddress: req.ip
});
res.json({ message: 'User deleted successfully' });
} catch (err) {
console.error('Delete user error:', err);

View File

@@ -15,6 +15,8 @@ const fs = require('fs');
const { requireAuth, requireRole } = require('./middleware/auth');
const createAuthRouter = require('./routes/auth');
const createUsersRouter = require('./routes/users');
const createAuditLogRouter = require('./routes/auditLog');
const logAudit = require('./helpers/auditLog');
const app = express();
const PORT = process.env.PORT || 3001;
@@ -46,10 +48,13 @@ const db = new sqlite3.Database('./cve_database.db', (err) => {
});
// Auth routes (public)
app.use('/api/auth', createAuthRouter(db));
app.use('/api/auth', createAuthRouter(db, logAudit));
// User management routes (admin only)
app.use('/api/users', createUsersRouter(db, requireAuth, requireRole));
app.use('/api/users', createUsersRouter(db, requireAuth, requireRole, logAudit));
// Audit log routes (admin only)
app.use('/api/audit-logs', createAuditLogRouter(db, requireAuth, requireRole));
// Simple storage - upload to temp directory first
const storage = multer.diskStorage({
@@ -215,10 +220,19 @@ app.post('/api/cves', requireAuth(db), requireRole('editor', 'admin'), (req, res
}
return res.status(500).json({ error: err.message });
}
res.json({
id: this.lastID,
logAudit(db, {
userId: req.user.id,
username: req.user.username,
action: 'cve_create',
entityType: 'cve',
entityId: cve_id,
details: { vendor, severity },
ipAddress: req.ip
});
res.json({
id: this.lastID,
cve_id,
message: `CVE created successfully for vendor: ${vendor}`
message: `CVE created successfully for vendor: ${vendor}`
});
});
});
@@ -230,12 +244,20 @@ app.patch('/api/cves/:cveId/status', requireAuth(db), requireRole('editor', 'adm
const { status } = req.body;
const query = `UPDATE cves SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE cve_id = ?`;
db.run(query, [
vendor,status, cveId], function(err) {
db.run(query, [status, cveId], function(err) {
if (err) {
return res.status(500).json({ error: err.message });
}
logAudit(db, {
userId: req.user.id,
username: req.user.username,
action: 'cve_update_status',
entityType: 'cve',
entityId: cveId,
details: { status },
ipAddress: req.ip
});
res.json({ message: 'Status updated successfully', changes: this.changes });
});
});
@@ -329,6 +351,15 @@ app.post('/api/cves/:cveId/documents', requireAuth(db), requireRole('editor', 'a
}
return res.status(500).json({ error: err.message });
}
logAudit(db, {
userId: req.user.id,
username: req.user.username,
action: 'document_upload',
entityType: 'document',
entityId: cveId,
details: { vendor, type, filename: file.originalname },
ipAddress: req.ip
});
res.json({
id: this.lastID,
message: 'Document uploaded successfully',
@@ -359,6 +390,15 @@ app.delete('/api/documents/:id', requireAuth(db), requireRole('admin'), (req, re
if (err) {
return res.status(500).json({ error: err.message });
}
logAudit(db, {
userId: req.user.id,
username: req.user.username,
action: 'document_delete',
entityType: 'document',
entityId: id,
details: { file_path: row ? row.file_path : null },
ipAddress: req.ip
});
res.json({ message: 'Document deleted successfully' });
});
});

View File

@@ -88,6 +88,24 @@ function initializeDatabase() {
CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
-- Audit log table for tracking user actions
CREATE TABLE IF NOT EXISTS audit_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
username VARCHAR(50) NOT NULL,
action VARCHAR(50) NOT NULL,
entity_type VARCHAR(50) NOT NULL,
entity_id VARCHAR(100),
details TEXT,
ip_address VARCHAR(45),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_audit_user_id ON audit_logs(user_id);
CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_logs(action);
CREATE INDEX IF NOT EXISTS idx_audit_entity_type ON audit_logs(entity_type);
CREATE INDEX IF NOT EXISTS idx_audit_created_at ON audit_logs(created_at);
INSERT OR IGNORE INTO required_documents (vendor, document_type, is_mandatory, description) VALUES
('Microsoft', 'advisory', 1, 'Official Microsoft Security Advisory'),
('Microsoft', 'screenshot', 0, 'Proof of patch application'),
@@ -244,7 +262,7 @@ function displaySummary() {
console.log('╚════════════════════════════════════════════════════════╝');
console.log('\n📊 What was created:');
console.log(' ✓ SQLite database (cve_database.db)');
console.log(' ✓ Tables: cves, documents, required_documents, users, sessions');
console.log(' ✓ Tables: cves, documents, required_documents, users, sessions, audit_logs');
console.log(' ✓ Multi-vendor support with UNIQUE(cve_id, vendor)');
console.log(' ✓ Vendor column in documents table');
console.log(' ✓ User authentication with session-based auth');