- common.js: idle session timeout 30min con avviso countdown 5min prima del logout - common.js: checkAuth() attiva automaticamente il monitor di inattività - api.js: messaggi errore connessione usano i18n (IT/EN) tramite I18n.t() - risks.html: saveRisk() e aiSuggest() con setButtonLoading durante salvataggio - risks.html: deleteRisk() ricarica la matrice se si è in matrix view - incidents.html: createIncident() con setButtonLoading durante registrazione - policies.html: savePolicy() e saveAIGeneratedPolicy() con setButtonLoading - policies.html: banner AI-draft con pulsante X per dismissione Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1220 lines
63 KiB
HTML
1220 lines
63 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="it">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Gestione Incidenti - NIS2 Agile</title>
|
|
<link rel="stylesheet" href="/css/style.css">
|
|
<style>
|
|
/* ── Filters Bar ─────────────────────────────────────── */
|
|
.filters-bar {
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
margin-bottom: 20px;
|
|
}
|
|
.filters-bar .form-select {
|
|
width: auto;
|
|
min-width: 160px;
|
|
padding: 8px 36px 8px 12px;
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
/* ── Severity Colors ─────────────────────────────────── */
|
|
.severity-critical { background: var(--danger-bg); color: var(--danger); }
|
|
.severity-high { background: #fff7ed; color: #c2410c; }
|
|
.severity-medium { background: var(--warning-bg); color: #a16207; }
|
|
.severity-low { background: var(--secondary-bg); color: #15803d; }
|
|
|
|
/* ── Overdue Warning Icon ─────────────────────────────── */
|
|
.overdue-icon {
|
|
color: var(--danger);
|
|
display: inline-flex;
|
|
vertical-align: middle;
|
|
animation: pulse-warn 1.5s ease-in-out infinite;
|
|
}
|
|
@keyframes pulse-warn {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.5; }
|
|
}
|
|
|
|
/* ── NIS2 Timeline ───────────────────────────────────── */
|
|
.nis2-timeline {
|
|
position: relative;
|
|
padding: 16px 0;
|
|
}
|
|
.nis2-timeline::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 20px;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 3px;
|
|
background: var(--gray-200);
|
|
border-radius: 2px;
|
|
}
|
|
.nis2-step {
|
|
position: relative;
|
|
padding-left: 52px;
|
|
margin-bottom: 28px;
|
|
}
|
|
.nis2-step:last-child { margin-bottom: 0; }
|
|
.nis2-step-dot {
|
|
position: absolute;
|
|
left: 10px;
|
|
top: 2px;
|
|
width: 24px;
|
|
height: 24px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: var(--gray-200);
|
|
z-index: 1;
|
|
}
|
|
.nis2-step-dot svg {
|
|
width: 14px;
|
|
height: 14px;
|
|
fill: var(--gray-500);
|
|
}
|
|
.nis2-step-dot.sent {
|
|
background: var(--secondary-bg);
|
|
}
|
|
.nis2-step-dot.sent svg { fill: var(--secondary); }
|
|
.nis2-step-dot.overdue {
|
|
background: var(--danger-bg);
|
|
}
|
|
.nis2-step-dot.overdue svg { fill: var(--danger); }
|
|
.nis2-step-dot.approaching {
|
|
background: var(--warning-bg);
|
|
}
|
|
.nis2-step-dot.approaching svg { fill: #a16207; }
|
|
|
|
.nis2-step-content {
|
|
background: var(--card-bg);
|
|
border: 1px solid var(--gray-200);
|
|
border-radius: var(--border-radius);
|
|
padding: 16px;
|
|
}
|
|
.nis2-step-content.sent { border-color: var(--secondary); background: #f0fdf4; }
|
|
.nis2-step-content.overdue { border-color: var(--danger); background: #fef2f2; }
|
|
.nis2-step-content.approaching { border-color: var(--warning); background: #fffbeb; }
|
|
|
|
.nis2-step-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 8px;
|
|
}
|
|
.nis2-step-title {
|
|
font-weight: 700;
|
|
font-size: 0.9rem;
|
|
color: var(--gray-800);
|
|
}
|
|
.nis2-step-time {
|
|
font-size: 0.8rem;
|
|
color: var(--gray-500);
|
|
}
|
|
.nis2-step-countdown {
|
|
font-size: 0.85rem;
|
|
font-weight: 600;
|
|
margin-top: 6px;
|
|
}
|
|
.countdown-overdue { color: var(--danger); }
|
|
.countdown-approaching { color: #a16207; }
|
|
.countdown-ok { color: var(--secondary); }
|
|
|
|
/* ── Event Timeline ──────────────────────────────────── */
|
|
.event-timeline {
|
|
position: relative;
|
|
padding-left: 20px;
|
|
}
|
|
.event-timeline::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 6px;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 2px;
|
|
background: var(--gray-200);
|
|
}
|
|
.event-item {
|
|
position: relative;
|
|
padding-left: 24px;
|
|
padding-bottom: 20px;
|
|
}
|
|
.event-item:last-child { padding-bottom: 0; }
|
|
.event-item::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: -17px;
|
|
top: 6px;
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 50%;
|
|
background: var(--primary);
|
|
border: 2px solid var(--card-bg);
|
|
z-index: 1;
|
|
}
|
|
.event-item.type-notification::before { background: var(--secondary); }
|
|
.event-item.type-detection::before { background: var(--danger); }
|
|
.event-item.type-action::before { background: var(--warning); }
|
|
.event-item-time {
|
|
font-size: 0.7rem;
|
|
color: var(--gray-400);
|
|
margin-bottom: 2px;
|
|
}
|
|
.event-item-text {
|
|
font-size: 0.85rem;
|
|
color: var(--gray-700);
|
|
}
|
|
.event-item-author {
|
|
font-size: 0.75rem;
|
|
color: var(--gray-400);
|
|
margin-top: 2px;
|
|
}
|
|
|
|
/* ── Detail View ─────────────────────────────────────── */
|
|
.detail-back {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-size: 0.85rem;
|
|
font-weight: 600;
|
|
color: var(--gray-500);
|
|
cursor: pointer;
|
|
margin-bottom: 16px;
|
|
border: none;
|
|
background: none;
|
|
font-family: inherit;
|
|
padding: 0;
|
|
}
|
|
.detail-back:hover { color: var(--primary); }
|
|
|
|
.detail-grid {
|
|
display: grid;
|
|
grid-template-columns: 2fr 1fr;
|
|
gap: 24px;
|
|
}
|
|
.detail-field {
|
|
margin-bottom: 16px;
|
|
}
|
|
.detail-field-label {
|
|
font-size: 0.7rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
font-weight: 600;
|
|
color: var(--gray-500);
|
|
margin-bottom: 4px;
|
|
}
|
|
.detail-field-value {
|
|
font-size: 0.9rem;
|
|
color: var(--gray-800);
|
|
}
|
|
|
|
/* ── Table ────────────────────────────────────────────── */
|
|
.table-clickable tbody tr { cursor: pointer; }
|
|
|
|
/* ── NIS2 Status Compact ──────────────────────────────── */
|
|
.nis2-compact {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 3px;
|
|
font-size: 0.7rem;
|
|
}
|
|
.nis2-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
display: inline-block;
|
|
}
|
|
.nis2-dot.sent { background: var(--secondary); }
|
|
.nis2-dot.overdue { background: var(--danger); }
|
|
.nis2-dot.pending { background: var(--gray-300); }
|
|
.nis2-dot.approaching { background: var(--warning); }
|
|
|
|
/* ── Checkbox inline ──────────────────────────────────── */
|
|
.form-check-inline {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 0.875rem;
|
|
cursor: pointer;
|
|
padding: 8px 0;
|
|
}
|
|
.form-check-inline input[type="checkbox"] {
|
|
accent-color: var(--primary);
|
|
width: 18px;
|
|
height: 18px;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.detail-grid { grid-template-columns: 1fr; }
|
|
.filters-bar { flex-direction: column; align-items: stretch; }
|
|
.filters-bar .form-select { min-width: 100%; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="app-layout">
|
|
<aside class="sidebar" id="sidebar"></aside>
|
|
|
|
<main class="main-content">
|
|
<header class="content-header">
|
|
<h2 data-i18n="incidents.title">Gestione Incidenti</h2>
|
|
<div class="content-header-actions">
|
|
<button class="btn btn-primary" onclick="showCreateIncidentModal()">+ Nuovo Incidente</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="content-body">
|
|
<!-- List View -->
|
|
<div id="view-list">
|
|
<div class="filters-bar">
|
|
<select class="form-select" id="filter-classification" onchange="loadIncidents()">
|
|
<option value="">Tutte le classificazioni</option>
|
|
<option value="cyber_attack">Attacco Cyber</option>
|
|
<option value="data_breach">Data Breach</option>
|
|
<option value="system_failure">Guasto Sistema</option>
|
|
<option value="human_error">Errore Umano</option>
|
|
<option value="natural_disaster">Disastro Naturale</option>
|
|
<option value="supply_chain">Supply Chain</option>
|
|
<option value="other">Altro</option>
|
|
</select>
|
|
<select class="form-select" id="filter-severity" onchange="loadIncidents()">
|
|
<option value="">Tutte le severita'</option>
|
|
<option value="critical">Critico</option>
|
|
<option value="high">Alto</option>
|
|
<option value="medium">Medio</option>
|
|
<option value="low">Basso</option>
|
|
</select>
|
|
<select class="form-select" id="filter-status" onchange="loadIncidents()">
|
|
<option value="">Tutti gli stati</option>
|
|
<option value="detected">Rilevato</option>
|
|
<option value="analyzing">In Analisi</option>
|
|
<option value="containing">Contenimento</option>
|
|
<option value="eradicating">Eradicazione</option>
|
|
<option value="recovering">Ripristino</option>
|
|
<option value="closed">Chiuso</option>
|
|
<option value="post_mortem">Post Mortem</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="table-container">
|
|
<table class="table-clickable">
|
|
<thead>
|
|
<tr>
|
|
<th>Codice</th>
|
|
<th>Titolo</th>
|
|
<th>Classificazione</th>
|
|
<th>Severita'</th>
|
|
<th>Stato</th>
|
|
<th>Rilevato</th>
|
|
<th>NIS2 Art. 23</th>
|
|
<th>Azioni</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="incidents-table-body">
|
|
<tr>
|
|
<td colspan="8">
|
|
<div class="spinner" style="margin:40px auto;"></div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Detail View -->
|
|
<div id="view-detail" class="hidden"></div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<script src="/js/api.js"></script>
|
|
<script src="/js/common.js"></script>
|
|
<script src="/js/i18n.js"></script>
|
|
<script src="/js/help.js"></script>
|
|
<script>
|
|
// ── Auth & Init ──────────────────────────────────────────────
|
|
if (!checkAuth()) throw new Error('Not authenticated');
|
|
loadSidebar();
|
|
I18n.init();
|
|
HelpSystem.init();
|
|
|
|
// ── Labels ───────────────────────────────────────────────────
|
|
const CLASSIFICATION_LABELS = {
|
|
cyber_attack: 'Attacco Cyber', data_breach: 'Data Breach',
|
|
system_failure: 'Guasto Sistema', human_error: 'Errore Umano',
|
|
natural_disaster: 'Disastro Naturale', supply_chain: 'Supply Chain', other: 'Altro'
|
|
};
|
|
const SEVERITY_LABELS = { low: 'Basso', medium: 'Medio', high: 'Alto', critical: 'Critico' };
|
|
const STATUS_LABELS = {
|
|
detected: 'Rilevato', analyzing: 'In Analisi', containing: 'Contenimento',
|
|
eradicating: 'Eradicazione', recovering: 'Ripristino',
|
|
closed: 'Chiuso', post_mortem: 'Post Mortem'
|
|
};
|
|
|
|
// ── Load ─────────────────────────────────────────────────────
|
|
loadIncidents();
|
|
|
|
async function loadIncidents() {
|
|
const params = {};
|
|
const cls = document.getElementById('filter-classification').value;
|
|
const sev = document.getElementById('filter-severity').value;
|
|
const sts = document.getElementById('filter-status').value;
|
|
if (cls) params.classification = cls;
|
|
if (sev) params.severity = sev;
|
|
if (sts) params.status = sts;
|
|
|
|
try {
|
|
const result = await api.listIncidents(params);
|
|
if (result.success) {
|
|
renderIncidentsTable(result.data || []);
|
|
} else {
|
|
showNotification(result.message || 'Errore nel caricamento incidenti', 'error');
|
|
}
|
|
} catch (e) {
|
|
showNotification('Errore di connessione', 'error');
|
|
}
|
|
}
|
|
|
|
// ── Table Render ─────────────────────────────────────────────
|
|
function renderIncidentsTable(incidents) {
|
|
const tbody = document.getElementById('incidents-table-body');
|
|
|
|
if (!incidents || incidents.length === 0) {
|
|
tbody.innerHTML = `
|
|
<tr><td colspan="8">
|
|
<div class="empty-state">
|
|
<svg viewBox="0 0 20 20" fill="currentColor"><path d="M10 2a6 6 0 00-6 6v3.586l-.707.707A1 1 0 004 14h12a1 1 0 00.707-1.707L16 11.586V8a6 6 0 00-6-6zM10 18a3 3 0 01-3-3h6a3 3 0 01-3 3z"/></svg>
|
|
<h4>Nessun incidente registrato</h4>
|
|
<p>Clicca "Nuovo Incidente" per registrare un nuovo incidente di sicurezza.</p>
|
|
</div>
|
|
</td></tr>`;
|
|
return;
|
|
}
|
|
|
|
const now = new Date();
|
|
let html = '';
|
|
|
|
incidents.forEach(inc => {
|
|
const severityBadge = getSeverityBadge(inc.severity);
|
|
const statusBadge = getIncidentStatusBadge(inc.status);
|
|
const classLabel = CLASSIFICATION_LABELS[inc.classification] || inc.classification;
|
|
const nis2Status = renderNis2Compact(inc, now);
|
|
|
|
html += `
|
|
<tr onclick="viewIncidentDetail(${inc.id})">
|
|
<td><code style="font-size:0.8rem; color:var(--primary);">${escapeHtml(inc.incident_code || '-')}</code></td>
|
|
<td>
|
|
<strong>${escapeHtml(inc.title)}</strong>
|
|
${inc.is_significant ? '<span class="badge badge-danger" style="margin-left:6px; font-size:0.6rem;">SIGNIFICATIVO</span>' : ''}
|
|
</td>
|
|
<td><span class="tag">${escapeHtml(classLabel)}</span></td>
|
|
<td>${severityBadge}</td>
|
|
<td>${statusBadge}</td>
|
|
<td style="font-size:0.8rem;">${formatDateTime(inc.detected_at)}</td>
|
|
<td>${nis2Status}</td>
|
|
<td>
|
|
<div class="btn-group" onclick="event.stopPropagation()">
|
|
<button class="btn btn-ghost btn-sm btn-icon" onclick="showUpdateIncidentModal(${inc.id})" title="Aggiorna">
|
|
<svg viewBox="0 0 20 20" fill="currentColor"><path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"/></svg>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>`;
|
|
});
|
|
|
|
tbody.innerHTML = html;
|
|
}
|
|
|
|
function getSeverityBadge(severity) {
|
|
const cls = { critical: 'severity-critical', high: 'severity-high', medium: 'severity-medium', low: 'severity-low' };
|
|
return `<span class="badge ${cls[severity] || 'badge-neutral'}">${escapeHtml(SEVERITY_LABELS[severity] || severity)}</span>`;
|
|
}
|
|
|
|
function getIncidentStatusBadge(status) {
|
|
const map = {
|
|
detected: 'badge-danger', analyzing: 'badge-warning', containing: 'badge-primary',
|
|
eradicating: 'badge-primary', recovering: 'badge-info',
|
|
closed: 'badge-success', post_mortem: 'badge-neutral'
|
|
};
|
|
return `<span class="badge ${map[status] || 'badge-neutral'}">${escapeHtml(STATUS_LABELS[status] || status)}</span>`;
|
|
}
|
|
|
|
function renderNis2Compact(inc, now) {
|
|
if (!inc.is_significant) return '<span class="text-muted" style="font-size:0.75rem;">N/A</span>';
|
|
|
|
function dotClass(dueDate, sentAt) {
|
|
if (sentAt) return 'sent';
|
|
if (!dueDate) return 'pending';
|
|
const due = new Date(dueDate);
|
|
if (now > due) return 'overdue';
|
|
const hoursLeft = (due - now) / 3600000;
|
|
if (hoursLeft < 6) return 'approaching';
|
|
return 'pending';
|
|
}
|
|
|
|
const ew = dotClass(inc.early_warning_due, inc.early_warning_sent_at);
|
|
const nt = dotClass(inc.notification_due, inc.notification_sent_at);
|
|
const fr = dotClass(inc.final_report_due, inc.final_report_sent_at);
|
|
|
|
return `
|
|
<div class="nis2-compact">
|
|
<span class="nis2-dot ${ew}" title="Early Warning 24h"></span>
|
|
<span class="nis2-dot ${nt}" title="Notifica 72h"></span>
|
|
<span class="nis2-dot ${fr}" title="Report Finale 30gg"></span>
|
|
${ew === 'overdue' || nt === 'overdue' || fr === 'overdue'
|
|
? '<span class="overdue-icon"><svg width="14" height="14" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg></span>'
|
|
: ''}
|
|
</div>`;
|
|
}
|
|
|
|
// ── Create Incident Modal ────────────────────────────────────
|
|
function showCreateIncidentModal() {
|
|
const nowLocal = new Date();
|
|
const dtLocal = nowLocal.getFullYear() + '-' +
|
|
String(nowLocal.getMonth()+1).padStart(2,'0') + '-' +
|
|
String(nowLocal.getDate()).padStart(2,'0') + 'T' +
|
|
String(nowLocal.getHours()).padStart(2,'0') + ':' +
|
|
String(nowLocal.getMinutes()).padStart(2,'0');
|
|
|
|
const content = `
|
|
<div class="form-group">
|
|
<label class="form-label">Titolo <span class="required">*</span></label>
|
|
<input type="text" class="form-input" id="inc-title" placeholder="Titolo dell'incidente">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Descrizione <span class="required">*</span></label>
|
|
<textarea class="form-textarea" id="inc-description" rows="3" placeholder="Descrizione dettagliata dell'incidente"></textarea>
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label">Classificazione</label>
|
|
<select class="form-select" id="inc-classification">
|
|
<option value="cyber_attack">Attacco Cyber</option>
|
|
<option value="data_breach">Data Breach</option>
|
|
<option value="system_failure">Guasto Sistema</option>
|
|
<option value="human_error">Errore Umano</option>
|
|
<option value="natural_disaster">Disastro Naturale</option>
|
|
<option value="supply_chain">Supply Chain</option>
|
|
<option value="other">Altro</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Severita'</label>
|
|
<select class="form-select" id="inc-severity">
|
|
<option value="low">Basso</option>
|
|
<option value="medium" selected>Medio</option>
|
|
<option value="high">Alto</option>
|
|
<option value="critical">Critico</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Data e ora rilevamento <span class="required">*</span></label>
|
|
<input type="datetime-local" class="form-input" id="inc-detected-at" value="${dtLocal}">
|
|
</div>
|
|
<!-- Decision tree Art.23 NIS2 - significativita' incidente -->
|
|
<div style="background:var(--gray-50); border:1px solid var(--gray-200); border-radius:var(--border-radius); padding:14px; margin-bottom:16px;">
|
|
<div style="font-size:0.8rem; font-weight:700; color:var(--gray-700); margin-bottom:10px; display:flex; align-items:center; gap:6px;">
|
|
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16" style="color:var(--primary);"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/></svg>
|
|
Criteri Art. 23 NIS2 — Incidente Significativo
|
|
</div>
|
|
<p style="font-size:0.75rem; color:var(--gray-500); margin-bottom:10px;">Un incidente e' significativo se soddisfa almeno uno dei criteri seguenti (Considerando 101 NIS2 / D.Lgs. 138/2024 art. 25):</p>
|
|
<div style="display:grid; gap:6px;">
|
|
<label style="display:flex; align-items:center; gap:8px; font-size:0.8rem; cursor:pointer;">
|
|
<input type="checkbox" id="crit-users" onchange="evalSignificance()" style="accent-color:var(--primary); width:16px; height:16px;">
|
|
<span>Ha causato o puo' causare interruzione grave dei servizi per <strong>≥500 utenti</strong></span>
|
|
</label>
|
|
<label style="display:flex; align-items:center; gap:8px; font-size:0.8rem; cursor:pointer;">
|
|
<input type="checkbox" id="crit-duration" onchange="evalSignificance()" style="accent-color:var(--primary); width:16px; height:16px;">
|
|
<span>Durata dell'interruzione superiore a <strong>4 ore</strong></span>
|
|
</label>
|
|
<label style="display:flex; align-items:center; gap:8px; font-size:0.8rem; cursor:pointer;">
|
|
<input type="checkbox" id="crit-crossborder" onchange="evalSignificance()" style="accent-color:var(--primary); width:16px; height:16px;">
|
|
<span>Impatto su altri Stati Membri UE (impatto transfrontaliero)</span>
|
|
</label>
|
|
<label style="display:flex; align-items:center; gap:8px; font-size:0.8rem; cursor:pointer;">
|
|
<input type="checkbox" id="crit-attack" onchange="evalSignificance()" style="accent-color:var(--primary); width:16px; height:16px;">
|
|
<span>E' un attacco cyber o data breach con dati personali coinvolti</span>
|
|
</label>
|
|
<label style="display:flex; align-items:center; gap:8px; font-size:0.8rem; cursor:pointer;">
|
|
<input type="checkbox" id="crit-financial" onchange="evalSignificance()" style="accent-color:var(--primary); width:16px; height:16px;">
|
|
<span>Perdita finanziaria stimata superiore a <strong>€100.000</strong></span>
|
|
</label>
|
|
</div>
|
|
<div id="significance-result" style="margin-top:10px; padding:8px 12px; border-radius:6px; font-size:0.8rem; font-weight:600; display:none;"></div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-check-inline">
|
|
<input type="checkbox" id="inc-significant">
|
|
<span><strong>Incidente significativo</strong> (richiede notifica CSIRT/ACN ai sensi dell'Art. 23 NIS2)</span>
|
|
</label>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Servizi interessati</label>
|
|
<textarea class="form-textarea" id="inc-affected-services" rows="2" placeholder="Elenco dei servizi e sistemi interessati"></textarea>
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label">Utenti coinvolti</label>
|
|
<input type="number" class="form-input" id="inc-affected-users" min="0" placeholder="0">
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-check-inline">
|
|
<input type="checkbox" id="inc-cross-border">
|
|
<span>Impatto transfrontaliero</span>
|
|
</label>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-check-inline">
|
|
<input type="checkbox" id="inc-malicious">
|
|
<span>Azione dolosa / malevola</span>
|
|
</label>
|
|
</div>
|
|
`;
|
|
|
|
showModal('Registra Nuovo Incidente', content, {
|
|
size: 'lg',
|
|
footer: `
|
|
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
|
|
<button class="btn btn-primary" onclick="createIncident()">Registra Incidente</button>
|
|
`
|
|
});
|
|
}
|
|
|
|
async function createIncident() {
|
|
const data = {
|
|
title: document.getElementById('inc-title').value.trim(),
|
|
description: document.getElementById('inc-description').value.trim(),
|
|
classification: document.getElementById('inc-classification').value,
|
|
severity: document.getElementById('inc-severity').value,
|
|
detected_at: document.getElementById('inc-detected-at').value,
|
|
is_significant: document.getElementById('inc-significant').checked ? 1 : 0,
|
|
affected_services: document.getElementById('inc-affected-services').value.trim(),
|
|
affected_users_count: parseInt(document.getElementById('inc-affected-users').value) || 0,
|
|
cross_border_impact: document.getElementById('inc-cross-border').checked ? 1 : 0,
|
|
malicious_action: document.getElementById('inc-malicious').checked ? 1 : 0,
|
|
};
|
|
|
|
if (!data.title) { showNotification('Il titolo e\' obbligatorio', 'warning'); return; }
|
|
if (!data.description) { showNotification('La descrizione e\' obbligatoria', 'warning'); return; }
|
|
if (!data.detected_at) { showNotification('La data di rilevamento e\' obbligatoria', 'warning'); return; }
|
|
|
|
const btn = document.querySelector('#modal-overlay .btn-primary');
|
|
setButtonLoading(btn, true);
|
|
try {
|
|
const result = await api.createIncident(data);
|
|
if (result.success) {
|
|
closeModal();
|
|
const msg = data.is_significant
|
|
? 'Incidente significativo registrato. Scadenze NIS2 Art. 23 attivate.'
|
|
: 'Incidente registrato con successo.';
|
|
showNotification(msg, 'success');
|
|
loadIncidents();
|
|
} else {
|
|
setButtonLoading(btn, false);
|
|
showNotification(result.message || 'Errore nella creazione', 'error');
|
|
}
|
|
} catch (e) {
|
|
setButtonLoading(btn, false);
|
|
showNotification('Errore di connessione', 'error');
|
|
}
|
|
}
|
|
|
|
// ── Update Incident Modal ────────────────────────────────────
|
|
async function showUpdateIncidentModal(id) {
|
|
let incident = {};
|
|
try {
|
|
const result = await api.getIncident(id);
|
|
if (result.success) incident = result.data;
|
|
} catch (e) { /* defaults */ }
|
|
|
|
const content = `
|
|
<div class="form-group">
|
|
<label class="form-label">Stato</label>
|
|
<select class="form-select" id="upd-status">
|
|
${Object.entries(STATUS_LABELS).map(([k, v]) =>
|
|
`<option value="${k}" ${incident.status === k ? 'selected' : ''}>${v}</option>`
|
|
).join('')}
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Causa Radice</label>
|
|
<textarea class="form-textarea" id="upd-root-cause" rows="3" placeholder="Analisi della causa radice">${escapeHtml(incident.root_cause || '')}</textarea>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Azioni di Rimedio</label>
|
|
<textarea class="form-textarea" id="upd-remediation" rows="3" placeholder="Azioni correttive intraprese">${escapeHtml(incident.remediation_actions || '')}</textarea>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Lezioni Apprese</label>
|
|
<textarea class="form-textarea" id="upd-lessons" rows="3" placeholder="Lezioni apprese dall'incidente">${escapeHtml(incident.lessons_learned || '')}</textarea>
|
|
</div>
|
|
`;
|
|
|
|
showModal('Aggiorna Incidente - ' + escapeHtml(incident.incident_code || ''), content, {
|
|
footer: `
|
|
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
|
|
<button class="btn btn-primary" onclick="updateIncident(${id})">Salva Aggiornamento</button>
|
|
`
|
|
});
|
|
}
|
|
|
|
async function updateIncident(id) {
|
|
const data = {
|
|
status: document.getElementById('upd-status').value,
|
|
root_cause: document.getElementById('upd-root-cause').value.trim(),
|
|
remediation_actions: document.getElementById('upd-remediation').value.trim(),
|
|
lessons_learned: document.getElementById('upd-lessons').value.trim(),
|
|
};
|
|
|
|
try {
|
|
const result = await api.updateIncident(id, data);
|
|
if (result.success) {
|
|
closeModal();
|
|
showNotification('Incidente aggiornato', 'success');
|
|
loadIncidents();
|
|
if (!document.getElementById('view-detail').classList.contains('hidden')) {
|
|
viewIncidentDetail(id);
|
|
}
|
|
} else {
|
|
showNotification(result.message || 'Errore nell\'aggiornamento', 'error');
|
|
}
|
|
} catch (e) {
|
|
showNotification('Errore di connessione', 'error');
|
|
}
|
|
}
|
|
|
|
// ── Detail View ──────────────────────────────────────────────
|
|
async function viewIncidentDetail(id) {
|
|
try {
|
|
const result = await api.getIncident(id);
|
|
if (!result.success) {
|
|
showNotification(result.message || 'Errore nel caricamento', 'error');
|
|
return;
|
|
}
|
|
renderIncidentDetail(result.data);
|
|
} catch (e) {
|
|
showNotification('Errore di connessione', 'error');
|
|
}
|
|
}
|
|
|
|
function renderIncidentDetail(inc) {
|
|
document.getElementById('view-list').classList.add('hidden');
|
|
const container = document.getElementById('view-detail');
|
|
container.classList.remove('hidden');
|
|
|
|
const now = new Date();
|
|
const classLabel = CLASSIFICATION_LABELS[inc.classification] || inc.classification;
|
|
|
|
// NIS2 Timeline
|
|
let nis2Html = '';
|
|
if (inc.is_significant) {
|
|
nis2Html = renderNis2Timeline(inc, now);
|
|
} else {
|
|
nis2Html = `
|
|
<div class="card mb-24">
|
|
<div class="card-header"><h3>NIS2 Art. 23 - Notifiche</h3></div>
|
|
<div class="card-body">
|
|
<p class="text-muted" style="font-size:0.85rem;">
|
|
Questo incidente non e' stato classificato come significativo.
|
|
Le notifiche NIS2 Art. 23 non sono richieste.
|
|
</p>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
// Event Timeline
|
|
let timelineHtml = '';
|
|
if (inc.timeline && inc.timeline.length > 0) {
|
|
inc.timeline.forEach(ev => {
|
|
const typeClass = 'type-' + (ev.event_type || 'action');
|
|
timelineHtml += `
|
|
<div class="event-item ${typeClass}">
|
|
<div class="event-item-time">${formatDateTime(ev.created_at)}</div>
|
|
<div class="event-item-text">${escapeHtml(ev.description)}</div>
|
|
${ev.created_by_name ? `<div class="event-item-author">di ${escapeHtml(ev.created_by_name)}</div>` : ''}
|
|
</div>`;
|
|
});
|
|
} else {
|
|
timelineHtml = '<p class="text-muted" style="font-size:0.85rem;">Nessun evento nella timeline.</p>';
|
|
}
|
|
|
|
container.innerHTML = `
|
|
<button class="detail-back" onclick="backToList()">
|
|
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z" clip-rule="evenodd"/></svg>
|
|
Torna alla lista
|
|
</button>
|
|
|
|
<div class="detail-grid">
|
|
<div>
|
|
<!-- Info Card -->
|
|
<div class="card mb-24">
|
|
<div class="card-header">
|
|
<h3>
|
|
<code style="color:var(--primary); margin-right:8px;">${escapeHtml(inc.incident_code || '')}</code>
|
|
${escapeHtml(inc.title)}
|
|
</h3>
|
|
<div class="btn-group">
|
|
<button class="btn btn-secondary btn-sm" onclick="showUpdateIncidentModal(${inc.id})">Aggiorna</button>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="form-row mb-16">
|
|
<div class="detail-field">
|
|
<div class="detail-field-label">Classificazione</div>
|
|
<div class="detail-field-value"><span class="tag">${escapeHtml(classLabel)}</span></div>
|
|
</div>
|
|
<div class="detail-field">
|
|
<div class="detail-field-label">Severita'</div>
|
|
<div class="detail-field-value">${getSeverityBadge(inc.severity)}</div>
|
|
</div>
|
|
<div class="detail-field">
|
|
<div class="detail-field-label">Stato</div>
|
|
<div class="detail-field-value">${getIncidentStatusBadge(inc.status)}</div>
|
|
</div>
|
|
</div>
|
|
|
|
${inc.description ? `
|
|
<div class="detail-field">
|
|
<div class="detail-field-label">Descrizione</div>
|
|
<div class="detail-field-value">${escapeHtml(inc.description)}</div>
|
|
</div>` : ''}
|
|
|
|
<div class="form-row">
|
|
<div class="detail-field">
|
|
<div class="detail-field-label">Data Rilevamento</div>
|
|
<div class="detail-field-value">${formatDateTime(inc.detected_at)}</div>
|
|
</div>
|
|
${inc.reported_by_name ? `
|
|
<div class="detail-field">
|
|
<div class="detail-field-label">Segnalato da</div>
|
|
<div class="detail-field-value">${escapeHtml(inc.reported_by_name)}</div>
|
|
</div>` : ''}
|
|
${inc.assigned_to_name ? `
|
|
<div class="detail-field">
|
|
<div class="detail-field-label">Assegnato a</div>
|
|
<div class="detail-field-value">${escapeHtml(inc.assigned_to_name)}</div>
|
|
</div>` : ''}
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
${inc.affected_services ? `
|
|
<div class="detail-field">
|
|
<div class="detail-field-label">Servizi Interessati</div>
|
|
<div class="detail-field-value">${escapeHtml(inc.affected_services)}</div>
|
|
</div>` : ''}
|
|
${inc.affected_users_count ? `
|
|
<div class="detail-field">
|
|
<div class="detail-field-label">Utenti Coinvolti</div>
|
|
<div class="detail-field-value">${inc.affected_users_count}</div>
|
|
</div>` : ''}
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="detail-field">
|
|
<div class="detail-field-label">Incidente Significativo</div>
|
|
<div class="detail-field-value">${inc.is_significant ? '<span class="badge badge-danger">Si</span>' : '<span class="badge badge-neutral">No</span>'}</div>
|
|
</div>
|
|
<div class="detail-field">
|
|
<div class="detail-field-label">Impatto Transfrontaliero</div>
|
|
<div class="detail-field-value">${inc.cross_border_impact ? 'Si' : 'No'}</div>
|
|
</div>
|
|
<div class="detail-field">
|
|
<div class="detail-field-label">Azione Malevola</div>
|
|
<div class="detail-field-value">${inc.malicious_action ? 'Si' : 'No'}</div>
|
|
</div>
|
|
</div>
|
|
|
|
${inc.root_cause ? `
|
|
<div class="detail-field">
|
|
<div class="detail-field-label">Causa Radice</div>
|
|
<div class="detail-field-value">${escapeHtml(inc.root_cause)}</div>
|
|
</div>` : ''}
|
|
${inc.remediation_actions ? `
|
|
<div class="detail-field">
|
|
<div class="detail-field-label">Azioni di Rimedio</div>
|
|
<div class="detail-field-value">${escapeHtml(inc.remediation_actions)}</div>
|
|
</div>` : ''}
|
|
${inc.lessons_learned ? `
|
|
<div class="detail-field">
|
|
<div class="detail-field-label">Lezioni Apprese</div>
|
|
<div class="detail-field-value">${escapeHtml(inc.lessons_learned)}</div>
|
|
</div>` : ''}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- NIS2 Timeline -->
|
|
${nis2Html}
|
|
|
|
<!-- Event Timeline -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3>Timeline Eventi</h3>
|
|
<button class="btn btn-sm btn-primary" onclick="showAddEventModal(${inc.id})">+ Aggiungi Evento</button>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="event-timeline">
|
|
${timelineHtml}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<!-- Summary -->
|
|
<div class="card mb-24">
|
|
<div class="card-header"><h3>Riepilogo</h3></div>
|
|
<div class="card-body">
|
|
<div class="detail-field">
|
|
<div class="detail-field-label">Codice</div>
|
|
<div class="detail-field-value"><code>${escapeHtml(inc.incident_code || '-')}</code></div>
|
|
</div>
|
|
<div class="detail-field">
|
|
<div class="detail-field-label">Creato il</div>
|
|
<div class="detail-field-value">${formatDateTime(inc.created_at)}</div>
|
|
</div>
|
|
<div class="detail-field">
|
|
<div class="detail-field-label">Aggiornato il</div>
|
|
<div class="detail-field-value">${formatDateTime(inc.updated_at)}</div>
|
|
</div>
|
|
${inc.closed_at ? `
|
|
<div class="detail-field">
|
|
<div class="detail-field-label">Chiuso il</div>
|
|
<div class="detail-field-value">${formatDateTime(inc.closed_at)}</div>
|
|
</div>` : ''}
|
|
<div class="detail-field">
|
|
<div class="detail-field-label">Eventi Timeline</div>
|
|
<div class="detail-field-value">${inc.timeline ? inc.timeline.length : 0}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Actions -->
|
|
${inc.is_significant ? `
|
|
<div class="card">
|
|
<div class="card-header"><h3>Azioni Rapide</h3></div>
|
|
<div class="card-body" style="display:flex; flex-direction:column; gap:8px;">
|
|
${!inc.early_warning_sent_at ? `<button class="btn btn-warning w-full" onclick="doSendEarlyWarning(${inc.id})">Invia Early Warning (24h)</button>` : `<button class="btn btn-success w-full" disabled>Early Warning Inviato</button>`}
|
|
${!inc.notification_sent_at ? `<button class="btn btn-warning w-full" onclick="doSendNotification(${inc.id})">Invia Notifica (72h)</button>` : `<button class="btn btn-success w-full" disabled>Notifica Inviata</button>`}
|
|
${!inc.final_report_sent_at ? `<button class="btn btn-warning w-full" onclick="doSendFinalReport(${inc.id})">Invia Report Finale (30gg)</button>` : `<button class="btn btn-success w-full" disabled>Report Finale Inviato</button>`}
|
|
</div>
|
|
</div>` : ''}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// ── NIS2 Art. 23 Timeline Render ─────────────────────────────
|
|
function renderNis2Timeline(inc, now) {
|
|
const steps = [
|
|
{
|
|
title: 'Early Warning (24h)',
|
|
dueDate: inc.early_warning_due,
|
|
sentAt: inc.early_warning_sent_at,
|
|
description: 'Allerta iniziale al CSIRT nazionale (ACN) entro 24 ore dal rilevamento.',
|
|
action: `doSendEarlyWarning(${inc.id})`,
|
|
actionLabel: 'Invia Early Warning'
|
|
},
|
|
{
|
|
title: 'Notifica Incidente (72h)',
|
|
dueDate: inc.notification_due,
|
|
sentAt: inc.notification_sent_at,
|
|
description: 'Notifica formale al CSIRT nazionale (ACN) con dettagli dell\'incidente entro 72 ore.',
|
|
action: `doSendNotification(${inc.id})`,
|
|
actionLabel: 'Invia Notifica'
|
|
},
|
|
{
|
|
title: 'Report Finale (30 giorni)',
|
|
dueDate: inc.final_report_due,
|
|
sentAt: inc.final_report_sent_at,
|
|
description: 'Report finale completo con analisi della causa radice, impatto e lezioni apprese entro 30 giorni.',
|
|
action: `doSendFinalReport(${inc.id})`,
|
|
actionLabel: 'Invia Report Finale'
|
|
}
|
|
];
|
|
|
|
let stepsHtml = '';
|
|
steps.forEach(step => {
|
|
let statusClass = '';
|
|
let dotClass = '';
|
|
let countdownHtml = '';
|
|
let actionHtml = '';
|
|
|
|
if (step.sentAt) {
|
|
statusClass = 'sent';
|
|
dotClass = 'sent';
|
|
countdownHtml = `<div class="nis2-step-countdown countdown-ok">Inviato il ${formatDateTime(step.sentAt)}</div>`;
|
|
} else if (step.dueDate) {
|
|
const due = new Date(step.dueDate);
|
|
const diffMs = due - now;
|
|
|
|
if (diffMs < 0) {
|
|
// Overdue
|
|
statusClass = 'overdue';
|
|
dotClass = 'overdue';
|
|
const overdueMins = Math.floor(-diffMs / 60000);
|
|
const overdueHours = Math.floor(overdueMins / 60);
|
|
const overdueDays = Math.floor(overdueHours / 24);
|
|
let overdueText = '';
|
|
if (overdueDays > 0) overdueText = `Scaduto da ${overdueDays} giorn${overdueDays === 1 ? 'o' : 'i'} e ${overdueHours % 24} ore`;
|
|
else if (overdueHours > 0) overdueText = `Scaduto da ${overdueHours} or${overdueHours === 1 ? 'a' : 'e'} e ${overdueMins % 60} min`;
|
|
else overdueText = `Scaduto da ${overdueMins} minut${overdueMins === 1 ? 'o' : 'i'}`;
|
|
countdownHtml = `<div class="nis2-step-countdown countdown-overdue">${overdueText}</div>`;
|
|
actionHtml = `<button class="btn btn-danger btn-sm mt-8" onclick="${step.action}">${step.actionLabel} (URGENTE)</button>`;
|
|
} else {
|
|
// Not yet due
|
|
const minsLeft = Math.floor(diffMs / 60000);
|
|
const hoursLeft = Math.floor(minsLeft / 60);
|
|
const daysLeft = Math.floor(hoursLeft / 24);
|
|
let timeText = '';
|
|
if (daysLeft > 0) timeText = `${daysLeft} giorn${daysLeft === 1 ? 'o' : 'i'} e ${hoursLeft % 24} ore rimanenti`;
|
|
else if (hoursLeft > 0) timeText = `${hoursLeft} or${hoursLeft === 1 ? 'a' : 'e'} e ${minsLeft % 60} min rimanenti`;
|
|
else timeText = `${minsLeft} minut${minsLeft === 1 ? 'o' : 'i'} rimanenti`;
|
|
|
|
if (hoursLeft < 6) {
|
|
statusClass = 'approaching';
|
|
dotClass = 'approaching';
|
|
countdownHtml = `<div class="nis2-step-countdown countdown-approaching">${timeText}</div>`;
|
|
} else {
|
|
countdownHtml = `<div class="nis2-step-countdown" style="color:var(--gray-600);">${timeText}</div>`;
|
|
}
|
|
actionHtml = `<button class="btn btn-primary btn-sm mt-8" onclick="${step.action}">${step.actionLabel}</button>`;
|
|
}
|
|
}
|
|
|
|
const checkIcon = '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>';
|
|
const clockIcon = '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"/></svg>';
|
|
const warnIcon = '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>';
|
|
|
|
let icon = clockIcon;
|
|
if (step.sentAt) icon = checkIcon;
|
|
else if (statusClass === 'overdue') icon = warnIcon;
|
|
|
|
stepsHtml += `
|
|
<div class="nis2-step">
|
|
<div class="nis2-step-dot ${dotClass}">${icon}</div>
|
|
<div class="nis2-step-content ${statusClass}">
|
|
<div class="nis2-step-header">
|
|
<div class="nis2-step-title">${step.title}</div>
|
|
${step.dueDate ? `<div class="nis2-step-time">Scadenza: ${formatDateTime(step.dueDate)}</div>` : ''}
|
|
</div>
|
|
<p style="font-size:0.8rem; color:var(--gray-600); margin:0;">${step.description}</p>
|
|
${countdownHtml}
|
|
${!step.sentAt ? actionHtml : ''}
|
|
</div>
|
|
</div>`;
|
|
});
|
|
|
|
return `
|
|
<div class="card mb-24">
|
|
<div class="card-header">
|
|
<h3>NIS2 Art. 23 - Obblighi di Notifica</h3>
|
|
<span class="badge badge-danger">Incidente Significativo</span>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="nis2-timeline">
|
|
${stepsHtml}
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
// ── NIS2 Actions ─────────────────────────────────────────────
|
|
async function doSendEarlyWarning(id) {
|
|
showModal('Conferma Early Warning', `
|
|
<p>Stai per registrare l'invio dell'<strong>Early Warning (24h)</strong> al CSIRT nazionale (ACN).</p>
|
|
<p class="text-muted" style="margin-top:8px; font-size:0.85rem;">
|
|
Questa azione registra nel sistema che l'allerta iniziale e' stata inviata.
|
|
Assicurati di aver effettivamente trasmesso la comunicazione tramite i canali ufficiali.
|
|
</p>
|
|
`, {
|
|
size: 'sm',
|
|
footer: `
|
|
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
|
|
<button class="btn btn-primary" onclick="executeSendEarlyWarning(${id})">Conferma Invio</button>
|
|
`
|
|
});
|
|
}
|
|
|
|
async function executeSendEarlyWarning(id) {
|
|
try {
|
|
const result = await api.sendEarlyWarning(id);
|
|
closeModal();
|
|
if (result.success) {
|
|
showNotification('Early Warning registrato con successo', 'success');
|
|
viewIncidentDetail(id);
|
|
} else {
|
|
showNotification(result.message || 'Errore', 'error');
|
|
}
|
|
} catch (e) {
|
|
showNotification('Errore di connessione', 'error');
|
|
}
|
|
}
|
|
|
|
async function doSendNotification(id) {
|
|
showModal('Conferma Notifica CSIRT', `
|
|
<p>Stai per registrare l'invio della <strong>Notifica Incidente (72h)</strong> al CSIRT nazionale (ACN).</p>
|
|
<p class="text-muted" style="margin-top:8px; font-size:0.85rem;">
|
|
La notifica deve contenere una valutazione iniziale dell'incidente, la sua gravita' e il suo impatto,
|
|
nonche' gli indicatori di compromissione, ove disponibili.
|
|
</p>
|
|
`, {
|
|
size: 'sm',
|
|
footer: `
|
|
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
|
|
<button class="btn btn-primary" onclick="executeSendNotification(${id})">Conferma Invio</button>
|
|
`
|
|
});
|
|
}
|
|
|
|
async function executeSendNotification(id) {
|
|
try {
|
|
const result = await api.sendNotification(id);
|
|
closeModal();
|
|
if (result.success) {
|
|
showNotification('Notifica CSIRT registrata con successo', 'success');
|
|
viewIncidentDetail(id);
|
|
} else {
|
|
showNotification(result.message || 'Errore', 'error');
|
|
}
|
|
} catch (e) {
|
|
showNotification('Errore di connessione', 'error');
|
|
}
|
|
}
|
|
|
|
async function doSendFinalReport(id) {
|
|
showModal('Conferma Report Finale', `
|
|
<p>Stai per registrare l'invio del <strong>Report Finale (30 giorni)</strong> al CSIRT nazionale (ACN).</p>
|
|
<p class="text-muted" style="margin-top:8px; font-size:0.85rem;">
|
|
Il report finale deve includere la descrizione dettagliata dell'incidente, la causa radice,
|
|
le misure di mitigazione applicate e l'impatto transfrontaliero, se applicabile.
|
|
</p>
|
|
`, {
|
|
size: 'sm',
|
|
footer: `
|
|
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
|
|
<button class="btn btn-primary" onclick="executeSendFinalReport(${id})">Conferma Invio</button>
|
|
`
|
|
});
|
|
}
|
|
|
|
async function executeSendFinalReport(id) {
|
|
try {
|
|
const result = await api.sendFinalReport(id);
|
|
closeModal();
|
|
if (result.success) {
|
|
showNotification('Report finale registrato con successo', 'success');
|
|
viewIncidentDetail(id);
|
|
} else {
|
|
showNotification(result.message || 'Errore', 'error');
|
|
}
|
|
} catch (e) {
|
|
showNotification('Errore di connessione', 'error');
|
|
}
|
|
}
|
|
|
|
// ── Add Timeline Event Modal ─────────────────────────────────
|
|
function showAddEventModal(incidentId) {
|
|
const content = `
|
|
<div class="form-group">
|
|
<label class="form-label">Tipo Evento</label>
|
|
<select class="form-select" id="event-type">
|
|
<option value="detection">Rilevamento</option>
|
|
<option value="analysis">Analisi</option>
|
|
<option value="containment">Contenimento</option>
|
|
<option value="eradication">Eradicazione</option>
|
|
<option value="recovery">Ripristino</option>
|
|
<option value="notification">Notifica</option>
|
|
<option value="action">Azione</option>
|
|
<option value="update">Aggiornamento</option>
|
|
<option value="other">Altro</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Descrizione <span class="required">*</span></label>
|
|
<textarea class="form-textarea" id="event-description" rows="3" placeholder="Descrizione dell'evento"></textarea>
|
|
</div>
|
|
`;
|
|
|
|
showModal('Aggiungi Evento alla Timeline', content, {
|
|
footer: `
|
|
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
|
|
<button class="btn btn-primary" onclick="addTimelineEvent(${incidentId})">Aggiungi Evento</button>
|
|
`
|
|
});
|
|
}
|
|
|
|
async function addTimelineEvent(incidentId) {
|
|
const data = {
|
|
event_type: document.getElementById('event-type').value,
|
|
description: document.getElementById('event-description').value.trim(),
|
|
};
|
|
|
|
if (!data.description) {
|
|
showNotification('La descrizione e\' obbligatoria', 'warning');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const result = await api.post(`/incidents/${incidentId}/timeline`, data);
|
|
if (result.success) {
|
|
closeModal();
|
|
showNotification('Evento aggiunto alla timeline', 'success');
|
|
viewIncidentDetail(incidentId);
|
|
} else {
|
|
showNotification(result.message || 'Errore nel salvataggio', 'error');
|
|
}
|
|
} catch (e) {
|
|
showNotification('Errore di connessione', 'error');
|
|
}
|
|
}
|
|
|
|
// ── Art.23 Significance Evaluator ────────────────────────────
|
|
function evalSignificance() {
|
|
const criteria = [
|
|
document.getElementById('crit-users'),
|
|
document.getElementById('crit-duration'),
|
|
document.getElementById('crit-crossborder'),
|
|
document.getElementById('crit-attack'),
|
|
document.getElementById('crit-financial'),
|
|
].filter(Boolean);
|
|
|
|
const checkedCount = criteria.filter(c => c.checked).length;
|
|
const resultDiv = document.getElementById('significance-result');
|
|
const sigCheckbox = document.getElementById('inc-significant');
|
|
|
|
if (!resultDiv) return;
|
|
resultDiv.style.display = 'block';
|
|
|
|
if (checkedCount >= 1) {
|
|
resultDiv.style.background = '#fef2f2';
|
|
resultDiv.style.border = '1px solid #fca5a5';
|
|
resultDiv.style.color = 'var(--danger)';
|
|
resultDiv.innerHTML = '⚠ ' + checkedCount + ' criteri soddisfatti — l'incidente e' probabilmente <strong>SIGNIFICATIVO</strong>. Attivare le notifiche Art. 23 (24h/72h/30gg).';
|
|
if (sigCheckbox) sigCheckbox.checked = true;
|
|
} else {
|
|
resultDiv.style.background = '#f0fdf4';
|
|
resultDiv.style.border = '1px solid #bbf7d0';
|
|
resultDiv.style.color = 'var(--secondary)';
|
|
resultDiv.innerHTML = '✓ Nessun criterio soddisfatto — l'incidente appare <strong>NON significativo</strong> ai fini Art. 23. Verificare manualmente prima di confermare.';
|
|
if (sigCheckbox) sigCheckbox.checked = false;
|
|
}
|
|
}
|
|
|
|
// ── Navigation ───────────────────────────────────────────────
|
|
function backToList() {
|
|
document.getElementById('view-detail').classList.add('hidden');
|
|
document.getElementById('view-list').classList.remove('hidden');
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|