// routes/archerTickets.js const express = require('express'); const { requireAuth, requireRole } = require('../middleware/auth'); const logAudit = require('../helpers/auditLog'); // Validation helpers const CVE_ID_PATTERN = /^CVE-\d{4}-\d{4,}$/; function isValidCveId(cveId) { return typeof cveId === 'string' && CVE_ID_PATTERN.test(cveId); } function isValidVendor(vendor) { return typeof vendor === 'string' && vendor.trim().length > 0 && vendor.length <= 200; } function createArcherTicketsRouter(db) { const router = express.Router(); // Get all Archer tickets (with optional filters) router.get('/', requireAuth(db), (req, res) => { const { cve_id, vendor, status } = req.query; let query = 'SELECT * FROM archer_tickets WHERE 1=1'; const params = []; if (cve_id) { query += ' AND cve_id = ?'; params.push(cve_id); } if (vendor) { query += ' AND vendor = ?'; params.push(vendor); } if (status) { query += ' AND status = ?'; params.push(status); } query += ' ORDER BY created_at DESC'; db.all(query, params, (err, rows) => { if (err) { console.error('Error fetching Archer tickets:', err); return res.status(500).json({ error: 'Internal server error.' }); } res.json(rows); }); }); // Create Archer ticket router.post('/', requireAuth(db), requireRole('editor', 'admin'), (req, res) => { const { exc_number, archer_url, status, cve_id, vendor } = req.body; // Validation if (!exc_number || typeof exc_number !== 'string' || exc_number.trim().length === 0) { return res.status(400).json({ error: 'EXC number is required.' }); } if (!/^EXC-\d+$/.test(exc_number.trim())) { return res.status(400).json({ error: 'EXC number must be in format EXC-XXXX (e.g., EXC-5754).' }); } if (!cve_id || !isValidCveId(cve_id)) { return res.status(400).json({ error: 'Valid CVE ID is required.' }); } if (!vendor || !isValidVendor(vendor)) { return res.status(400).json({ error: 'Valid vendor is required.' }); } if (archer_url && (typeof archer_url !== 'string' || archer_url.length > 500)) { return res.status(400).json({ error: 'Archer URL must be under 500 characters.' }); } if (status && !['Draft', 'Open', 'Under Review', 'Accepted'].includes(status)) { return res.status(400).json({ error: 'Invalid status. Must be Draft, Open, Under Review, or Accepted.' }); } const validatedStatus = status || 'Draft'; db.run( `INSERT INTO archer_tickets (exc_number, archer_url, status, cve_id, vendor) VALUES (?, ?, ?, ?, ?)`, [exc_number.trim(), archer_url || null, validatedStatus, cve_id, vendor], function(err) { if (err) { console.error('Error creating Archer ticket:', err); if (err.message.includes('UNIQUE constraint failed')) { return res.status(409).json({ error: 'An Archer ticket with this EXC number already exists.' }); } return res.status(500).json({ error: 'Internal server error.' }); } logAudit(db, { userId: req.user.id, action: 'CREATE_ARCHER_TICKET', targetType: 'archer_ticket', targetId: this.lastID, details: { exc_number, archer_url, status: validatedStatus, cve_id, vendor }, ipAddress: req.ip }); res.status(201).json({ id: this.lastID, message: 'Archer ticket created successfully' }); } ); }); // Update Archer ticket router.put('/:id', requireAuth(db), requireRole('editor', 'admin'), (req, res) => { const { id } = req.params; const { exc_number, archer_url, status } = req.body; // Validation if (exc_number !== undefined) { if (typeof exc_number !== 'string' || exc_number.trim().length === 0) { return res.status(400).json({ error: 'EXC number cannot be empty.' }); } if (!/^EXC-\d+$/.test(exc_number.trim())) { return res.status(400).json({ error: 'EXC number must be in format EXC-XXXX (e.g., EXC-5754).' }); } } if (archer_url !== undefined && archer_url !== null && (typeof archer_url !== 'string' || archer_url.length > 500)) { return res.status(400).json({ error: 'Archer URL must be under 500 characters.' }); } if (status !== undefined && !['Draft', 'Open', 'Under Review', 'Accepted'].includes(status)) { return res.status(400).json({ error: 'Invalid status. Must be Draft, Open, Under Review, or Accepted.' }); } // Get existing ticket db.get('SELECT * FROM archer_tickets WHERE id = ?', [id], (err, existing) => { if (err) { console.error(err); return res.status(500).json({ error: 'Internal server error.' }); } if (!existing) { return res.status(404).json({ error: 'Archer ticket not found.' }); } const updates = []; const params = []; if (exc_number !== undefined) { updates.push('exc_number = ?'); params.push(exc_number.trim()); } if (archer_url !== undefined) { updates.push('archer_url = ?'); params.push(archer_url || null); } if (status !== undefined) { updates.push('status = ?'); params.push(status); } if (updates.length === 0) { return res.status(400).json({ error: 'No fields to update.' }); } updates.push('updated_at = CURRENT_TIMESTAMP'); params.push(id); db.run( `UPDATE archer_tickets SET ${updates.join(', ')} WHERE id = ?`, params, function(err) { if (err) { console.error(err); if (err.message.includes('UNIQUE constraint failed')) { return res.status(409).json({ error: 'An Archer ticket with this EXC number already exists.' }); } return res.status(500).json({ error: 'Internal server error.' }); } logAudit(db, { userId: req.user.id, action: 'UPDATE_ARCHER_TICKET', targetType: 'archer_ticket', targetId: id, details: { before: existing, changes: req.body }, ipAddress: req.ip }); res.json({ message: 'Archer ticket updated successfully', changes: this.changes }); } ); }); }); // Delete Archer ticket router.delete('/:id', requireAuth(db), requireRole('editor', 'admin'), (req, res) => { const { id } = req.params; db.get('SELECT * FROM archer_tickets WHERE id = ?', [id], (err, ticket) => { if (err) { console.error(err); return res.status(500).json({ error: 'Internal server error.' }); } if (!ticket) { return res.status(404).json({ error: 'Archer ticket not found.' }); } db.run('DELETE FROM archer_tickets WHERE id = ?', [id], function(err) { if (err) { console.error(err); return res.status(500).json({ error: 'Internal server error.' }); } logAudit(db, { userId: req.user.id, action: 'DELETE_ARCHER_TICKET', targetType: 'archer_ticket', targetId: id, details: { deleted: ticket }, ipAddress: req.ip }); res.json({ message: 'Archer ticket deleted successfully' }); }); }); }); return router; } module.exports = createArcherTicketsRouter;