[SEC+UX] Hardening sicurezza + miglioramenti UX pre-audit

SICUREZZA:
- index.php: rimosso CORS wildcard in debug mode (solo origini autorizzate)
- AuthController: getClientIP() con X-Forwarded-For sicuro (proxy-aware)
- AuthController: refresh token con SELECT FOR UPDATE in transazione atomica
- AIService: anonimizzazione dati org nei prompt Anthropic API (no nome/fatturato)

UX AUDIT-READY:
- dashboard.html: gauge rinominato 'Avanzamento implementazione misure Art.21'
- incidents.html: decision tree Art.23 con 5 criteri per 'Is Significant?'
- policies.html: banner warning obbligatorio su bozze generate da AI
- risks.html: tooltip dettagliati scala Likelihood/Impact (ISO 27005)
- assessment.html: progress bar % completamento risposta domande

DB:
- migration 006: indici performance + audit_log immutabile (trigger) + soft delete

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-02-20 12:01:33 +01:00
co-authored by Claude Sonnet 4.6
parent 0e78ec24c1
commit 782389849f
9 changed files with 282 additions and 39 deletions
+31
View File
@@ -44,6 +44,20 @@
<!-- Steps Indicator -->
<div class="steps" id="steps-indicator"></div>
<!-- Progress Bar -->
<div style="margin-bottom:16px;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:6px;">
<span style="font-size:0.78rem; font-weight:600; color:var(--gray-600);">Avanzamento risposta domande</span>
<span style="font-size:0.78rem; font-weight:700; color:var(--primary);" id="progress-pct">0%</span>
</div>
<div style="background:var(--gray-200); border-radius:99px; height:8px; overflow:hidden;">
<div id="progress-bar-fill" style="height:100%; background:var(--primary); border-radius:99px; transition:width 0.3s ease; width:0%;"></div>
</div>
<div style="font-size:0.7rem; color:var(--gray-400); margin-top:4px;">
<span id="progress-answered">0</span> di <span id="progress-total">0</span> domande con risposta
</div>
</div>
<!-- Question Card -->
<div class="card mb-24">
<div class="card-header">
@@ -295,6 +309,22 @@
}
}
function updateProgressBar() {
const total = questions.length;
const answered = Object.values(responses).filter(r => r && r.answer).length;
const pct = total > 0 ? Math.round((answered / total) * 100) : 0;
const pctEl = document.getElementById('progress-pct');
const fillEl = document.getElementById('progress-bar-fill');
const answeredEl = document.getElementById('progress-answered');
const totalEl = document.getElementById('progress-total');
if (pctEl) pctEl.textContent = pct + '%';
if (fillEl) fillEl.style.width = pct + '%';
if (answeredEl) answeredEl.textContent = answered;
if (totalEl) totalEl.textContent = total;
}
function renderCurrentQuestion() {
if (categories.length === 0) return;
@@ -365,6 +395,7 @@
`;
document.getElementById('question-area').innerHTML = html;
updateProgressBar();
// Navigation buttons
document.getElementById('btn-prev').disabled = (currentCategoryIdx === 0 && currentQuestionIdx === 0);
+2 -1
View File
@@ -29,7 +29,8 @@
<div id="compliance-gauge">
<div class="spinner-lg" style="margin:40px auto;"></div>
</div>
<p class="text-muted mt-8" style="font-size:0.8rem;">Punteggio complessivo di conformita' NIS2</p>
<p class="text-muted mt-8" style="font-size:0.8rem;">Avanzamento implementazione misure Art.21 NIS2</p>
<p class="text-muted" style="font-size:0.7rem; margin-top:2px;">Misura l'implementazione tecnica, non la conformita' legale</p>
</div>
</div>
+63
View File
@@ -519,6 +519,37 @@
<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>&ge;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">
@@ -1142,6 +1173,38 @@
}
}
// ── 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 = '&#9888; ' + 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 = '&#10003; 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');
+1 -2
View File
@@ -19,9 +19,8 @@ $origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, CORS_ALLOWED_ORIGINS)) {
header("Access-Control-Allow-Origin: {$origin}");
} elseif (APP_DEBUG) {
header("Access-Control-Allow-Origin: *");
}
// NOTE: No wildcard CORS even in debug mode — use CORS_ALLOWED_ORIGINS in config
header("Access-Control-Allow-Methods: " . CORS_ALLOWED_METHODS);
header("Access-Control-Allow-Headers: " . CORS_ALLOWED_HEADERS);
+8
View File
@@ -519,6 +519,14 @@
<span class="badge ${badgeClass}">${escapeHtml(statusLabel)}</span>
${policy.ai_generated ? '<span class="badge badge-info" style="margin-left:6px;">AI Generata</span>' : ''}
</div>
${policy.ai_generated && (policy.status === 'draft' || policy.status === 'review') ? `
<div style="background:#fff7ed; border:1px solid #fed7aa; border-radius:var(--border-radius); padding:12px 16px; margin-top:12px; display:flex; gap:10px; align-items:flex-start;">
<svg viewBox="0 0 20 20" fill="#c2410c" width="18" height="18" style="flex-shrink:0; margin-top:2px;"><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>
<div>
<strong style="color:#c2410c; font-size:0.875rem;">Bozza AI — Revisione obbligatoria prima dell'approvazione</strong>
<p style="font-size:0.78rem; color:#92400e; margin:4px 0 0;">Questo documento e' stato generato automaticamente da un sistema AI. Prima di approvarlo, verificare che sia conforme alle procedure interne, al quadro normativo applicabile e alla realta' specifica dell'organizzazione. L'AI puo' produrre contenuti imprecisi o incompleti.</p>
</div>
</div>` : ''}
</div>
<div class="detail-header-actions">
${policy.status === 'draft' || policy.status === 'review' ? `
+26 -12
View File
@@ -442,7 +442,25 @@
const TREATMENT_STATUS_LABELS = {
planned: 'Pianificato', in_progress: 'In Corso', completed: 'Completato', cancelled: 'Annullato'
};
// Scala Likelihood (probabilità annua)
const LIKELIHOOD_LABELS = ['', 'Molto Basso', 'Basso', 'Medio', 'Alto', 'Molto Alto'];
const LIKELIHOOD_DETAILS = [
'',
'1 - Remota: <1% probabilità annua (evento eccezionale)',
'2 - Improbabile: 1-10% (evento raro ma documentato nel settore)',
'3 - Possibile: 10-30% (può verificarsi nel corso dell'attività)',
'4 - Probabile: 30-70% (ci si aspetta che avvenga almeno una volta l'anno)',
'5 - Quasi Certa: >70% (si verificherà con elevata probabilità)',
];
// Scala Impact (danno economico / utenti impattati)
const IMPACT_DETAILS = [
'',
'1 - Minimo: <€10k danno, <100 utenti, servizio ripristinato in <1h',
'2 - Basso: €10k-50k, 100-500 utenti, ripristino <8h',
'3 - Significativo: €50k-500k, 500-5000 utenti, ripristino <24h',
'4 - Grave: €500k-10M, 5000-50000 utenti, ripristino <7 giorni',
'5 - Catastrofico: >€10M, >50000 utenti, servizi critici nazionali impattati',
];
// ── State ────────────────────────────────────────────────────
let currentView = 'table';
@@ -878,22 +896,16 @@
<div class="form-row">
<div class="form-group">
<label class="form-label">Probabilita': <span id="likelihood-label">${LIKELIHOOD_LABELS[likelihood]}</span></label>
<label class="form-label">Probabilita' <span style="color:var(--gray-400); font-size:0.75rem; font-weight:400;">(frequenza stimata annua)</span></label>
<div class="range-value-display" id="likelihood-value">${likelihood}</div>
<input type="range" class="form-range" id="risk-likelihood" min="1" max="5" value="${likelihood}" oninput="updateRiskScore()">
<div class="form-range-labels">
<span>1 - Molto Basso</span>
<span>5 - Molto Alto</span>
</div>
<div style="font-size:0.75rem; color:var(--gray-500); margin-top:4px; padding:6px 8px; background:var(--gray-50); border-radius:4px;" id="likelihood-detail">${LIKELIHOOD_DETAILS[likelihood]}</div>
</div>
<div class="form-group">
<label class="form-label">Impatto: <span id="impact-label">${LIKELIHOOD_LABELS[impact]}</span></label>
<label class="form-label">Impatto <span style="color:var(--gray-400); font-size:0.75rem; font-weight:400;">(danno economico / utenti)</span></label>
<div class="range-value-display" id="impact-value">${impact}</div>
<input type="range" class="form-range" id="risk-impact" min="1" max="5" value="${impact}" oninput="updateRiskScore()">
<div class="form-range-labels">
<span>1 - Molto Basso</span>
<span>5 - Molto Alto</span>
</div>
<div style="font-size:0.75rem; color:var(--gray-500); margin-top:4px; padding:6px 8px; background:var(--gray-50); border-radius:4px;" id="impact-detail">${IMPACT_DETAILS[impact]}</div>
</div>
</div>
@@ -934,8 +946,10 @@
document.getElementById('likelihood-value').textContent = l;
document.getElementById('impact-value').textContent = i;
document.getElementById('likelihood-label').textContent = LIKELIHOOD_LABELS[l];
document.getElementById('impact-label').textContent = LIKELIHOOD_LABELS[i];
const lDetail = document.getElementById('likelihood-detail');
const iDetail = document.getElementById('impact-detail');
if (lDetail) lDetail.textContent = LIKELIHOOD_DETAILS[l];
if (iDetail) iDetail.textContent = IMPACT_DETAILS[i];
document.getElementById('score-preview-value').textContent = score;
document.getElementById('score-preview-value').style.color = getScoreColorForRisk(score);
}