Documento ai sensi della Direttiva (UE) 2022/2555 e del D.Lgs. 138/2024 — Requisito GV.OC-04 (NIST CSF 2.0)
Settore: {$sector} | Tipo soggetto: {$entity} | Data emissione: {$date} | Generato da {$appName} v{$appVer}
1. Premessa e metodologia
Il presente documento costituisce l'elenco formale dei sistemi informativi e di rete classificati come rilevanti ai fini della conformita alla Direttiva NIS2. La classificazione adotta una metodologia di scoring 0-100 su sei criteri (Criticita Operativa, Impatto Interruzione, Dati Trattati, Dipendenze, Esposizione, Obblighi Normativi). Soglia di rilevanza: punteggio ≥ 40. I sistemi con punteggio ≥ 80 sono considerati critici e richiedono misure di sicurezza massime e monitoraggio continuo.
{$total}
Sistemi rilevanti
{$counts['critico']}
Critici (≥80)
{$counts['alto']}
Alti (60-79)
{$counts['medio']}
Medi (40-59)
2. Elenco sistemi rilevanti
#
Sistema
Tipo/Categoria
IP
Responsabile
Punteggio
Classe
{$rowsHtml}
Fonti normative certe:
• Direttiva (UE) 2022/2555 (NIS2) — Parlamento europeo e Consiglio UE
• D.Lgs. 4 settembre 2024, n. 138 — recepimento NIS2 (artt. 23, 24)
• NIST Cybersecurity Framework 2.0 — controllo GV.OC-04 (elenco sistemi rilevanti)
Il presente report è generato automaticamente dalla piattaforma {$appName} ed è
destinato agli organi di gestione ai sensi dell'Art. 20 della Direttiva NIS2 (UE 2022/2555) e
del D.Lgs. 138/2024. I dati si riferiscono alla data di generazione e potrebbero non riflettere
variazioni successive. Si raccomanda una revisione periodica da parte del responsabile compliance.
HTML;
return $html;
}
// ═══════════════════════════════════════════════════════════════════════════
// EXPORT CSV
// ═══════════════════════════════════════════════════════════════════════════
/**
* Esporta tutti i rischi dell'organizzazione in formato CSV.
*
* @param int $orgId ID dell'organizzazione
* @return string Contenuto CSV con BOM UTF-8
*/
public function exportRisksCSV(int $orgId): string
{
$risks = Database::fetchAll(
'SELECT r.risk_code, r.title, r.description, r.category,
r.threat_source, r.vulnerability,
r.likelihood, r.impact, r.inherent_risk_score,
r.residual_likelihood, r.residual_impact, r.residual_risk_score,
r.treatment, r.status, r.nis2_article, r.review_date,
r.created_at,
u.full_name AS responsabile
FROM risks r
LEFT JOIN users u ON u.id = r.owner_user_id
WHERE r.organization_id = ?
ORDER BY r.inherent_risk_score DESC',
[$orgId]
);
$headers = [
'Codice',
'Titolo',
'Descrizione',
'Categoria',
'Fonte minaccia',
'Vulnerabilita',
'Probabilita',
'Impatto',
'Score rischio inerente',
'Probabilita residua',
'Impatto residuo',
'Score rischio residuo',
'Trattamento',
'Stato',
'Articolo NIS2',
'Data revisione',
'Data creazione',
'Responsabile',
];
$rows = [];
foreach ($risks as $r) {
$rows[] = [
$r['risk_code'] ?? '',
$r['title'] ?? '',
$r['description'] ?? '',
$r['category'] ?? '',
$r['threat_source'] ?? '',
$r['vulnerability'] ?? '',
$r['likelihood'] ?? '',
$r['impact'] ?? '',
$r['inherent_risk_score'] ?? '',
$r['residual_likelihood'] ?? '',
$r['residual_impact'] ?? '',
$r['residual_risk_score'] ?? '',
$r['treatment'] ?? '',
$r['status'] ?? '',
$r['nis2_article'] ?? '',
$this->formatDateIT($r['review_date'] ?? ''),
$this->formatDateTimeIT($r['created_at'] ?? ''),
$r['responsabile'] ?? '',
];
}
return $this->buildCSV($headers, $rows);
}
/**
* Esporta tutti gli incidenti dell'organizzazione in formato CSV.
*
* @param int $orgId ID dell'organizzazione
* @return string Contenuto CSV con BOM UTF-8
*/
public function exportIncidentsCSV(int $orgId): string
{
$incidents = Database::fetchAll(
'SELECT i.incident_code, i.title, i.description, i.classification,
i.severity, i.is_significant, i.status,
i.detected_at, i.closed_at,
i.affected_services, i.affected_users_count,
i.cross_border_impact, i.malicious_action,
i.root_cause, i.remediation_actions, i.lessons_learned,
i.early_warning_due, i.early_warning_sent_at,
i.notification_due, i.notification_sent_at,
i.final_report_due, i.final_report_sent_at,
u1.full_name AS segnalato_da,
u2.full_name AS assegnato_a
FROM incidents i
LEFT JOIN users u1 ON u1.id = i.reported_by
LEFT JOIN users u2 ON u2.id = i.assigned_to
WHERE i.organization_id = ?
ORDER BY i.detected_at DESC',
[$orgId]
);
$headers = [
'Codice',
'Titolo',
'Descrizione',
'Classificazione',
'Severita',
'Significativo NIS2',
'Stato',
'Data rilevamento',
'Data chiusura',
'Servizi impattati',
'Utenti impattati',
'Impatto transfrontaliero',
'Azione malevola',
'Causa radice',
'Azioni di rimedio',
'Lezioni apprese',
'Early warning scadenza',
'Early warning inviato',
'Notifica CSIRT scadenza',
'Notifica CSIRT inviata',
'Report finale scadenza',
'Report finale inviato',
'Segnalato da',
'Assegnato a',
];
$rows = [];
foreach ($incidents as $i) {
$rows[] = [
$i['incident_code'] ?? '',
$i['title'] ?? '',
$i['description'] ?? '',
$i['classification'] ?? '',
$i['severity'] ?? '',
$i['is_significant'] ? 'Si' : 'No',
$i['status'] ?? '',
$this->formatDateTimeIT($i['detected_at'] ?? ''),
$this->formatDateTimeIT($i['closed_at'] ?? ''),
$i['affected_services'] ?? '',
$i['affected_users_count'] ?? '',
$i['cross_border_impact'] ? 'Si' : 'No',
$i['malicious_action'] ? 'Si' : 'No',
$i['root_cause'] ?? '',
$i['remediation_actions'] ?? '',
$i['lessons_learned'] ?? '',
$this->formatDateTimeIT($i['early_warning_due'] ?? ''),
$this->formatDateTimeIT($i['early_warning_sent_at'] ?? ''),
$this->formatDateTimeIT($i['notification_due'] ?? ''),
$this->formatDateTimeIT($i['notification_sent_at'] ?? ''),
$this->formatDateTimeIT($i['final_report_due'] ?? ''),
$this->formatDateTimeIT($i['final_report_sent_at'] ?? ''),
$i['segnalato_da'] ?? '',
$i['assegnato_a'] ?? '',
];
}
return $this->buildCSV($headers, $rows);
}
/**
* Esporta i controlli di compliance dell'organizzazione in formato CSV.
*
* @param int $orgId ID dell'organizzazione
* @return string Contenuto CSV con BOM UTF-8
*/
public function exportControlsCSV(int $orgId): string
{
$controls = Database::fetchAll(
'SELECT control_code, title, status, implementation_percentage
FROM compliance_controls
WHERE organization_id = ?
ORDER BY control_code',
[$orgId]
);
$headers = [
'Codice controllo',
'Titolo',
'Stato',
'Percentuale implementazione',
];
$rows = [];
foreach ($controls as $c) {
$rows[] = [
$c['control_code'] ?? '',
$c['title'] ?? '',
$c['status'] ?? '',
$c['implementation_percentage'] ?? '0',
];
}
return $this->buildCSV($headers, $rows);
}
/**
* Esporta tutti gli asset dell'organizzazione in formato CSV.
*
* @param int $orgId ID dell'organizzazione
* @return string Contenuto CSV con BOM UTF-8
*/
public function exportAssetsCSV(int $orgId): string
{
$assets = Database::fetchAll(
'SELECT a.name, a.asset_type, a.category, a.description,
a.criticality, a.location, a.ip_address,
a.vendor, a.version, a.serial_number,
a.purchase_date, a.warranty_expiry, a.status,
u.full_name AS responsabile
FROM assets a
LEFT JOIN users u ON u.id = a.owner_user_id
WHERE a.organization_id = ?
ORDER BY a.criticality DESC, a.name',
[$orgId]
);
$headers = [
'Nome',
'Tipo asset',
'Categoria',
'Descrizione',
'Criticita',
'Ubicazione',
'Indirizzo IP',
'Vendor',
'Versione',
'Numero seriale',
'Data acquisto',
'Scadenza garanzia',
'Stato',
'Responsabile',
];
$rows = [];
foreach ($assets as $a) {
$rows[] = [
$a['name'] ?? '',
$a['asset_type'] ?? '',
$a['category'] ?? '',
$a['description'] ?? '',
$a['criticality'] ?? '',
$a['location'] ?? '',
$a['ip_address'] ?? '',
$a['vendor'] ?? '',
$a['version'] ?? '',
$a['serial_number'] ?? '',
$this->formatDateIT($a['purchase_date'] ?? ''),
$this->formatDateIT($a['warranty_expiry'] ?? ''),
$a['status'] ?? '',
$a['responsabile'] ?? '',
];
}
return $this->buildCSV($headers, $rows);
}
// ═══════════════════════════════════════════════════════════════════════════
// METODI PRIVATI - COSTRUZIONE CSV
// ═══════════════════════════════════════════════════════════════════════════
/**
* Costruisce il contenuto CSV con BOM UTF-8 e separatore punto e virgola.
*
* @param string[] $headers Intestazioni colonne
* @param array $rows Righe dati
* @return string Contenuto CSV completo
*/
private function buildCSV(array $headers, array $rows): string
{
// BOM UTF-8 per compatibilita Excel
$bom = "\xEF\xBB\xBF";
$output = fopen('php://temp', 'r+');
fwrite($output, $bom);
// Intestazioni
fputcsv($output, $headers, ';', '"', '\\');
// Righe dati
foreach ($rows as $row) {
fputcsv($output, $row, ';', '"', '\\');
}
rewind($output);
$csv = stream_get_contents($output);
fclose($output);
return $csv;
}
/**
* Formatta una data in formato italiano (gg/mm/aaaa).
*/
private function formatDateIT(string $date): string
{
if (empty($date) || $date === '0000-00-00') {
return '';
}
$ts = strtotime($date);
return $ts ? date('d/m/Y', $ts) : '';
}
/**
* Formatta una data/ora in formato italiano (gg/mm/aaaa HH:ii).
*/
private function formatDateTimeIT(string $datetime): string
{
if (empty($datetime) || str_starts_with($datetime, '0000')) {
return '';
}
$ts = strtotime($datetime);
return $ts ? date('d/m/Y H:i', $ts) : '';
}
// ═══════════════════════════════════════════════════════════════════════════
// METODI PRIVATI - HTML HELPER
// ═══════════════════════════════════════════════════════════════════════════
/**
* Restituisce il colore corrispondente allo score di compliance.
*/
private function getScoreColor(float $score): string
{
return match (true) {
$score >= 80 => '#16a34a', // verde
$score >= 60 => '#ca8a04', // giallo scuro
$score >= 40 => '#ea580c', // arancione
default => '#dc2626', // rosso
};
}
/**
* Restituisce l'etichetta testuale dello score di compliance.
*/
private function getScoreLabel(float $score): string
{
return match (true) {
$score >= 80 => 'Buon livello di compliance',
$score >= 60 => 'Compliance parziale - interventi necessari',
$score >= 40 => 'Compliance insufficiente - azioni urgenti',
$score > 0 => 'Compliance critica - rischio elevato',
default => 'Nessun assessment completato',
};
}
/**
* Genera un badge HTML per una percentuale di raggiungimento.
*/
private function getStatusBadge(float $percentage): string
{
$color = $this->getScoreColor($percentage);
$label = match (true) {
$percentage >= 80 => 'Buono',
$percentage >= 60 => 'Parziale',
$percentage >= 40 => 'Insufficiente',
default => 'Critico',
};
return "{$label}";
}
/**
* Genera un badge HTML basato su un conteggio (0 = buono, >0 = attenzione).
*/
private function getCountBadge(int $count): string
{
if ($count === 0) {
return 'OK';
}
$color = $count >= 3 ? '#dc2626' : '#ea580c';
return "Attenzione";
}
/**
* Somma due valori interi (helper per il template HTML).
*/
private function sumValues(int $a, int $b): int
{
return $a + $b;
}
/**
* Genera la lista di raccomandazioni prioritarie basate sui dati del report.
*
* @param array $data Dati completi del report di compliance
* @return string[] Lista di raccomandazioni
*/
private function buildRecommendations(array $data): array
{
$recs = [];
$score = $data['assessment']['overall_score'] ?? 0;
$controls = $data['controls']['stats'];
$risks = $data['risks'];
$incidents = $data['incidents'];
$policies = $data['policies'];
$training = $data['training'];
$supplyChain = $data['supply_chain'];
// Score complessivo basso
if ($score === null || $score === 0) {
$recs[] = 'Completare il primo assessment NIS2 per ottenere una baseline di compliance.';
} elseif ($score < 40) {
$recs[] = 'Lo score di compliance e critico (' . number_format($score, 1, ',', '.') . '%). Avviare un programma di adeguamento urgente con priorita sui controlli a rischio piu elevato.';
} elseif ($score < 60) {
$recs[] = 'Lo score di compliance e insufficiente (' . number_format($score, 1, ',', '.') . '%). Definire un piano di remediation con timeline e responsabili per ogni area carente.';
}
// Rischi critici/alti
$sevDist = $risks['severity_distribution'] ?? [];
$criticalHigh = ($sevDist['critical'] ?? 0) + ($sevDist['high'] ?? 0);
if ($criticalHigh > 0) {
$recs[] = "Sono presenti {$criticalHigh} rischi con severita critica o alta. Prioritizzare il trattamento immediato con piani di mitigazione specifici.";
}
// Controlli non avviati
$notStarted = $controls['not_started'] ?? 0;
if ($notStarted > 3) {
$recs[] = "{$notStarted} controlli di sicurezza non sono ancora stati avviati. Assegnare responsabili e definire tempistiche per l'implementazione.";
}
// Policy non approvate
if ($policies['total'] > 0 && $policies['approved'] < $policies['total']) {
$pending = $policies['total'] - $policies['approved'];
$recs[] = "{$pending} policy sono in attesa di approvazione. L'Art. 20 NIS2 richiede che gli organi di gestione approvino le misure di sicurezza.";
} elseif ($policies['total'] === 0) {
$recs[] = 'Nessuna policy di sicurezza presente. Creare e approvare le policy fondamentali (sicurezza informazioni, gestione incidenti, continuita operativa).';
}
// Formazione
$trainingRate = $training['completion_rate'] ?? 0;
if ($trainingRate < 50) {
$recs[] = 'Il tasso di completamento della formazione e basso (' . number_format($trainingRate, 1, ',', '.') . '%). L\'Art. 20 NIS2 richiede formazione obbligatoria per organi di gestione e personale.';
}
$overdueTraining = $training['overdue'] ?? 0;
if ($overdueTraining > 0) {
$recs[] = "{$overdueTraining} assegnazioni formative sono scadute. Sollecitare il completamento per garantire la conformita.";
}
// Supply chain
if ($supplyChain['non_compliant'] > 0) {
$recs[] = "{$supplyChain['non_compliant']} fornitori non soddisfano i requisiti di sicurezza (Art. 21.2.d NIS2). Avviare assessment e definire requisiti contrattuali.";
}
// Incidenti significativi
if ($incidents['significant'] > 0 && $incidents['open'] > 0) {
$recs[] = "Sono presenti {$incidents['open']} incidenti attivi di cui {$incidents['significant']} significativi. Verificare il rispetto delle scadenze di notifica CSIRT (Art. 23 NIS2: 24h/72h/30gg).";
}
// Se tutto sembra a posto
if (empty($recs)) {
$recs[] = 'Il livello di compliance e complessivamente buono. Continuare con il monitoraggio continuo e le revisioni periodiche.';
$recs[] = 'Pianificare il prossimo assessment per verificare il mantenimento dello score.';
}
return $recs;
}
/**
* Costruisce una pagina HTML di errore minimale.
*/
private function buildErrorHtml(string $message): string
{
$escapedMessage = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');
$appName = defined('APP_NAME') ? APP_NAME : 'NIS2 Agile';
return <<
Errore Report - {$appName}