requireOrgAccess(); $orgId = $this->getCurrentOrgId(); // Ultimo assessment $lastAssessment = Database::fetchOne( 'SELECT id, title, overall_score, status, completed_at FROM assessments WHERE organization_id = ? AND status = "completed" ORDER BY completed_at DESC LIMIT 1', [$orgId] ); // Compliance controls status $controlStats = Database::fetchAll( 'SELECT status, COUNT(*) as count FROM compliance_controls WHERE organization_id = ? GROUP BY status', [$orgId] ); // Rischi aperti $riskCounts = Database::fetchOne( 'SELECT SUM(CASE WHEN inherent_risk_score >= 20 THEN 1 ELSE 0 END) as critical_high, SUM(CASE WHEN inherent_risk_score BETWEEN 10 AND 19 THEN 1 ELSE 0 END) as medium, SUM(CASE WHEN inherent_risk_score < 10 THEN 1 ELSE 0 END) as low, COUNT(*) as total FROM risks WHERE organization_id = ? AND status != "closed"', [$orgId] ); // Incidenti attivi $activeIncidents = Database::count( 'incidents', 'organization_id = ? AND status NOT IN ("closed", "post_mortem")', [$orgId] ); // Policy per stato $policyStats = Database::fetchAll( 'SELECT status, COUNT(*) as count FROM policies WHERE organization_id = ? GROUP BY status', [$orgId] ); // Fornitori critici $criticalSuppliers = Database::count( 'suppliers', 'organization_id = ? AND criticality IN ("high", "critical") AND security_requirements_met = 0', [$orgId] ); // Training completamento $trainingStats = Database::fetchOne( 'SELECT COUNT(*) as total, SUM(CASE WHEN status = "completed" THEN 1 ELSE 0 END) as completed, SUM(CASE WHEN status = "overdue" THEN 1 ELSE 0 END) as overdue FROM training_assignments WHERE organization_id = ?', [$orgId] ); // Conteggi asset $assetCount = Database::count('assets', 'organization_id = ? AND status = "active"', [$orgId]); // Organizzazione info $org = Database::fetchOne('SELECT name, sector, entity_type, subscription_plan FROM organizations WHERE id = ?', [$orgId]); $this->jsonSuccess([ 'organization' => $org, 'last_assessment' => $lastAssessment, 'compliance_score' => $lastAssessment ? (float) $lastAssessment['overall_score'] : null, 'controls' => $controlStats, 'risks' => $riskCounts, 'active_incidents' => $activeIncidents, 'policies' => $policyStats, 'critical_suppliers' => $criticalSuppliers, 'training' => $trainingStats, 'asset_count' => $assetCount, ]); } /** * GET /api/dashboard/compliance-score */ public function complianceScore(): void { $this->requireOrgAccess(); $orgId = $this->getCurrentOrgId(); // Score dall'ultimo assessment $assessments = Database::fetchAll( 'SELECT id, title, overall_score, category_scores, completed_at FROM assessments WHERE organization_id = ? AND status = "completed" ORDER BY completed_at DESC LIMIT 5', [$orgId] ); // Score dei controlli $controls = Database::fetchAll( 'SELECT control_code, title, status, implementation_percentage FROM compliance_controls WHERE organization_id = ? ORDER BY control_code', [$orgId] ); $totalControls = count($controls); $implementedControls = 0; $avgImplementation = 0; foreach ($controls as $c) { if ($c['status'] === 'implemented' || $c['status'] === 'verified') { $implementedControls++; } $avgImplementation += (int) $c['implementation_percentage']; } $avgImplementation = $totalControls > 0 ? round($avgImplementation / $totalControls) : 0; $this->jsonSuccess([ 'assessments' => $assessments, 'controls' => $controls, 'total_controls' => $totalControls, 'implemented_controls' => $implementedControls, 'avg_implementation' => $avgImplementation, ]); } /** * GET /api/dashboard/upcoming-deadlines */ public function deadlines(): void { $this->requireOrgAccess(); $orgId = $this->getCurrentOrgId(); $deadlines = []; // Incidenti con scadenze notifica $incidentDeadlines = Database::fetchAll( 'SELECT id, incident_code, title, severity, early_warning_due, early_warning_sent_at, notification_due, notification_sent_at, final_report_due, final_report_sent_at FROM incidents WHERE organization_id = ? AND is_significant = 1 AND status NOT IN ("closed", "post_mortem") ORDER BY detected_at DESC', [$orgId] ); foreach ($incidentDeadlines as $inc) { if ($inc['early_warning_due'] && !$inc['early_warning_sent_at']) { $deadlines[] = [ 'type' => 'incident_early_warning', 'title' => "Early Warning: {$inc['title']}", 'due_date' => $inc['early_warning_due'], 'severity' => 'critical', 'entity_type' => 'incident', 'entity_id' => $inc['id'], ]; } if ($inc['notification_due'] && !$inc['notification_sent_at']) { $deadlines[] = [ 'type' => 'incident_notification', 'title' => "Notifica CSIRT: {$inc['title']}", 'due_date' => $inc['notification_due'], 'severity' => 'high', 'entity_type' => 'incident', 'entity_id' => $inc['id'], ]; } if ($inc['final_report_due'] && !$inc['final_report_sent_at']) { $deadlines[] = [ 'type' => 'incident_final_report', 'title' => "Report finale: {$inc['title']}", 'due_date' => $inc['final_report_due'], 'severity' => 'medium', 'entity_type' => 'incident', 'entity_id' => $inc['id'], ]; } } // Policy in scadenza revisione $policyDeadlines = Database::fetchAll( 'SELECT id, title, next_review_date FROM policies WHERE organization_id = ? AND next_review_date IS NOT NULL AND next_review_date <= DATE_ADD(NOW(), INTERVAL 30 DAY) AND status NOT IN ("archived") ORDER BY next_review_date', [$orgId] ); foreach ($policyDeadlines as $p) { $deadlines[] = [ 'type' => 'policy_review', 'title' => "Revisione policy: {$p['title']}", 'due_date' => $p['next_review_date'], 'severity' => 'medium', 'entity_type' => 'policy', 'entity_id' => $p['id'], ]; } // Risk treatments in scadenza $treatmentDeadlines = Database::fetchAll( 'SELECT rt.id, rt.action_description, rt.due_date, r.title as risk_title FROM risk_treatments rt JOIN risks r ON r.id = rt.risk_id WHERE r.organization_id = ? AND rt.status IN ("planned", "in_progress") AND rt.due_date IS NOT NULL AND rt.due_date <= DATE_ADD(NOW(), INTERVAL 30 DAY) ORDER BY rt.due_date', [$orgId] ); foreach ($treatmentDeadlines as $t) { $deadlines[] = [ 'type' => 'risk_treatment', 'title' => "Trattamento rischio: {$t['risk_title']}", 'due_date' => $t['due_date'], 'severity' => 'medium', 'entity_type' => 'risk_treatment', 'entity_id' => $t['id'], ]; } // Training in scadenza $trainingDeadlines = Database::fetchAll( 'SELECT ta.id, tc.title, ta.due_date, u.full_name FROM training_assignments ta JOIN training_courses tc ON tc.id = ta.course_id JOIN users u ON u.id = ta.user_id WHERE ta.organization_id = ? AND ta.status IN ("assigned", "in_progress") AND ta.due_date IS NOT NULL AND ta.due_date <= DATE_ADD(NOW(), INTERVAL 30 DAY) ORDER BY ta.due_date', [$orgId] ); foreach ($trainingDeadlines as $t) { $deadlines[] = [ 'type' => 'training_due', 'title' => "Formazione: {$t['title']} - {$t['full_name']}", 'due_date' => $t['due_date'], 'severity' => 'low', 'entity_type' => 'training', 'entity_id' => $t['id'], ]; } // Ordina per data usort($deadlines, fn($a, $b) => strcmp($a['due_date'], $b['due_date'])); $this->jsonSuccess($deadlines); } /** * GET /api/dashboard/recent-activity */ public function recentActivity(): void { $this->requireOrgAccess(); $activities = Database::fetchAll( 'SELECT al.*, u.full_name FROM audit_logs al LEFT JOIN users u ON u.id = al.user_id WHERE al.organization_id = ? ORDER BY al.created_at DESC LIMIT 20', [$this->getCurrentOrgId()] ); $this->jsonSuccess($activities); } /** * GET /api/dashboard/risk-heatmap */ public function riskHeatmap(): void { $this->requireOrgAccess(); $risks = Database::fetchAll( 'SELECT id, title, category, likelihood, impact, inherent_risk_score, status FROM risks WHERE organization_id = ? AND status != "closed" ORDER BY inherent_risk_score DESC', [$this->getCurrentOrgId()] ); // Costruisci matrice 5x5 $matrix = []; for ($l = 1; $l <= 5; $l++) { for ($i = 1; $i <= 5; $i++) { $matrix["{$l}_{$i}"] = []; } } foreach ($risks as $risk) { if ($risk['likelihood'] && $risk['impact']) { $key = "{$risk['likelihood']}_{$risk['impact']}"; $matrix[$key][] = [ 'id' => $risk['id'], 'title' => $risk['title'], 'score' => $risk['inherent_risk_score'], ]; } } $this->jsonSuccess([ 'risks' => $risks, 'matrix' => $matrix, ]); } }