3 Commits

Author SHA1 Message Date
5102a2c5b4 docs: update 'Reporting page' references to 'Vulnerability Triage'
Updated all human-readable references in documentation to reflect the
page rename. File path citations in security-audit-2026-04-01.md
(ReportingPage.js:51) are left unchanged as the file itself was not
renamed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 10:15:51 -06:00
a0a8979c63 fix(triage): fix missed setCurrentPage('reporting') in Archer ticket filter button
One reference to the old page ID was missed in the previous rename commit.
The Archer ticket EXC filter button in App.js was still navigating to
'reporting', which would silently fail to navigate. Updated to 'triage'.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 10:13:57 -06:00
15ad207464 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>
2026-04-02 10:12:04 -06:00
8 changed files with 428 additions and 13 deletions

View File

@@ -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 {

View File

@@ -9,7 +9,7 @@ Renders natively in GitHub, GitLab, and most modern documentation tools.
```mermaid
flowchart TD
START([Open Reporting Page]) --> SYNC
START([Open Vulnerability Triage Page]) --> SYNC
SYNC["① Sync & Sort<br/>Click Sync · Sort Due Date ascending"]
SYNC --> DUE{Overdue<br/>findings?}
@@ -94,11 +94,11 @@ flowchart TD
## Diagram 2 — FP Workflow Badge Status Decision Tree
What to do when a finding already has a workflow badge in the Reporting page.
What to do when a finding already has a workflow badge in the Vulnerability Triage page.
```mermaid
flowchart LR
A([Finding in<br/>Reporting Page]) --> B{"Check<br/>Workflow column"}
A([Finding in<br/>Vulnerability Triage]) --> B{"Check<br/>Workflow column"}
B -->|No badge| C["UNTRIAGED<br/>No action on record"]
C --> C1(["Follow the<br/>Step 15 triage workflow ↑"])

View File

@@ -0,0 +1,158 @@
# STEAM Security Dashboard — Team Training Agenda
**Session length:** 3040 minutes
**Format:** Live walkthrough (share your screen on the dashboard)
**Reference docs:** `security-posture-workflow.md` for full detail on anything covered here
---
## Pre-meeting prep
- Have the dashboard open and logged in before the meeting starts
- Sync Vulnerability Triage page so data is fresh when you get there
- Print or share `security-posture-workflow.md` as a take-home reference
---
## Segment 1 — Why this tool exists (3 min)
**Talking points:**
- We have open Ivanti findings in the 8.59.9 VRR range — these are the ones we own and are accountable for
- Every finding needs a documented action within **60 days of detection** (the SLA rule)
- Findings that age past their Due Date make a device non-compliant in AEO posture reporting
- This dashboard is how we track, triage, and prove we've actioned everything — replaces manual spreadsheet tracking
---
## Segment 2 — Dashboard orientation (4 min)
**Show on screen:** Navigate through each page in the nav drawer
- **Home (CVE Management)** — our CVE research library; this is where we store screenshots, advisories, and Archer EXC numbers against each CVE/vendor pair
- **Vulnerability Triage (Host Findings)** — the daily operational page; this is where you spend most of your time
- **Compliance** — AEO posture data uploaded from the NTS_AEO xlsx; shows metric health per team
- **Knowledge Base** — internal docs, runbooks, advisories
- **Exports** — bulk data extracts when needed
> Tell the team: *"The Vulnerability Triage page is what we'll focus on today — that's where the workflow lives."*
---
## Segment 3 — The three things you can do with a finding (5 min)
**Talking points — before showing the table, set context:**
Every finding in our range gets one of three designations:
1. **Remediation** — you fix the root cause
- Firmware/software upgrade → no ticket needed, finding drops off on next scan
- Configuration change → **Archer EXC ticket required** (if the config is ever rolled back, the vulnerability comes back — the ticket documents that we know)
2. **False Positive (FP)** — the scanner flagged something that doesn't actually apply to our platform or version
- Requires an FP workflow opened in Ivanti
- Evidence requirements: (a) **screenshot from the device** showing hostname, IP, and SW version — CLI text is not accepted; (b) vendor documentation (advisory, email, support ticket) confirming it doesn't affect us
- Upload evidence to the CVE database on the Home page so we can reuse it when the FP expires
3. **Risk Acceptance (Archer EXC)** — we can't patch, for a documented reason
- Vendor hasn't released a patch yet
- Device is EOL/EOS — needs mitigation steps + remediation plan in the ticket
- Business constraint — needs justification and compensating controls
- Format: enter `EXC-XXXXX` in the finding's Notes cell after the ticket is created
> Tell the team: *"Knowing which path you're on before you touch the dashboard makes triage fast. The workflow is just deciding which of these three it is."*
---
## Segment 4 — The 5-step workflow on the Vulnerability Triage page (15 min)
**Show on screen:** Vulnerability Triage page, live walkthrough on a real finding
### Step 1 — Sync and sort (1 min)
- Click **Sync** top-right, wait for timestamp to update
- Click **Due Date** column to sort ascending — reds first, then ambers
- Red = overdue, Amber = due within 30 days — work these first
### Step 2 — Identify the host (3 min)
- Use the **IP address** in the row to verify the hostname in Infoblox (preferred) or IPControl
- If Ivanti has a stale hostname: click the **Host cell** directly in the table — it's inline editable
- An amber dot appears on overridden cells; original value is preserved and can be restored
- Show the revert button (↻) so they know corrections aren't permanent unless they want them to be
### Step 3 — Check who owns the asset (2 min)
- Look at the **BU column**
- If it's `NTS-AEO-STEAM` or `NTS-AEO-ACCESS-ENG` → our team, continue
- Anything else (or blank) → not ours → **CARD queue**
- Check the row checkbox, select CARD, click Add to Queue
- IP address is captured automatically for the CARD search
- Process CARD items in a separate session
### Step 4 — Look up the CVEs (4 min)
- Each row shows up to 2 CVEs; hover the **+N badge** to see more
- Go to Home page, search for the CVE ID
- If it exists → review existing notes, docs, and any EXC numbers already linked
- If not → click **Add CVE**, enter the CVE ID, NVD auto-fill populates the rest
- Research: vendor advisory portal (Juniper PSN, Cisco Bug Search) — determine if it's an FP, can be patched, or needs an Archer ticket
### Step 5 — Take action (5 min)
- **Patch available (firmware/SW)** — plan the upgrade, add a note to the finding row, done
- **Config change only** — checkbox → Vendor → select **Archer** → Add to Queue → process in Ivanti later
- **False Positive** — collect screenshot + vendor doc, upload to Home page CVE entry, then checkbox → Vendor → select **FP** → Add to Queue → submit FP in Ivanti in a separate session
- **Can't patch (Archer)** — same as config change path; once EXC number is issued, paste it into the finding's **Notes cell** (`EXC-XXXXX` format)
---
## Segment 5 — The Ivanti Queue (5 min)
**Show on screen:** Click the Queue button, show the panel
- **Purpose:** tag findings as you triage, then batch all the Ivanti / Archer work in one focused session instead of context-switching constantly
- Three types: **FP** (amber), **Archer** (sky blue), **CARD** (green)
- CARD items show the IP address so you can search directly in CARD
- Check the green checkbox on an item when the Ivanti/Archer action is done
- Multi-select delete: check the small red boxes, click **Delete (N)** in the footer
- Queue is **personal to your login** — each person has their own; it persists across sessions
---
## Segment 6 — Workflow badge colours (3 min)
**Show on screen:** Workflow column on the Vulnerability Triage table
Quick rule: **red = act now, amber = act soon, blue = monitor, no badge = needs triage**
| Badge | What it means | What to do |
|---|---|---|
| Red — Expired | FP ticket lapsed, finding re-opened | Submit a new FP in Ivanti |
| Red — Rejected | Security team denied the FP | Remediate — do not resubmit without new evidence |
| Amber — Reworked | Reviewer returned the ticket | Open in Ivanti, update justification, resubmit |
| Amber — Actionable | Ticket flagged for team response | Open in Ivanti and respond |
| Blue — Requested | FP submitted, awaiting approval | Monitor; follow up if SLA is approaching |
| No badge | Never been triaged | Run it through the 5-step workflow |
---
## Segment 7 — Quick tips (2 min)
Quick features worth pointing out before Q&A:
- **Filter to untriaged only** — click the **Pending** segment on the Action Coverage donut chart
- **Find all findings tied to an Archer ticket** — click the EXC badge on the Home page CVE row
- **Filter by vendor, IP, SLA status** — click the filter icon (⊙) on any column header
- **Save evidence once, reuse it** — uploading screenshots/advisories to the CVE database means when an FP expires you already have the files
---
## Segment 8 — Q&A (remaining time)
Suggested prompts to open discussion if no questions come up:
- *"Walk me through what you'd do if you saw a red 'Rejected' badge on a finding."*
- *"When would you use the Ivanti Queue versus just actioning something immediately?"*
- *"What's the difference between Path B (config change) and Path D (risk acceptance) — when does each apply?"*
---
## Takeaway for the team
Point them to:
- `docs/security-posture-workflow.md` — the full process guide with all the steps, evidence requirements, and decision matrix
- `docs/security-posture-workflow-diagrams.md` — the Mermaid flowcharts if they're visual learners

View File

@@ -10,7 +10,7 @@ import KnowledgeBaseModal from './components/KnowledgeBaseModal';
import KnowledgeBaseViewer from './components/KnowledgeBaseViewer';
import NavDrawer from './components/NavDrawer';
import CalendarWidget from './components/CalendarWidget';
import ReportingPage from './components/pages/ReportingPage';
import VulnerabilityTriagePage from './components/pages/ReportingPage';
import KnowledgeBasePage from './components/pages/KnowledgeBasePage';
import ExportsPage from './components/pages/ExportsPage';
import CompliancePage from './components/pages/CompliancePage';
@@ -966,14 +966,14 @@ export default function App() {
currentPage={currentPage}
onNavigate={(page) => {
// Clear contextual filters when navigating directly via the nav drawer
if (page === 'reporting') { setCalendarFilter(null); setReportingExcFilter(null); }
if (page === 'triage') { setCalendarFilter(null); setReportingExcFilter(null); }
setCurrentPage(page);
}}
/>
{/* Scanning line effect */}
<div className="scan-line"></div>
<div className={`${currentPage === 'reporting' ? 'w-full' : 'max-w-7xl mx-auto'} relative z-10`}>
<div className={`${currentPage === 'triage' ? 'w-full' : 'max-w-7xl mx-auto'} relative z-10`}>
{/* Header */}
<div className="mb-8">
<div className="flex justify-between items-start mb-6">
@@ -1043,7 +1043,7 @@ export default function App() {
</div>
{/* Page content */}
{currentPage === 'reporting' && <ReportingPage filterDate={calendarFilter} filterEXC={reportingExcFilter} />}
{currentPage === 'triage' && <VulnerabilityTriagePage filterDate={calendarFilter} filterEXC={reportingExcFilter} />}
{currentPage === 'compliance' && <CompliancePage onNavigate={setCurrentPage} />}
{currentPage === 'knowledge-base' && <KnowledgeBasePage />}
{currentPage === 'exports' && <ExportsPage />}
@@ -2231,7 +2231,7 @@ export default function App() {
<CalendarWidget
onDateClick={(dateStr) => {
setCalendarFilter(dateStr);
setCurrentPage('reporting');
setCurrentPage('triage');
}}
/>
</div>
@@ -2337,7 +2337,7 @@ export default function App() {
</a>
<div className="flex gap-1">
<button
onClick={() => { setReportingExcFilter(ticket.exc_number); setCurrentPage('reporting'); }}
onClick={() => { setReportingExcFilter(ticket.exc_number); setCurrentPage('triage'); }}
title="View findings referencing this ticket"
className="text-gray-400 hover:text-sky-400 transition-colors"
>

View File

@@ -3,7 +3,7 @@ import { X, Home, BarChart2, BookOpen, Download, ShieldCheck } from 'lucide-reac
const NAV_ITEMS = [
{ id: 'home', label: 'Home', icon: Home, color: '#0EA5E9', description: 'Main dashboard' },
{ id: 'reporting', label: 'Reporting', icon: BarChart2, color: '#F59E0B', description: 'Reports & analytics' },
{ id: 'triage', label: 'Vuln Triage', icon: BarChart2, color: '#F59E0B', description: 'Active findings & CVE triage' },
{ id: 'compliance', label: 'Compliance', icon: ShieldCheck, color: '#14B8A6', description: 'AEO posture & metrics' },
{ id: 'knowledge-base', label: 'Knowledge Base', icon: BookOpen, color: '#10B981', description: 'Articles & documentation' },
{ id: 'exports', label: 'Exports', icon: Download, color: '#8B5CF6', description: 'Export data & reports' },

View File

@@ -333,7 +333,7 @@ function MetricRow({ metric, resolved, onNavigate }) {
</div>
{onNavigate && (
<button
onClick={e => { e.stopPropagation(); onNavigate('reporting'); }}
onClick={e => { e.stopPropagation(); onNavigate('triage'); }}
style={{
flexShrink: 0, marginLeft: '0.5rem',
background: 'rgba(14,165,233,0.1)',
@@ -348,7 +348,7 @@ function MetricRow({ metric, resolved, onNavigate }) {
onMouseEnter={e => { e.currentTarget.style.background = 'rgba(14,165,233,0.18)'; e.currentTarget.style.borderColor = 'rgba(14,165,233,0.7)'; }}
onMouseLeave={e => { e.currentTarget.style.background = 'rgba(14,165,233,0.1)'; e.currentTarget.style.borderColor = 'rgba(14,165,233,0.4)'; }}
>
View in Reporting
View in Triage
</button>
)}
</div>

View File

@@ -0,0 +1,207 @@
// IvantiCountsChart.js
// Collapsible trend panel for the Vulnerability Triage page.
// Shows open vs closed Ivanti finding counts over time (last sync per day).
import React, { useState, useEffect, useMemo } from 'react';
import {
LineChart, Line,
XAxis, YAxis, CartesianGrid,
Tooltip, Legend, ReferenceLine,
ResponsiveContainer,
} from 'recharts';
import { TrendingUp, ChevronDown, ChevronUp, Loader } from 'lucide-react';
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
const AMBER = '#F59E0B';
const SKY = '#0EA5E9';
const GREEN = '#10B981';
const RED = '#EF4444';
const AXIS_STYLE = { fontFamily: 'monospace', fontSize: '0.6rem', fill: '#475569' };
const GRID_STYLE = { stroke: 'rgba(255,255,255,0.05)', strokeDasharray: '3 3' };
const LEGEND_STYLE = { fontFamily: 'monospace', fontSize: '0.62rem', color: '#64748B' };
// ---------------------------------------------------------------------------
// Custom dark tooltip
// ---------------------------------------------------------------------------
function DarkTooltip({ active, payload, label }) {
if (!active || !payload?.length) return null;
const openVal = payload.find(p => p.dataKey === 'open_count')?.value;
const closedVal = payload.find(p => p.dataKey === 'closed_count')?.value;
return (
<div style={{
background: 'rgba(10,17,32,0.97)',
border: '1px solid rgba(245,158,11,0.3)',
borderRadius: '0.375rem',
padding: '0.5rem 0.75rem',
fontFamily: 'monospace',
fontSize: '0.7rem',
minWidth: '160px',
}}>
<div style={{ color: AMBER, marginBottom: '0.35rem', fontWeight: '700', fontSize: '0.65rem' }}>
{label}
</div>
{payload.map(p => (
<div key={p.dataKey} style={{ color: p.color, marginTop: '0.125rem', display: 'flex', justifyContent: 'space-between', gap: '1rem' }}>
<span style={{ opacity: 0.8 }}>{p.name}</span>
<span style={{ fontWeight: '700' }}>{p.value}</span>
</div>
))}
{openVal != null && closedVal != null && (
<div style={{ borderTop: '1px solid rgba(255,255,255,0.06)', marginTop: '0.35rem', paddingTop: '0.3rem', color: '#475569', display: 'flex', justifyContent: 'space-between', gap: '1rem' }}>
<span>total</span>
<span style={{ color: '#64748B', fontWeight: '600' }}>{openVal + closedVal}</span>
</div>
)}
</div>
);
}
// ---------------------------------------------------------------------------
// Shorten YYYY-MM-DD to MM/DD/YY
// ---------------------------------------------------------------------------
function fmtDate(d) {
if (!d) return '';
const p = d.split('-');
if (p.length === 3) return `${p[1]}/${p[2]}/${p[0].slice(2)}`;
return d;
}
// ---------------------------------------------------------------------------
// Main component
// ---------------------------------------------------------------------------
export default function IvantiCountsChart() {
const [collapsed, setCollapsed] = useState(false);
const [loading, setLoading] = useState(true);
const [history, setHistory] = useState([]);
useEffect(() => {
let cancelled = false;
const load = async () => {
setLoading(true);
try {
const res = await fetch(`${API_BASE}/ivanti/findings/counts/history`, { credentials: 'include' });
if (res.ok && !cancelled) {
const d = await res.json();
setHistory(d.history || []);
}
} catch { /* silent — chart shows no-data state */ }
finally { if (!cancelled) setLoading(false); }
};
load();
return () => { cancelled = true; };
}, []);
const chartData = useMemo(
() => history.map(r => ({ ...r, date: fmtDate(r.date) })),
[history]
);
// Compute a simple delta label for the latest vs previous point
const deltaLabel = useMemo(() => {
if (chartData.length < 2) return null;
const latest = chartData[chartData.length - 1];
const prev = chartData[chartData.length - 2];
const delta = latest.open_count - prev.open_count;
if (delta === 0) return { text: 'no change in open', color: '#475569' };
if (delta < 0) return { text: `${Math.abs(delta)} open since ${prev.date}`, color: GREEN };
return { text: `${delta} open since ${prev.date}`, color: RED };
}, [chartData]);
return (
<div style={{ marginBottom: '1.25rem' }}>
{/* ── Header ────────────────────────────────────────────────── */}
<button
onClick={() => setCollapsed(c => !c)}
style={{
width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
background: 'none', border: 'none', cursor: 'pointer',
padding: '0 0 0.625rem 0',
borderBottom: collapsed ? 'none' : '1px solid rgba(245,158,11,0.1)',
marginBottom: collapsed ? 0 : '0.875rem',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<TrendingUp style={{ width: '14px', height: '14px', color: AMBER }} />
<span style={{
fontFamily: 'monospace', fontSize: '0.65rem', fontWeight: '700',
color: '#334155', textTransform: 'uppercase', letterSpacing: '0.1em',
}}>
Findings Trend
</span>
{loading && (
<Loader style={{ width: '12px', height: '12px', color: '#334155', animation: 'spin 1s linear infinite' }} />
)}
{!loading && deltaLabel && (
<span style={{ fontFamily: 'monospace', fontSize: '0.62rem', color: deltaLabel.color, marginLeft: '0.25rem' }}>
{deltaLabel.text}
</span>
)}
</div>
{collapsed
? <ChevronDown style={{ width: '14px', height: '14px', color: '#334155' }} />
: <ChevronUp style={{ width: '14px', height: '14px', color: '#334155' }} />
}
</button>
{!collapsed && (
<div style={{
background: 'linear-gradient(135deg,rgba(30,41,59,0.95) 0%,rgba(15,23,42,0.98) 100%)',
border: '1px solid rgba(245,158,11,0.15)',
borderRadius: '0.5rem',
padding: '1rem 1.25rem 0.875rem',
}}>
<div style={{ marginBottom: '0.625rem', display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
<div style={{ fontFamily: 'monospace', fontSize: '0.65rem', color: '#334155', textTransform: 'uppercase', letterSpacing: '0.08em' }}>
Open vs Closed end-of-day snapshot per sync day
</div>
{chartData.length > 0 && (
<div style={{ fontFamily: 'monospace', fontSize: '0.62rem', color: '#334155' }}>
{chartData.length} day{chartData.length !== 1 ? 's' : ''} of data
</div>
)}
</div>
{chartData.length < 2 ? (
<div style={{
display: 'flex', alignItems: 'center', justifyContent: 'center',
height: '160px', color: '#334155',
fontFamily: 'monospace', fontSize: '0.72rem',
border: '1px dashed rgba(245,158,11,0.1)', borderRadius: '0.375rem',
}}>
{chartData.length === 0
? 'Trend data begins accumulating after the first sync — check back tomorrow'
: 'Need at least 2 days of syncs to display a trend'}
</div>
) : (
<ResponsiveContainer width="100%" height={220}>
<LineChart data={chartData} margin={{ top: 4, right: 12, bottom: 0, left: -12 }}>
<CartesianGrid {...GRID_STYLE} />
<XAxis dataKey="date" tick={AXIS_STYLE} />
<YAxis tick={AXIS_STYLE} allowDecimals={false} />
<Tooltip content={<DarkTooltip />} />
<Legend wrapperStyle={LEGEND_STYLE} iconSize={8} />
<Line
type="monotone" dataKey="open_count" name="Open"
stroke={AMBER} strokeWidth={2}
dot={{ r: 3, fill: AMBER, strokeWidth: 0 }}
activeDot={{ r: 5 }}
/>
<Line
type="monotone" dataKey="closed_count" name="Closed"
stroke={SKY} strokeWidth={2}
dot={{ r: 3, fill: SKY, strokeWidth: 0 }}
activeDot={{ r: 5 }}
/>
</LineChart>
</ResponsiveContainer>
)}
</div>
)}
</div>
);
}

View File

@@ -3,6 +3,7 @@ import ReactDOM from 'react-dom';
import { RefreshCw, Loader, AlertCircle, PieChart, ChevronUp, ChevronDown, ChevronsUpDown, Settings2, GripVertical, Eye, EyeOff, Filter, Download, RotateCcw, Trash2, X, ListTodo } from 'lucide-react';
import * as XLSX from 'xlsx';
import { useAuth } from '../../contexts/AuthContext';
import IvantiCountsChart from './IvantiCountsChart';
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
const STORAGE_KEY = 'steam_findings_columns_v2';
@@ -1536,7 +1537,7 @@ function QueuePanel({ open, items, onClose, onUpdate, onDelete, onDeleteMany, on
// ---------------------------------------------------------------------------
// Main ReportingPage
// ---------------------------------------------------------------------------
export default function ReportingPage({ filterDate, filterEXC }) {
export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
const { canWrite } = useAuth();
const [findings, setFindings] = useState([]);
const [total, setTotal] = useState(null);
@@ -1965,6 +1966,11 @@ export default function ReportingPage({ filterDate, filterEXC }) {
</div>
</div>
{/* ----------------------------------------------------------------
Panel 1.5 — Open vs Closed trend over time
---------------------------------------------------------------- */}
<IvantiCountsChart />
{/* ----------------------------------------------------------------
Panel 2 — Findings table
---------------------------------------------------------------- */}