Compare commits
10 Commits
feature/re
...
9893460b64
| Author | SHA1 | Date | |
|---|---|---|---|
| 9893460b64 | |||
| 51b1f99b3a | |||
| 669396f635 | |||
| 8b3ea22fa0 | |||
| 75b8ecc61d | |||
| ade3cc25ad | |||
| 3fd6158eb3 | |||
| 5bbaaf5918 | |||
| 1f36d302ea | |||
| 8697ba4ef3 |
@@ -120,6 +120,16 @@ function initTables(db) {
|
|||||||
// Extract only the fields we need from a raw finding object
|
// Extract only the fields we need from a raw finding object
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
function extractFinding(f) {
|
function extractFinding(f) {
|
||||||
|
// statusEmbedded.dueDate = "2026-03-06T00:00:00" — strip to date part
|
||||||
|
const rawDueDate = f.statusEmbedded?.dueDate || '';
|
||||||
|
const dueDate = rawDueDate ? rawDueDate.split('T')[0] : '';
|
||||||
|
|
||||||
|
// BU ownership: assetCustomAttributes['1550_host_1'] is an array like ["NTS-AEO-STEAM"]
|
||||||
|
const buOwnership = f.assetCustomAttributes?.['1550_host_1']?.[0] || '';
|
||||||
|
|
||||||
|
// CVE list: vulnerabilities.vulnInfoList[].cve
|
||||||
|
const cves = (f.vulnerabilities?.vulnInfoList || []).map(v => v.cve).filter(Boolean);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: String(f.id),
|
id: String(f.id),
|
||||||
title: f.title || '',
|
title: f.title || '',
|
||||||
@@ -130,11 +140,10 @@ function extractFinding(f) {
|
|||||||
dns: f.dns || f.host?.fqdn || '',
|
dns: f.dns || f.host?.fqdn || '',
|
||||||
status: f.status || '',
|
status: f.status || '',
|
||||||
slaStatus: f.slaStatus || '',
|
slaStatus: f.slaStatus || '',
|
||||||
discoveredOn: f.discoveredOn || '',
|
dueDate,
|
||||||
lastFoundOn: f.lastFoundOn || '',
|
lastFoundOn: f.lastFoundOn || '',
|
||||||
source: f.scannerPrettyName || f.scannerName || f.source || '',
|
buOwnership,
|
||||||
pluginFamily: f.pluginFamily || '',
|
cves
|
||||||
findingType: f.findingType || ''
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import NvdSyncModal from './components/NvdSyncModal';
|
|||||||
import KnowledgeBaseModal from './components/KnowledgeBaseModal';
|
import KnowledgeBaseModal from './components/KnowledgeBaseModal';
|
||||||
import KnowledgeBaseViewer from './components/KnowledgeBaseViewer';
|
import KnowledgeBaseViewer from './components/KnowledgeBaseViewer';
|
||||||
import NavDrawer from './components/NavDrawer';
|
import NavDrawer from './components/NavDrawer';
|
||||||
|
import CalendarWidget from './components/CalendarWidget';
|
||||||
import ReportingPage from './components/pages/ReportingPage';
|
import ReportingPage from './components/pages/ReportingPage';
|
||||||
import KnowledgeBasePage from './components/pages/KnowledgeBasePage';
|
import KnowledgeBasePage from './components/pages/KnowledgeBasePage';
|
||||||
import ExportsPage from './components/pages/ExportsPage';
|
import ExportsPage from './components/pages/ExportsPage';
|
||||||
@@ -177,6 +178,7 @@ export default function App() {
|
|||||||
const [quickCheckResult, setQuickCheckResult] = useState(null);
|
const [quickCheckResult, setQuickCheckResult] = useState(null);
|
||||||
const [currentPage, setCurrentPage] = useState('home');
|
const [currentPage, setCurrentPage] = useState('home');
|
||||||
const [navOpen, setNavOpen] = useState(false);
|
const [navOpen, setNavOpen] = useState(false);
|
||||||
|
const [calendarFilter, setCalendarFilter] = useState(null);
|
||||||
const [showAddCVE, setShowAddCVE] = useState(false);
|
const [showAddCVE, setShowAddCVE] = useState(false);
|
||||||
const [showUserManagement, setShowUserManagement] = useState(false);
|
const [showUserManagement, setShowUserManagement] = useState(false);
|
||||||
const [showAuditLog, setShowAuditLog] = useState(false);
|
const [showAuditLog, setShowAuditLog] = useState(false);
|
||||||
@@ -960,12 +962,16 @@ export default function App() {
|
|||||||
isOpen={navOpen}
|
isOpen={navOpen}
|
||||||
onClose={() => setNavOpen(false)}
|
onClose={() => setNavOpen(false)}
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
onNavigate={setCurrentPage}
|
onNavigate={(page) => {
|
||||||
|
// Clear calendar filter when navigating directly via the nav drawer
|
||||||
|
if (page === 'reporting') setCalendarFilter(null);
|
||||||
|
setCurrentPage(page);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{/* Scanning line effect */}
|
{/* Scanning line effect */}
|
||||||
<div className="scan-line"></div>
|
<div className="scan-line"></div>
|
||||||
|
|
||||||
<div className="max-w-7xl mx-auto relative z-10">
|
<div className={`${currentPage === 'reporting' ? 'w-full' : 'max-w-7xl mx-auto'} relative z-10`}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="flex justify-between items-start mb-6">
|
<div className="flex justify-between items-start mb-6">
|
||||||
@@ -1035,7 +1041,7 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Page content */}
|
{/* Page content */}
|
||||||
{currentPage === 'reporting' && <ReportingPage />}
|
{currentPage === 'reporting' && <ReportingPage filterDate={calendarFilter} />}
|
||||||
{currentPage === 'knowledge-base' && <KnowledgeBasePage />}
|
{currentPage === 'knowledge-base' && <KnowledgeBasePage />}
|
||||||
{currentPage === 'exports' && <ExportsPage />}
|
{currentPage === 'exports' && <ExportsPage />}
|
||||||
|
|
||||||
@@ -2219,63 +2225,12 @@ export default function App() {
|
|||||||
Calendar
|
Calendar
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{/* Simple Calendar Grid */}
|
<CalendarWidget
|
||||||
<div className="mb-2">
|
onDateClick={(dateStr) => {
|
||||||
<div className="text-center mb-3">
|
setCalendarFilter(dateStr);
|
||||||
<span className="text-white font-semibold font-mono">February 2024</span>
|
setCurrentPage('reporting');
|
||||||
</div>
|
}}
|
||||||
<div className="grid grid-cols-7 gap-1 text-center text-xs mb-2">
|
/>
|
||||||
<div className="text-gray-400 font-mono">Su</div>
|
|
||||||
<div className="text-gray-400 font-mono">Mo</div>
|
|
||||||
<div className="text-gray-400 font-mono">Tu</div>
|
|
||||||
<div className="text-gray-400 font-mono">We</div>
|
|
||||||
<div className="text-gray-400 font-mono">Th</div>
|
|
||||||
<div className="text-gray-400 font-mono">Fr</div>
|
|
||||||
<div className="text-gray-400 font-mono">Sa</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-7 gap-1 text-center">
|
|
||||||
{/* Week 1 */}
|
|
||||||
<div className="text-gray-600 font-mono text-xs p-1">28</div>
|
|
||||||
<div className="text-gray-600 font-mono text-xs p-1">29</div>
|
|
||||||
<div className="text-gray-600 font-mono text-xs p-1">30</div>
|
|
||||||
<div className="text-gray-600 font-mono text-xs p-1">31</div>
|
|
||||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">1</div>
|
|
||||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">2</div>
|
|
||||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">3</div>
|
|
||||||
{/* Week 2 */}
|
|
||||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">4</div>
|
|
||||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">5</div>
|
|
||||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">6</div>
|
|
||||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">7</div>
|
|
||||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">8</div>
|
|
||||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">9</div>
|
|
||||||
<div className="bg-intel-accent/30 text-white font-mono text-xs p-1 rounded font-bold border border-intel-accent">10</div>
|
|
||||||
{/* Week 3 */}
|
|
||||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">11</div>
|
|
||||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">12</div>
|
|
||||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">13</div>
|
|
||||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">14</div>
|
|
||||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">15</div>
|
|
||||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">16</div>
|
|
||||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">17</div>
|
|
||||||
{/* Week 4 */}
|
|
||||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">18</div>
|
|
||||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">19</div>
|
|
||||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">20</div>
|
|
||||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">21</div>
|
|
||||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">22</div>
|
|
||||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">23</div>
|
|
||||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">24</div>
|
|
||||||
{/* Week 5 */}
|
|
||||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">25</div>
|
|
||||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">26</div>
|
|
||||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">27</div>
|
|
||||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">28</div>
|
|
||||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">29</div>
|
|
||||||
<div className="text-gray-600 font-mono text-xs p-1">1</div>
|
|
||||||
<div className="text-gray-600 font-mono text-xs p-1">2</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Open Vendor Tickets */}
|
{/* Open Vendor Tickets */}
|
||||||
|
|||||||
167
frontend/src/components/CalendarWidget.js
Normal file
167
frontend/src/components/CalendarWidget.js
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||||
|
|
||||||
|
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
||||||
|
|
||||||
|
const MONTH_NAMES = [
|
||||||
|
'January', 'February', 'March', 'April', 'May', 'June',
|
||||||
|
'July', 'August', 'September', 'October', 'November', 'December'
|
||||||
|
];
|
||||||
|
const DAY_NAMES = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
|
||||||
|
|
||||||
|
function toLocalDateStr(date) {
|
||||||
|
const y = date.getFullYear();
|
||||||
|
const m = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const d = String(date.getDate()).padStart(2, '0');
|
||||||
|
return `${y}-${m}-${d}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CalendarWidget({ onDateClick }) {
|
||||||
|
const today = new Date();
|
||||||
|
const todayStr = toLocalDateStr(today);
|
||||||
|
|
||||||
|
const [calYear, setCalYear] = useState(today.getFullYear());
|
||||||
|
const [calMonth, setCalMonth] = useState(today.getMonth()); // 0-indexed
|
||||||
|
|
||||||
|
// Map of "YYYY-MM-DD" → count of findings due that day
|
||||||
|
const [dueDates, setDueDates] = useState({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch(`${API_BASE}/ivanti/findings`, { credentials: 'include' })
|
||||||
|
.then((r) => (r.ok ? r.json() : null))
|
||||||
|
.then((data) => {
|
||||||
|
if (!data?.findings) return;
|
||||||
|
const counts = {};
|
||||||
|
data.findings.forEach((f) => {
|
||||||
|
if (f.dueDate) {
|
||||||
|
counts[f.dueDate] = (counts[f.dueDate] || 0) + 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setDueDates(counts);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const prevMonth = () => {
|
||||||
|
if (calMonth === 0) { setCalMonth(11); setCalYear((y) => y - 1); }
|
||||||
|
else { setCalMonth((m) => m - 1); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextMonth = () => {
|
||||||
|
if (calMonth === 11) { setCalMonth(0); setCalYear((y) => y + 1); }
|
||||||
|
else { setCalMonth((m) => m + 1); }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build cell array: null = padding, number = day of month
|
||||||
|
const firstDow = new Date(calYear, calMonth, 1).getDay(); // 0=Sun
|
||||||
|
const daysInMonth = new Date(calYear, calMonth + 1, 0).getDate();
|
||||||
|
const cells = [
|
||||||
|
...Array(firstDow).fill(null),
|
||||||
|
...Array.from({ length: daysInMonth }, (_, i) => i + 1),
|
||||||
|
];
|
||||||
|
while (cells.length % 7 !== 0) cells.push(null); // complete last row
|
||||||
|
|
||||||
|
const hasDueDatesThisMonth = cells.some((day) => {
|
||||||
|
if (!day) return false;
|
||||||
|
const ds = `${calYear}-${String(calMonth + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||||
|
return !!dueDates[ds];
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* Month navigation */}
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '0.75rem' }}>
|
||||||
|
<button
|
||||||
|
onClick={prevMonth}
|
||||||
|
style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#64748B', padding: '2px 4px', borderRadius: '4px', lineHeight: 1, transition: 'color 0.15s' }}
|
||||||
|
onMouseEnter={(e) => { e.currentTarget.style.color = '#0EA5E9'; }}
|
||||||
|
onMouseLeave={(e) => { e.currentTarget.style.color = '#64748B'; }}
|
||||||
|
>
|
||||||
|
<ChevronLeft style={{ width: '14px', height: '14px' }} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<span style={{ color: '#E2E8F0', fontFamily: 'monospace', fontWeight: '600', fontSize: '0.85rem' }}>
|
||||||
|
{MONTH_NAMES[calMonth]} {calYear}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={nextMonth}
|
||||||
|
style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#64748B', padding: '2px 4px', borderRadius: '4px', lineHeight: 1, transition: 'color 0.15s' }}
|
||||||
|
onMouseEnter={(e) => { e.currentTarget.style.color = '#0EA5E9'; }}
|
||||||
|
onMouseLeave={(e) => { e.currentTarget.style.color = '#64748B'; }}
|
||||||
|
>
|
||||||
|
<ChevronRight style={{ width: '14px', height: '14px' }} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Day-of-week headers */}
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: '2px', textAlign: 'center', marginBottom: '4px' }}>
|
||||||
|
{DAY_NAMES.map((d) => (
|
||||||
|
<div key={d} style={{ fontSize: '0.6rem', color: '#475569', fontFamily: 'monospace', fontWeight: '600', textTransform: 'uppercase' }}>
|
||||||
|
{d}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Day cells */}
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: '2px' }}>
|
||||||
|
{cells.map((day, idx) => {
|
||||||
|
if (!day) return <div key={idx} />;
|
||||||
|
|
||||||
|
const dateStr = `${calYear}-${String(calMonth + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||||
|
const isToday = dateStr === todayStr;
|
||||||
|
const dueCount = dueDates[dateStr] || 0;
|
||||||
|
const hasDue = dueCount > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
title={hasDue ? `${dueCount} finding${dueCount > 1 ? 's' : ''} due — click to view` : undefined}
|
||||||
|
onClick={hasDue && onDateClick ? () => onDateClick(dateStr) : undefined}
|
||||||
|
style={{
|
||||||
|
display: 'flex', flexDirection: 'column', alignItems: 'center',
|
||||||
|
gap: '2px', padding: '3px 1px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
background: isToday ? 'rgba(14,165,233,0.2)' : 'transparent',
|
||||||
|
border: isToday ? '1px solid rgba(14,165,233,0.5)' : '1px solid transparent',
|
||||||
|
cursor: hasDue ? 'pointer' : 'default',
|
||||||
|
transition: hasDue ? 'background 0.15s' : undefined,
|
||||||
|
}}
|
||||||
|
onMouseEnter={hasDue ? (e) => { e.currentTarget.style.background = isToday ? 'rgba(14,165,233,0.35)' : 'rgba(239,68,68,0.15)'; } : undefined}
|
||||||
|
onMouseLeave={hasDue ? (e) => { e.currentTarget.style.background = isToday ? 'rgba(14,165,233,0.2)' : 'transparent'; } : undefined}
|
||||||
|
>
|
||||||
|
<span style={{
|
||||||
|
fontSize: '0.7rem', fontFamily: 'monospace', lineHeight: 1,
|
||||||
|
color: isToday ? '#0EA5E9' : hasDue ? '#EF4444' : '#CBD5E1',
|
||||||
|
fontWeight: (isToday || hasDue) ? '700' : '400',
|
||||||
|
}}>
|
||||||
|
{day}
|
||||||
|
</span>
|
||||||
|
{/* Red dot indicator for due dates */}
|
||||||
|
{hasDue ? (
|
||||||
|
<div style={{
|
||||||
|
width: '4px', height: '4px', borderRadius: '50%',
|
||||||
|
background: '#EF4444',
|
||||||
|
boxShadow: '0 0 4px rgba(239,68,68,0.6)',
|
||||||
|
flexShrink: 0,
|
||||||
|
}} />
|
||||||
|
) : (
|
||||||
|
<div style={{ width: '4px', height: '4px' }} /> // spacer to keep rows even
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Legend — only shown when there are due dates this month */}
|
||||||
|
{hasDueDatesThisMonth && (
|
||||||
|
<div style={{ marginTop: '0.75rem', paddingTop: '0.625rem', borderTop: '1px solid rgba(255,255,255,0.05)', display: 'flex', alignItems: 'center', gap: '0.375rem' }}>
|
||||||
|
<div style={{ width: '6px', height: '6px', borderRadius: '50%', background: '#EF4444', boxShadow: '0 0 4px rgba(239,68,68,0.5)', flexShrink: 0 }} />
|
||||||
|
<span style={{ fontSize: '0.62rem', color: '#64748B', fontFamily: 'monospace', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
|
||||||
|
Ivanti finding due
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user