feat: implement finding archive tracking system
- Add migration script for ivanti_finding_archives and ivanti_archive_transitions tables - Add archive detection logic (detectArchiveChanges, detectClosedFindings) in sync pipeline - Add archive API router with list, stats, and history endpoints at /api/ivanti/archive - Add ArchiveSummaryBar UI component with four state cards (ACTIVE, ARCHIVED, RETURNED, CLOSED) - Integrate ArchiveSummaryBar into Ivanti findings page in App.js - Register archive router in server.js
This commit is contained in:
75
backend/migrations/add_finding_archive_tables.js
Normal file
75
backend/migrations/add_finding_archive_tables.js
Normal file
@@ -0,0 +1,75 @@
|
||||
// Migration: Add ivanti_finding_archives and ivanti_archive_transitions tables
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const path = require('path');
|
||||
|
||||
const dbPath = path.join(__dirname, '..', 'cve_database.db');
|
||||
const db = new sqlite3.Database(dbPath);
|
||||
|
||||
console.log('Starting finding archive tables migration...');
|
||||
|
||||
db.serialize(() => {
|
||||
// Archive records — one row per finding that has entered the archive lifecycle
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS ivanti_finding_archives (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
finding_id TEXT NOT NULL UNIQUE,
|
||||
finding_title TEXT NOT NULL DEFAULT '',
|
||||
host_name TEXT NOT NULL DEFAULT '',
|
||||
ip_address TEXT NOT NULL DEFAULT '',
|
||||
current_state TEXT NOT NULL CHECK(current_state IN ('ACTIVE', 'ARCHIVED', 'RETURNED', 'CLOSED')),
|
||||
last_severity REAL NOT NULL DEFAULT 0,
|
||||
first_archived_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
last_transition_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`, (err) => {
|
||||
if (err) console.error('Error creating ivanti_finding_archives table:', err);
|
||||
else console.log('✓ ivanti_finding_archives table created');
|
||||
});
|
||||
|
||||
// Transition history — one row per state change on an archive record
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS ivanti_archive_transitions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
archive_id INTEGER NOT NULL,
|
||||
from_state TEXT NOT NULL,
|
||||
to_state TEXT NOT NULL,
|
||||
severity_at_transition REAL NOT NULL DEFAULT 0,
|
||||
reason TEXT NOT NULL DEFAULT '',
|
||||
transitioned_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (archive_id) REFERENCES ivanti_finding_archives(id)
|
||||
)
|
||||
`, (err) => {
|
||||
if (err) console.error('Error creating ivanti_archive_transitions table:', err);
|
||||
else console.log('✓ ivanti_archive_transitions table created');
|
||||
});
|
||||
|
||||
// Indexes for query performance
|
||||
db.run(`
|
||||
CREATE INDEX IF NOT EXISTS idx_archive_finding_id
|
||||
ON ivanti_finding_archives(finding_id)
|
||||
`, (err) => {
|
||||
if (err) console.error('Error creating idx_archive_finding_id:', err);
|
||||
else console.log('✓ idx_archive_finding_id index created');
|
||||
});
|
||||
|
||||
db.run(`
|
||||
CREATE INDEX IF NOT EXISTS idx_archive_current_state
|
||||
ON ivanti_finding_archives(current_state)
|
||||
`, (err) => {
|
||||
if (err) console.error('Error creating idx_archive_current_state:', err);
|
||||
else console.log('✓ idx_archive_current_state index created');
|
||||
});
|
||||
|
||||
db.run(`
|
||||
CREATE INDEX IF NOT EXISTS idx_transition_archive_id
|
||||
ON ivanti_archive_transitions(archive_id)
|
||||
`, (err) => {
|
||||
if (err) console.error('Error creating idx_transition_archive_id:', err);
|
||||
else console.log('✓ idx_transition_archive_id index created');
|
||||
});
|
||||
});
|
||||
|
||||
db.close(() => {
|
||||
console.log('Migration complete!');
|
||||
});
|
||||
122
backend/routes/ivantiArchive.js
Normal file
122
backend/routes/ivantiArchive.js
Normal file
@@ -0,0 +1,122 @@
|
||||
// Ivanti Archive Routes — list, stats, and transition history for archived findings
|
||||
const express = require('express');
|
||||
|
||||
const VALID_STATES = ['ACTIVE', 'ARCHIVED', 'RETURNED', 'CLOSED'];
|
||||
|
||||
function createIvantiArchiveRouter(db, requireAuth) {
|
||||
const router = express.Router();
|
||||
|
||||
// All routes require authentication
|
||||
router.use(requireAuth(db));
|
||||
|
||||
// GET / — List archive records with optional ?state= filter
|
||||
router.get('/', async (req, res) => {
|
||||
const { state } = req.query;
|
||||
|
||||
if (state && !VALID_STATES.includes(state)) {
|
||||
return res.status(400).json({
|
||||
error: 'Invalid state parameter. Valid values: ACTIVE, ARCHIVED, RETURNED, CLOSED'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
let query = 'SELECT * FROM ivanti_finding_archives';
|
||||
const params = [];
|
||||
|
||||
if (state) {
|
||||
query += ' WHERE current_state = ?';
|
||||
params.push(state);
|
||||
}
|
||||
|
||||
query += ' ORDER BY last_transition_at DESC';
|
||||
|
||||
const archives = await new Promise((resolve, reject) => {
|
||||
db.all(query, params, (err, rows) => {
|
||||
if (err) reject(err);
|
||||
else resolve(rows || []);
|
||||
});
|
||||
});
|
||||
|
||||
res.json({ archives, total: archives.length });
|
||||
} catch (err) {
|
||||
console.error('Archive list error:', err);
|
||||
res.status(500).json({ error: 'Failed to fetch archive records' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /stats — Summary counts by state
|
||||
router.get('/stats', async (req, res) => {
|
||||
try {
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
db.all(
|
||||
`SELECT current_state, COUNT(*) as count
|
||||
FROM ivanti_finding_archives
|
||||
GROUP BY current_state`,
|
||||
(err, rows) => {
|
||||
if (err) reject(err);
|
||||
else resolve(rows || []);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const stats = { ACTIVE: 0, ARCHIVED: 0, RETURNED: 0, CLOSED: 0 };
|
||||
let total = 0;
|
||||
|
||||
for (const row of rows) {
|
||||
if (stats.hasOwnProperty(row.current_state)) {
|
||||
stats[row.current_state] = row.count;
|
||||
}
|
||||
total += row.count;
|
||||
}
|
||||
|
||||
res.json({ ...stats, total });
|
||||
} catch (err) {
|
||||
console.error('Archive stats error:', err);
|
||||
res.status(500).json({ error: 'Failed to fetch archive stats' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /:findingId/history — Transition history for a finding
|
||||
router.get('/:findingId/history', async (req, res) => {
|
||||
const { findingId } = req.params;
|
||||
|
||||
try {
|
||||
const archive = await new Promise((resolve, reject) => {
|
||||
db.get(
|
||||
'SELECT id FROM ivanti_finding_archives WHERE finding_id = ?',
|
||||
[findingId],
|
||||
(err, row) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
if (!archive) {
|
||||
return res.json({ finding_id: findingId, transitions: [] });
|
||||
}
|
||||
|
||||
const transitions = await new Promise((resolve, reject) => {
|
||||
db.all(
|
||||
`SELECT * FROM ivanti_archive_transitions
|
||||
WHERE archive_id = ?
|
||||
ORDER BY transitioned_at DESC`,
|
||||
[archive.id],
|
||||
(err, rows) => {
|
||||
if (err) reject(err);
|
||||
else resolve(rows || []);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
res.json({ finding_id: findingId, transitions });
|
||||
} catch (err) {
|
||||
console.error('Archive history error:', err);
|
||||
res.status(500).json({ error: 'Failed to fetch transition history' });
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
module.exports = createIvantiArchiveRouter;
|
||||
@@ -192,6 +192,201 @@ function initTables(db) {
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Archive table init — creates archive tracking tables alongside the main cache
|
||||
// ---------------------------------------------------------------------------
|
||||
function initArchiveTables(db) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.serialize(() => {
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS ivanti_finding_archives (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
finding_id TEXT NOT NULL UNIQUE,
|
||||
finding_title TEXT NOT NULL DEFAULT '',
|
||||
host_name TEXT NOT NULL DEFAULT '',
|
||||
ip_address TEXT NOT NULL DEFAULT '',
|
||||
current_state TEXT NOT NULL CHECK(current_state IN ('ARCHIVED','RETURNED','CLOSED')),
|
||||
last_severity REAL NOT NULL DEFAULT 0,
|
||||
first_archived_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
last_transition_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`, (err) => { if (err) return reject(err); });
|
||||
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS ivanti_archive_transitions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
archive_id INTEGER NOT NULL,
|
||||
from_state TEXT NOT NULL,
|
||||
to_state TEXT NOT NULL,
|
||||
severity_at_transition REAL NOT NULL DEFAULT 0,
|
||||
reason TEXT NOT NULL DEFAULT '',
|
||||
transitioned_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (archive_id) REFERENCES ivanti_finding_archives(id)
|
||||
)
|
||||
`, (err) => { if (err) return reject(err); });
|
||||
|
||||
db.run(`
|
||||
CREATE INDEX IF NOT EXISTS idx_archive_finding_id
|
||||
ON ivanti_finding_archives(finding_id)
|
||||
`, (err) => { if (err) return reject(err); });
|
||||
|
||||
db.run(`
|
||||
CREATE INDEX IF NOT EXISTS idx_archive_current_state
|
||||
ON ivanti_finding_archives(current_state)
|
||||
`, (err) => { if (err) return reject(err); });
|
||||
|
||||
db.run(`
|
||||
CREATE INDEX IF NOT EXISTS idx_transition_archive_id
|
||||
ON ivanti_archive_transitions(archive_id)
|
||||
`, (err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Archive detection — compare previous vs current findings to detect state changes
|
||||
// ---------------------------------------------------------------------------
|
||||
async function detectArchiveChanges(db, previousFindings, currentFindings) {
|
||||
const previousIds = new Set(previousFindings.map(f => String(f.id)));
|
||||
const currentIds = new Set(currentFindings.map(f => String(f.id)));
|
||||
|
||||
// Build lookup maps for metadata
|
||||
const previousMap = new Map(previousFindings.map(f => [String(f.id), f]));
|
||||
const currentMap = new Map(currentFindings.map(f => [String(f.id), f]));
|
||||
|
||||
// 1. Disappeared findings: in previous but not in current → ARCHIVED
|
||||
const disappearedIds = [...previousIds].filter(id => !currentIds.has(id));
|
||||
|
||||
for (const id of disappearedIds) {
|
||||
const finding = previousMap.get(id);
|
||||
const title = finding.title || '';
|
||||
const hostName = finding.hostName || '';
|
||||
const ipAddress = finding.ipAddress || '';
|
||||
const severity = typeof finding.severity === 'number' ? finding.severity : 0;
|
||||
|
||||
try {
|
||||
// Check if this finding already has an archive record
|
||||
const existing = await dbGet(db,
|
||||
`SELECT id, current_state FROM ivanti_finding_archives WHERE finding_id = ?`,
|
||||
[id]
|
||||
);
|
||||
|
||||
if (existing && existing.current_state === 'RETURNED') {
|
||||
// Re-disappeared: RETURNED → ARCHIVED
|
||||
await dbRun(db,
|
||||
`UPDATE ivanti_finding_archives
|
||||
SET current_state = 'ARCHIVED', last_severity = ?, last_transition_at = datetime('now')
|
||||
WHERE id = ?`,
|
||||
[severity, existing.id]
|
||||
);
|
||||
await dbRun(db,
|
||||
`INSERT INTO ivanti_archive_transitions (archive_id, from_state, to_state, severity_at_transition, reason, transitioned_at)
|
||||
VALUES (?, 'RETURNED', 'ARCHIVED', ?, 'severity_score_drift', datetime('now'))`,
|
||||
[existing.id, severity]
|
||||
);
|
||||
console.log(`[Archive Detection] Finding ${id} re-archived (RETURNED → ARCHIVED)`);
|
||||
} else if (!existing) {
|
||||
// First disappearance: NONE → ARCHIVED
|
||||
const result = await dbRun(db,
|
||||
`INSERT INTO ivanti_finding_archives (finding_id, finding_title, host_name, ip_address, current_state, last_severity, first_archived_at, last_transition_at)
|
||||
VALUES (?, ?, ?, ?, 'ARCHIVED', ?, datetime('now'), datetime('now'))`,
|
||||
[id, title, hostName, ipAddress, severity]
|
||||
);
|
||||
const archiveId = result.lastID;
|
||||
await dbRun(db,
|
||||
`INSERT INTO ivanti_archive_transitions (archive_id, from_state, to_state, severity_at_transition, reason, transitioned_at)
|
||||
VALUES (?, 'NONE', 'ARCHIVED', ?, 'severity_score_drift', datetime('now'))`,
|
||||
[archiveId, severity]
|
||||
);
|
||||
console.log(`[Archive Detection] Finding ${id} archived (NONE → ARCHIVED)`);
|
||||
}
|
||||
// If existing state is ARCHIVED or CLOSED, no action needed
|
||||
} catch (err) {
|
||||
console.error(`[Archive Detection] Error processing disappeared finding ${id}:`, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Returned findings: in current AND has existing ARCHIVED record → RETURNED
|
||||
const currentIdsList = [...currentIds];
|
||||
if (currentIdsList.length > 0) {
|
||||
try {
|
||||
const archivedRecords = await dbAll(db,
|
||||
`SELECT id, finding_id FROM ivanti_finding_archives WHERE current_state = 'ARCHIVED'`
|
||||
);
|
||||
|
||||
for (const record of archivedRecords) {
|
||||
if (currentIds.has(record.finding_id)) {
|
||||
const finding = currentMap.get(record.finding_id);
|
||||
const severity = typeof finding.severity === 'number' ? finding.severity : 0;
|
||||
|
||||
await dbRun(db,
|
||||
`UPDATE ivanti_finding_archives
|
||||
SET current_state = 'RETURNED', last_severity = ?, last_transition_at = datetime('now')
|
||||
WHERE id = ?`,
|
||||
[severity, record.id]
|
||||
);
|
||||
await dbRun(db,
|
||||
`INSERT INTO ivanti_archive_transitions (archive_id, from_state, to_state, severity_at_transition, reason, transitioned_at)
|
||||
VALUES (?, 'ARCHIVED', 'RETURNED', ?, 'reappeared_in_sync', datetime('now'))`,
|
||||
[record.id, severity]
|
||||
);
|
||||
console.log(`[Archive Detection] Finding ${record.finding_id} returned (ARCHIVED → RETURNED)`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Archive Detection] Error processing returned findings:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[Archive Detection] Processed ${disappearedIds.length} disappeared, checked ${currentIdsList.length} current findings against archive`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Closed finding detection — check archived/returned findings against Ivanti closed set
|
||||
// ---------------------------------------------------------------------------
|
||||
async function detectClosedFindings(db, closedFindingIds) {
|
||||
if (!closedFindingIds || closedFindingIds.length === 0) return;
|
||||
|
||||
const closedSet = new Set(closedFindingIds.map(String));
|
||||
|
||||
try {
|
||||
const records = await dbAll(db,
|
||||
`SELECT id, finding_id, current_state, last_severity FROM ivanti_finding_archives WHERE current_state IN ('ARCHIVED', 'RETURNED')`
|
||||
);
|
||||
|
||||
let closedCount = 0;
|
||||
for (const record of records) {
|
||||
if (!closedSet.has(record.finding_id)) continue;
|
||||
|
||||
try {
|
||||
await dbRun(db,
|
||||
`UPDATE ivanti_finding_archives
|
||||
SET current_state = 'CLOSED', last_transition_at = datetime('now')
|
||||
WHERE id = ?`,
|
||||
[record.id]
|
||||
);
|
||||
await dbRun(db,
|
||||
`INSERT INTO ivanti_archive_transitions (archive_id, from_state, to_state, severity_at_transition, reason, transitioned_at)
|
||||
VALUES (?, ?, 'CLOSED', ?, 'remediated_in_ivanti', datetime('now'))`,
|
||||
[record.id, record.current_state, record.last_severity || 0]
|
||||
);
|
||||
closedCount++;
|
||||
console.log(`[Archive Detection] Finding ${record.finding_id} closed (${record.current_state} → CLOSED)`);
|
||||
} catch (err) {
|
||||
console.error(`[Archive Detection] Error closing finding ${record.finding_id}:`, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[Archive Detection] Closed ${closedCount} findings as remediated`);
|
||||
} catch (err) {
|
||||
console.error('[Archive Detection] Error querying archive records for closed detection:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Extract only the fields we need from a raw finding object
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -266,7 +461,7 @@ async function syncClosedCount(db, openCount, apiKey, clientId, skipTls) {
|
||||
projection: 'internal',
|
||||
sort: [{ field: 'severity', direction: 'ASC' }],
|
||||
page: 0,
|
||||
size: 1
|
||||
size: 100
|
||||
};
|
||||
|
||||
const result = await ivantiPost(urlPath, body, apiKey, skipTls);
|
||||
@@ -275,6 +470,27 @@ async function syncClosedCount(db, openCount, apiKey, clientId, skipTls) {
|
||||
const data = JSON.parse(result.body);
|
||||
// RiskSense returns total in page.totalElements or page.total
|
||||
const closedCount = data.page?.totalElements ?? data.page?.total ?? 0;
|
||||
const totalPages = data.page?.totalPages || 1;
|
||||
|
||||
// Collect closed finding IDs for archive detection
|
||||
const closedFindingIds = [];
|
||||
const firstPageFindings = data._embedded?.hostFindings || [];
|
||||
firstPageFindings.forEach(f => { if (f.id) closedFindingIds.push(String(f.id)); });
|
||||
|
||||
// Fetch remaining pages to collect all closed finding IDs
|
||||
for (let pg = 1; pg < totalPages; pg++) {
|
||||
try {
|
||||
const pageBody = { ...body, page: pg };
|
||||
const pageResult = await ivantiPost(urlPath, pageBody, apiKey, skipTls);
|
||||
if (pageResult.status !== 200) break;
|
||||
const pageData = JSON.parse(pageResult.body);
|
||||
const pageFindings = pageData._embedded?.hostFindings || [];
|
||||
pageFindings.forEach(f => { if (f.id) closedFindingIds.push(String(f.id)); });
|
||||
} catch (err) {
|
||||
console.error(`[Ivanti Findings] Failed to fetch closed findings page ${pg}:`, err.message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await dbRun(db,
|
||||
`UPDATE ivanti_counts_cache SET open_count=?, closed_count=?, synced_at=datetime('now') WHERE id=1`,
|
||||
@@ -289,6 +505,13 @@ async function syncClosedCount(db, openCount, apiKey, clientId, skipTls) {
|
||||
);
|
||||
|
||||
console.log(`[Ivanti Findings] Counts updated — open: ${openCount}, closed: ${closedCount}`);
|
||||
|
||||
// Detect closed findings in the archive — wrap in try/catch so errors don't break sync
|
||||
try {
|
||||
await detectClosedFindings(db, closedFindingIds);
|
||||
} catch (err) {
|
||||
console.error('[Ivanti Findings] Closed finding archive detection failed (non-fatal):', err.message);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Ivanti Findings] Failed to fetch closed count:', err.message);
|
||||
// Still update open count so it stays in sync; leave closed_count as-is
|
||||
@@ -441,17 +664,36 @@ async function syncFindings(db) {
|
||||
page++;
|
||||
} while (page < totalPages);
|
||||
|
||||
// Read previous findings BEFORE updating the cache (they'll be overwritten)
|
||||
let previousFindings = [];
|
||||
try {
|
||||
const state = await readState(db);
|
||||
previousFindings = state.findings || [];
|
||||
} catch (err) {
|
||||
console.error('[Ivanti Findings] Failed to read previous findings for archive detection:', err.message);
|
||||
}
|
||||
|
||||
await dbRun(db,
|
||||
`UPDATE ivanti_findings_cache SET total=?, findings_json=?, synced_at=datetime('now'), sync_status='success', error_message=NULL WHERE id=1`,
|
||||
[allFindings.length, JSON.stringify(allFindings)]
|
||||
);
|
||||
|
||||
console.log(`[Ivanti Findings] Sync complete — ${allFindings.length} findings`);
|
||||
|
||||
// Archive detection — compare previous vs current to detect disappeared/returned findings
|
||||
// Only runs after a successful sync (skipped on error per requirement 1.5)
|
||||
try {
|
||||
await detectArchiveChanges(db, previousFindings, allFindings);
|
||||
} catch (err) {
|
||||
console.error('[Ivanti Findings] Archive detection failed (non-fatal):', err.message);
|
||||
}
|
||||
|
||||
await syncClosedCount(db, allFindings.length, apiKey, clientId, skipTls);
|
||||
await syncFPWorkflowCounts(db, allFindings, apiKey, clientId, skipTls);
|
||||
} catch (err) {
|
||||
const msg = err.message || 'Unknown error';
|
||||
console.error('[Ivanti Findings] Sync failed:', msg);
|
||||
// Archive detection is intentionally skipped on sync error (requirement 1.5)
|
||||
await dbRun(db, `UPDATE ivanti_findings_cache SET sync_status='error', error_message=?, synced_at=datetime('now') WHERE id=1`, [msg]);
|
||||
}
|
||||
}
|
||||
@@ -482,7 +724,19 @@ function scheduleSync(db) {
|
||||
// ---------------------------------------------------------------------------
|
||||
function dbRun(db, sql, params = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.run(sql, params, (err) => { if (err) reject(err); else resolve(); });
|
||||
db.run(sql, params, function (err) { if (err) reject(err); else resolve(this); });
|
||||
});
|
||||
}
|
||||
|
||||
function dbGet(db, sql, params = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.get(sql, params, (err, row) => { if (err) reject(err); else resolve(row); });
|
||||
});
|
||||
}
|
||||
|
||||
function dbAll(db, sql, params = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.all(sql, params, (err, rows) => { if (err) reject(err); else resolve(rows || []); });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -559,7 +813,7 @@ async function readStateWithNotes(db) {
|
||||
function createIvantiFindingsRouter(db, requireAuth) {
|
||||
const router = express.Router();
|
||||
|
||||
initTables(db)
|
||||
Promise.all([initTables(db), initArchiveTables(db)])
|
||||
.then(() => scheduleSync(db))
|
||||
.catch((err) => console.error('[Ivanti Findings] Init failed:', err));
|
||||
|
||||
@@ -700,3 +954,6 @@ function createIvantiFindingsRouter(db, requireAuth) {
|
||||
}
|
||||
|
||||
module.exports = createIvantiFindingsRouter;
|
||||
module.exports.detectArchiveChanges = detectArchiveChanges;
|
||||
module.exports.detectClosedFindings = detectClosedFindings;
|
||||
module.exports.initArchiveTables = initArchiveTables;
|
||||
|
||||
@@ -23,6 +23,7 @@ const createArcherTicketsRouter = require('./routes/archerTickets');
|
||||
const createIvantiWorkflowsRouter = require('./routes/ivantiWorkflows');
|
||||
const createIvantiFindingsRouter = require('./routes/ivantiFindings');
|
||||
const createIvantiTodoQueueRouter = require('./routes/ivantiTodoQueue');
|
||||
const createIvantiArchiveRouter = require('./routes/ivantiArchive');
|
||||
const createComplianceRouter = require('./routes/compliance');
|
||||
|
||||
const app = express();
|
||||
@@ -219,6 +220,9 @@ app.use('/api/ivanti/findings', createIvantiFindingsRouter(db, requireAuth));
|
||||
// Ivanti queue routes — per-user staging queue for FP / Archer workflows
|
||||
app.use('/api/ivanti/todo-queue', createIvantiTodoQueueRouter(db, requireAuth));
|
||||
|
||||
// Ivanti archive routes — finding archive tracking for severity score drift
|
||||
app.use('/api/ivanti/archive', createIvantiArchiveRouter(db, requireAuth));
|
||||
|
||||
// AEO compliance routes — xlsx upload, non-compliant item tracking, notes
|
||||
app.use('/api/compliance', createComplianceRouter(db, upload, requireAuth, requireRole));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user