'Organizzazione non trovata']; } // --- Ultimo assessment completato --- $lastAssessment = Database::fetchOne( 'SELECT id, title, overall_score, category_scores, status, completed_at, ai_summary, ai_recommendations FROM assessments WHERE organization_id = ? AND status = "completed" ORDER BY completed_at DESC LIMIT 1', [$orgId] ); $categoryScores = []; if ($lastAssessment && $lastAssessment['category_scores']) { $categoryScores = json_decode($lastAssessment['category_scores'], true) ?? []; } // --- Compliance controls --- $controls = Database::fetchAll( 'SELECT control_code, title, status, implementation_percentage FROM compliance_controls WHERE organization_id = ? ORDER BY control_code', [$orgId] ); $controlStats = [ 'total' => count($controls), 'implemented' => 0, 'in_progress' => 0, 'not_started' => 0, 'not_applicable' => 0, 'verified' => 0, ]; $totalImplementation = 0; foreach ($controls as $c) { $status = $c['status'] ?? 'not_started'; if (isset($controlStats[$status])) { $controlStats[$status]++; } else { $controlStats['not_started']++; } $totalImplementation += (int) ($c['implementation_percentage'] ?? 0); } $controlStats['avg_implementation'] = $controlStats['total'] > 0 ? round($totalImplementation / $controlStats['total'], 1) : 0; // --- Rischi --- $risks = Database::fetchAll( 'SELECT id, risk_code, title, category, likelihood, impact, inherent_risk_score, residual_risk_score, treatment, status FROM risks WHERE organization_id = ? ORDER BY inherent_risk_score DESC', [$orgId] ); $openRisks = array_filter($risks, fn(array $r): bool => ($r['status'] ?? '') !== 'closed'); $riskByCategory = []; foreach ($openRisks as $r) { $cat = $r['category'] ?? 'other'; if (!isset($riskByCategory[$cat])) { $riskByCategory[$cat] = 0; } $riskByCategory[$cat]++; } $severityDistribution = [ 'critical' => count(array_filter($openRisks, fn(array $r): bool => ($r['inherent_risk_score'] ?? 0) >= 20)), 'high' => count(array_filter($openRisks, fn(array $r): bool => ($r['inherent_risk_score'] ?? 0) >= 12 && ($r['inherent_risk_score'] ?? 0) < 20)), 'medium' => count(array_filter($openRisks, fn(array $r): bool => ($r['inherent_risk_score'] ?? 0) >= 6 && ($r['inherent_risk_score'] ?? 0) < 12)), 'low' => count(array_filter($openRisks, fn(array $r): bool => ($r['inherent_risk_score'] ?? 0) < 6)), ]; $riskSummary = [ 'total_risks' => count($risks), 'open_risks' => count($openRisks), 'by_category' => $riskByCategory, 'severity_distribution' => $severityDistribution, ]; // --- Incidenti aperti --- $openIncidents = Database::count( 'incidents', 'organization_id = ? AND status NOT IN ("closed", "post_mortem")', [$orgId] ); $totalIncidents = Database::count('incidents', 'organization_id = ?', [$orgId]); $significantIncidents = Database::count( 'incidents', 'organization_id = ? AND is_significant = 1', [$orgId] ); $incidentSummary = [ 'total' => $totalIncidents, 'open' => $openIncidents, 'significant' => $significantIncidents, ]; // --- Policy --- $policyStats = Database::fetchAll( 'SELECT status, COUNT(*) as count FROM policies WHERE organization_id = ? GROUP BY status', [$orgId] ); $policyMap = []; $totalPolicies = 0; foreach ($policyStats as $ps) { $policyMap[$ps['status']] = (int) $ps['count']; $totalPolicies += (int) $ps['count']; } $policySummary = [ 'total' => $totalPolicies, 'approved' => $policyMap['approved'] ?? 0, 'draft' => $policyMap['draft'] ?? 0, 'review' => $policyMap['review'] ?? 0, 'archived' => $policyMap['archived'] ?? 0, ]; // --- Formazione --- $trainingStats = Database::fetchOne( 'SELECT COUNT(*) as total, SUM(CASE WHEN status = "completed" THEN 1 ELSE 0 END) as completed, SUM(CASE WHEN status = "in_progress" THEN 1 ELSE 0 END) as in_progress, SUM(CASE WHEN status = "assigned" THEN 1 ELSE 0 END) as assigned, SUM(CASE WHEN status = "overdue" THEN 1 ELSE 0 END) as overdue FROM training_assignments WHERE organization_id = ?', [$orgId] ); $trainingTotal = (int) ($trainingStats['total'] ?? 0); $trainingCompleted = (int) ($trainingStats['completed'] ?? 0); $trainingSummary = [ 'total_assignments' => $trainingTotal, 'completed' => $trainingCompleted, 'in_progress' => (int) ($trainingStats['in_progress'] ?? 0), 'assigned' => (int) ($trainingStats['assigned'] ?? 0), 'overdue' => (int) ($trainingStats['overdue'] ?? 0), 'completion_rate' => $trainingTotal > 0 ? round($trainingCompleted / $trainingTotal * 100, 1) : 0, ]; // --- Supply chain --- $supplierStats = Database::fetchOne( 'SELECT COUNT(*) as total, SUM(CASE WHEN criticality IN ("high", "critical") THEN 1 ELSE 0 END) as critical_high, SUM(CASE WHEN security_requirements_met = 1 THEN 1 ELSE 0 END) as compliant, SUM(CASE WHEN security_requirements_met = 0 THEN 1 ELSE 0 END) as non_compliant, AVG(risk_score) as avg_risk_score FROM suppliers WHERE organization_id = ? AND status = "active"', [$orgId] ); $supplyChainSummary = [ 'total_suppliers' => (int) ($supplierStats['total'] ?? 0), 'critical_high' => (int) ($supplierStats['critical_high'] ?? 0), 'compliant' => (int) ($supplierStats['compliant'] ?? 0), 'non_compliant' => (int) ($supplierStats['non_compliant'] ?? 0), 'avg_risk_score' => round((float) ($supplierStats['avg_risk_score'] ?? 0), 1), ]; // --- Composizione report --- return [ 'generated_at' => date('Y-m-d H:i:s'), 'organization' => $organization, 'assessment' => [ 'last_assessment' => $lastAssessment, 'overall_score' => $lastAssessment ? (float) $lastAssessment['overall_score'] : null, 'category_scores' => $categoryScores, ], 'controls' => [ 'items' => $controls, 'stats' => $controlStats, ], 'risks' => $riskSummary, 'incidents' => $incidentSummary, 'policies' => $policySummary, 'training' => $trainingSummary, 'supply_chain' => $supplyChainSummary, ]; } // ═══════════════════════════════════════════════════════════════════════════ // REPORT ESECUTIVO HTML (Art. 20 NIS2) // ═══════════════════════════════════════════════════════════════════════════ /** * Genera un report esecutivo HTML stampabile per il CdA. * * Il documento e completamente autonomo (inline CSS) e pensato * per essere stampato dal browser o salvato come PDF. * * Riferimento: Art. 20 Direttiva NIS2 - Governance e responsabilita * degli organi di gestione. * * @param int $orgId ID dell'organizzazione * @return string Documento HTML completo */ public function generateExecutiveReport(int $orgId): string { $data = $this->generateComplianceReport($orgId); if (isset($data['error'])) { return $this->buildErrorHtml($data['error']); } $org = $data['organization']; $assessment = $data['assessment']; $controls = $data['controls']['stats']; $riskSummary = $data['risks']; $incidents = $data['incidents']; $policies = $data['policies']; $training = $data['training']; $supplyChain = $data['supply_chain']; $orgName = htmlspecialchars($org['name'] ?? '', ENT_QUOTES, 'UTF-8'); $orgSector = htmlspecialchars($org['sector'] ?? '', ENT_QUOTES, 'UTF-8'); $orgEntityType = htmlspecialchars($org['entity_type'] ?? '', ENT_QUOTES, 'UTF-8'); $score = $assessment['overall_score'] ?? 0; $scoreFormatted = number_format((float) $score, 1, ',', '.'); $reportDate = date('d/m/Y'); $reportTime = date('H:i'); $appName = defined('APP_NAME') ? APP_NAME : 'NIS2 Agile'; $appVersion = defined('APP_VERSION') ? APP_VERSION : '1.0.0'; $appUrl = defined('APP_URL') ? APP_URL : ''; $scoreColor = $this->getScoreColor((float) $score); $scoreLabel = $this->getScoreLabel((float) $score); // Categoria scores per la tabella $categoryRows = ''; if (!empty($assessment['category_scores'])) { foreach ($assessment['category_scores'] as $catId => $catData) { $catScore = is_array($catData) ? ($catData['score'] ?? 0) : (float) $catData; $catColor = $this->getScoreColor($catScore); $catLabel = htmlspecialchars(ucfirst(str_replace('_', ' ', $catId)), ENT_QUOTES, 'UTF-8'); $catScoreFmt = number_format($catScore, 1, ',', '.'); $categoryRows .= << {$catLabel} {$catScoreFmt}%
ROW; } } // Risk heatmap summary $sevDist = $riskSummary['severity_distribution'] ?? []; $riskCritical = (int) ($sevDist['critical'] ?? 0); $riskHigh = (int) ($sevDist['high'] ?? 0); $riskMedium = (int) ($sevDist['medium'] ?? 0); $riskLow = (int) ($sevDist['low'] ?? 0); // Risk by category $riskCategoryRows = ''; $riskByCategory = $riskSummary['by_category'] ?? []; arsort($riskByCategory); foreach ($riskByCategory as $cat => $count) { $catLabel = htmlspecialchars(ucfirst(str_replace('_', ' ', $cat)), ENT_QUOTES, 'UTF-8'); $riskCategoryRows .= << {$catLabel} {$count} ROW; } // Raccomandazioni $recommendations = $this->buildRecommendations($data); $recommendationItems = ''; foreach ($recommendations as $rec) { $recText = htmlspecialchars($rec, ENT_QUOTES, 'UTF-8'); $recommendationItems .= "
  • {$recText}
  • \n"; } // Assessment info $assessmentDate = 'N/D'; $assessmentTitle = 'Nessun assessment completato'; if ($assessment['last_assessment']) { $assessmentDate = date('d/m/Y', strtotime($assessment['last_assessment']['completed_at'])); $assessmentTitle = htmlspecialchars($assessment['last_assessment']['title'] ?? '', ENT_QUOTES, 'UTF-8'); } // Training completion $trainingRate = number_format($training['completion_rate'] ?? 0, 1, ',', '.'); $trainingColor = $this->getScoreColor($training['completion_rate'] ?? 0); // Supply chain $scCompliance = $supplyChain['total_suppliers'] > 0 ? round($supplyChain['compliant'] / $supplyChain['total_suppliers'] * 100, 1) : 0; $scComplianceFmt = number_format($scCompliance, 1, ',', '.'); // Costruzione HTML $html = << Report Esecutivo NIS2 - {$orgName}

    Report Esecutivo Compliance NIS2

    {$orgName}

    Settore: {$orgSector} | Tipo entità: {$orgEntityType} Data: {$reportDate} ore {$reportTime}
    Indice di Compliance NIS2
    {$scoreFormatted}%
    {$scoreLabel}
    Ultimo assessment: {$assessmentTitle} ({$assessmentDate})
    Metriche Chiave
    {$controls['total']}
    Controlli totali
    {$controls['avg_implementation']}%
    Implementazione media
    {$riskSummary['open_risks']}
    Rischi aperti
    {$incidents['open']}
    Incidenti attivi
    Indicatore Valore Stato
    Policy approvate {$policies['approved']} / {$policies['total']} {$this->getStatusBadge($policies['total'] > 0 ? $policies['approved'] / $policies['total'] * 100 : 0)}
    Formazione completata {$trainingRate}% {$this->getStatusBadge($training['completion_rate'] ?? 0)}
    Fornitori conformi (supply chain) {$supplyChain['compliant']} / {$supplyChain['total_suppliers']} {$this->getStatusBadge($scCompliance)}
    Incidenti significativi (NIS2 Art. 23) {$incidents['significant']} {$this->getCountBadge($incidents['significant'])}
    Fornitori critici non conformi {$supplyChain['non_compliant']} {$this->getCountBadge($supplyChain['non_compliant'])}
    Dettaglio per Categoria (Art. 21.2 NIS2)
    {$categoryRows}
    Categoria Score Progresso
    Riepilogo Rischi Cyber
    {$riskCritical}
    Critici
    {$riskHigh}
    Alti
    {$riskMedium}
    Medi
    {$riskLow}
    Bassi
    {$riskCategoryRows}
    Categoria rischio N. rischi
    Metrica Valore
    Rischi totali registrati {$riskSummary['total_risks']}
    Rischi aperti {$riskSummary['open_risks']}
    Critici + Alti {$this->sumValues($riskCritical, $riskHigh)}
    Score medio fornitori {$supplyChain['avg_risk_score']}
    Raccomandazioni Prioritarie
      {$recommendationItems}

    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}

    Errore generazione report

    {$escapedMessage}

    HTML; } }