feat(triage): Ivanti findings trend chart + rename Reporting to Vulnerability Triage
Add time-based open/closed tracking for Ivanti findings (Tier 2 from
the reporting recommendations doc) and rename the Reporting page to
Vulnerability Triage to better reflect its purpose.
Backend — ivantiFindings.js:
- Create ivanti_counts_history table (appended on every sync, never
overwritten — Option B from design discussion)
- INSERT snapshot after each successful syncClosedCount() call
- GET /api/ivanti/findings/counts/history endpoint — returns last
snapshot per calendar day using ROW_NUMBER window function, so
multiple daily syncs collapse to the end-of-day value
Frontend:
- New IvantiCountsChart component: collapsible dual-line chart
(open vs closed) with dark tooltip, delta label showing change
since previous day, and graceful no-data states
- Chart placed between the donut metrics panel and the findings table
on the Vulnerability Triage page
- Renamed page: 'reporting' → 'triage' (page ID, nav label, component
export, all cross-file references)
- ComplianceDetailPanel "View in Reporting" link updated to "View in
Triage" and navigates to the correct page ID
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -175,6 +175,15 @@ function initTables(db) {
|
||||
db.run(`
|
||||
CREATE INDEX IF NOT EXISTS idx_finding_overrides_finding_id
|
||||
ON ivanti_finding_overrides(finding_id)
|
||||
`, (err) => { if (err) return reject(err); });
|
||||
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS ivanti_counts_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
open_count INTEGER NOT NULL,
|
||||
closed_count INTEGER NOT NULL,
|
||||
recorded_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`, (err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
@@ -271,6 +280,14 @@ async function syncClosedCount(db, openCount, apiKey, clientId, skipTls) {
|
||||
`UPDATE ivanti_counts_cache SET open_count=?, closed_count=?, synced_at=datetime('now') WHERE id=1`,
|
||||
[openCount, closedCount]
|
||||
);
|
||||
|
||||
// Append a snapshot to history — every sync is stored; the history
|
||||
// endpoint aggregates to last-per-day at query time (Option B).
|
||||
await dbRun(db,
|
||||
`INSERT INTO ivanti_counts_history (open_count, closed_count) VALUES (?, ?)`,
|
||||
[openCount, closedCount]
|
||||
);
|
||||
|
||||
console.log(`[Ivanti Findings] Counts updated — open: ${openCount}, closed: ${closedCount}`);
|
||||
} catch (err) {
|
||||
console.error('[Ivanti Findings] Failed to fetch closed count:', err.message);
|
||||
@@ -576,6 +593,33 @@ function createIvantiFindingsRouter(db, requireAuth) {
|
||||
}
|
||||
});
|
||||
|
||||
// GET /counts/history — last snapshot per day, ascending, for the trend chart.
|
||||
// Uses a window function (ROW_NUMBER) to pick the final sync of each calendar day.
|
||||
router.get('/counts/history', async (req, res) => {
|
||||
try {
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
db.all(
|
||||
`SELECT date, open_count, closed_count FROM (
|
||||
SELECT DATE(recorded_at) AS date,
|
||||
open_count, closed_count,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY DATE(recorded_at)
|
||||
ORDER BY recorded_at DESC
|
||||
) AS rn
|
||||
FROM ivanti_counts_history
|
||||
) WHERE rn = 1
|
||||
ORDER BY date ASC`,
|
||||
[],
|
||||
(err, rows) => { if (err) reject(err); else resolve(rows || []); }
|
||||
);
|
||||
});
|
||||
res.json({ history: rows });
|
||||
} catch (err) {
|
||||
console.error('[Ivanti Findings] GET /counts/history error:', err.message);
|
||||
res.status(500).json({ error: 'Database error' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /fp-workflow-counts — FP finding + unique workflow counts (open + closed)
|
||||
router.get('/fp-workflow-counts', async (req, res) => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user