// routes/archerTickets.js const express = require('express'); const { requireAuth, requireGroup } = 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), requireGroup('Admin', 'Standard_User'), (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, created_by) VALUES (?, ?, ?, ?, ?, ?)`, [exc_number.trim(), archer_url || null, validatedStatus, cve_id, vendor, req.user.id], 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), requireGroup('Admin', 'Standard_User'), (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 }); } ); }); }); // Helper: perform the actual Archer ticket deletion function performArcherDelete(db, req, res, id, ticket) { 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' }); }); } // Delete Archer ticket router.delete('/:id', requireAuth(db), requireGroup('Admin', 'Standard_User'), (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.' }); } // Admin bypasses all delete restrictions if (req.user.group === 'Admin') { return performArcherDelete(db, req, res, id, ticket); } // Standard_User: ownership check if (ticket.created_by && ticket.created_by !== req.user.id) { return res.status(403).json({ error: 'You can only delete resources you created' }); } // Standard_User: compliance linkage check const excNumber = ticket.exc_number; db.all( `SELECT ci.id, ci.extra_json FROM compliance_items ci JOIN compliance_uploads cu ON ci.upload_id = cu.id WHERE ci.status = 'active' AND ci.extra_json LIKE ?`, [`%${excNumber}%`], (compErr, compLinks) => { // If compliance_items table doesn't exist yet, treat as no linkage if (compErr && compErr.message && compErr.message.includes('no such table')) { compLinks = []; } else if (compErr) { console.error(compErr); return res.status(500).json({ error: 'Internal server error.' }); } const isLinked = (compLinks || []).some(cl => { const json = cl.extra_json || ''; return json.includes(excNumber); }); if (isLinked) { return res.status(403).json({ error: 'Cannot delete ticket linked to compliance report. Contact an admin.' }); } return performArcherDelete(db, req, res, id, ticket); } ); }); }); // GET /status-trend — ticket counts grouped by creation date + status // Used for time-based Archer pipeline chart on the Compliance page. router.get('/status-trend', requireAuth(db), (req, res) => { db.all( `SELECT DATE(created_at) AS date, status, COUNT(*) AS count FROM archer_tickets GROUP BY DATE(created_at), status ORDER BY date ASC`, [], (err, rows) => { if (err) { console.error('Error fetching Archer status trend:', err); return res.status(500).json({ error: 'Internal server error.' }); } res.json({ statusTrend: rows }); } ); }); return router; } module.exports = createArcherTicketsRouter;