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
|
||||
// ---------------------------------------------------------------------------
|
||||
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 {
|
||||
id: String(f.id),
|
||||
title: f.title || '',
|
||||
@@ -130,11 +140,10 @@ function extractFinding(f) {
|
||||
dns: f.dns || f.host?.fqdn || '',
|
||||
status: f.status || '',
|
||||
slaStatus: f.slaStatus || '',
|
||||
discoveredOn: f.discoveredOn || '',
|
||||
dueDate,
|
||||
lastFoundOn: f.lastFoundOn || '',
|
||||
source: f.scannerPrettyName || f.scannerName || f.source || '',
|
||||
pluginFamily: f.pluginFamily || '',
|
||||
findingType: f.findingType || ''
|
||||
buOwnership,
|
||||
cves
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import NvdSyncModal from './components/NvdSyncModal';
|
||||
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 KnowledgeBasePage from './components/pages/KnowledgeBasePage';
|
||||
import ExportsPage from './components/pages/ExportsPage';
|
||||
@@ -177,6 +178,7 @@ export default function App() {
|
||||
const [quickCheckResult, setQuickCheckResult] = useState(null);
|
||||
const [currentPage, setCurrentPage] = useState('home');
|
||||
const [navOpen, setNavOpen] = useState(false);
|
||||
const [calendarFilter, setCalendarFilter] = useState(null);
|
||||
const [showAddCVE, setShowAddCVE] = useState(false);
|
||||
const [showUserManagement, setShowUserManagement] = useState(false);
|
||||
const [showAuditLog, setShowAuditLog] = useState(false);
|
||||
@@ -960,12 +962,16 @@ export default function App() {
|
||||
isOpen={navOpen}
|
||||
onClose={() => setNavOpen(false)}
|
||||
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 */}
|
||||
<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 */}
|
||||
<div className="mb-8">
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
@@ -1035,7 +1041,7 @@ export default function App() {
|
||||
</div>
|
||||
|
||||
{/* Page content */}
|
||||
{currentPage === 'reporting' && <ReportingPage />}
|
||||
{currentPage === 'reporting' && <ReportingPage filterDate={calendarFilter} />}
|
||||
{currentPage === 'knowledge-base' && <KnowledgeBasePage />}
|
||||
{currentPage === 'exports' && <ExportsPage />}
|
||||
|
||||
@@ -2219,63 +2225,12 @@ export default function App() {
|
||||
Calendar
|
||||
</h2>
|
||||
|
||||
{/* Simple Calendar Grid */}
|
||||
<div className="mb-2">
|
||||
<div className="text-center mb-3">
|
||||
<span className="text-white font-semibold font-mono">February 2024</span>
|
||||
</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>
|
||||
<CalendarWidget
|
||||
onDateClick={(dateStr) => {
|
||||
setCalendarFilter(dateStr);
|
||||
setCurrentPage('reporting');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 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