requireOrgAccess(); $assessments = Database::fetchAll( 'SELECT a.*, u.full_name as completed_by_name FROM assessments a LEFT JOIN users u ON u.id = a.completed_by WHERE a.organization_id = ? ORDER BY a.created_at DESC', [$this->getCurrentOrgId()] ); $this->jsonSuccess($assessments); } /** * POST /api/assessments/create */ public function create(): void { $this->requireOrgRole(['org_admin', 'compliance_manager']); $title = $this->getParam('title', 'Assessment NIS2 - ' . date('d/m/Y')); $type = $this->getParam('assessment_type', 'initial'); $assessmentId = Database::insert('assessments', [ 'organization_id' => $this->getCurrentOrgId(), 'title' => $title, 'assessment_type' => $type, 'status' => 'draft', ]); // Pre-popola le risposte con le domande dal questionario $questionnaire = $this->loadQuestionnaire(); foreach ($questionnaire['categories'] as $category) { foreach ($category['questions'] as $question) { Database::insert('assessment_responses', [ 'assessment_id' => $assessmentId, 'question_code' => $question['code'], 'nis2_article' => $question['nis2_article'], 'iso27001_control' => $question['iso27001_control'], 'category' => $category['id'], 'question_text' => $question['text_it'], ]); } } $this->logAudit('assessment_created', 'assessment', $assessmentId); $this->jsonSuccess([ 'id' => $assessmentId, 'title' => $title, 'status' => 'draft', ], 'Assessment creato', 201); } /** * GET /api/assessments/{id} */ public function get(int $id): void { $this->requireOrgAccess(); $assessment = $this->getAssessment($id); // Conta risposte per stato $stats = Database::fetchAll( 'SELECT response_value, COUNT(*) as count FROM assessment_responses WHERE assessment_id = ? AND response_value IS NOT NULL GROUP BY response_value', [$id] ); $totalQuestions = Database::count('assessment_responses', 'assessment_id = ?', [$id]); $answeredQuestions = Database::count( 'assessment_responses', 'assessment_id = ? AND response_value IS NOT NULL', [$id] ); $assessment['stats'] = $stats; $assessment['total_questions'] = $totalQuestions; $assessment['answered_questions'] = $answeredQuestions; $assessment['progress_percentage'] = $totalQuestions > 0 ? round($answeredQuestions / $totalQuestions * 100) : 0; $this->jsonSuccess($assessment); } /** * GET /api/assessments/{id}/questions * Restituisce domande con risposte correnti, organizzate per categoria */ public function getQuestions(int $id): void { $this->requireOrgAccess(); $this->getAssessment($id); $responses = Database::fetchAll( 'SELECT * FROM assessment_responses WHERE assessment_id = ? ORDER BY question_code', [$id] ); // Carica questionario per i metadati $questionnaire = $this->loadQuestionnaire(); $questionMeta = []; foreach ($questionnaire['categories'] as $cat) { foreach ($cat['questions'] as $q) { $questionMeta[$q['code']] = [ 'text_en' => $q['text_en'], 'guidance_it' => $q['guidance_it'], 'evidence_examples' => $q['evidence_examples'], 'weight' => $q['weight'], ]; } } // Organizza per categoria $byCategory = []; foreach ($responses as $r) { $cat = $r['category']; if (!isset($byCategory[$cat])) { // Trova titolo categoria $catTitle = $cat; foreach ($questionnaire['categories'] as $c) { if ($c['id'] === $cat) { $catTitle = $c['title_it']; break; } } $byCategory[$cat] = [ 'category_id' => $cat, 'category_title' => $catTitle, 'questions' => [], ]; } $meta = $questionMeta[$r['question_code']] ?? []; $r['text_en'] = $meta['text_en'] ?? null; $r['guidance_it'] = $meta['guidance_it'] ?? null; $r['evidence_examples'] = $meta['evidence_examples'] ?? []; $r['weight'] = $meta['weight'] ?? 1; $byCategory[$cat]['questions'][] = $r; } $this->jsonSuccess(array_values($byCategory)); } /** * POST /api/assessments/{id}/respond * Salva una o più risposte */ public function saveResponse(int $id): void { $this->requireOrgRole(['org_admin', 'compliance_manager', 'auditor']); $assessment = $this->getAssessment($id); if ($assessment['status'] === 'completed') { $this->jsonError('Assessment già completato', 400, 'ALREADY_COMPLETED'); } // Accetta singola risposta o array di risposte $responses = $this->getParam('responses'); if (!$responses) { // Singola risposta $this->validateRequired(['question_code', 'response_value']); $responses = [[ 'question_code' => $this->getParam('question_code'), 'response_value' => $this->getParam('response_value'), 'maturity_level' => $this->getParam('maturity_level'), 'evidence_description' => $this->getParam('evidence_description'), 'notes' => $this->getParam('notes'), ]]; } $savedCount = 0; foreach ($responses as $resp) { $code = $resp['question_code'] ?? null; $value = $resp['response_value'] ?? null; if (!$code || !$value) continue; Database::update('assessment_responses', [ 'response_value' => $value, 'maturity_level' => $resp['maturity_level'] ?? null, 'evidence_description' => $resp['evidence_description'] ?? null, 'notes' => $resp['notes'] ?? null, 'answered_by' => $this->getCurrentUserId(), 'answered_at' => date('Y-m-d H:i:s'), ], 'assessment_id = ? AND question_code = ?', [$id, $code]); $savedCount++; } // Aggiorna status a in_progress se era draft if ($assessment['status'] === 'draft') { Database::update('assessments', ['status' => 'in_progress'], 'id = ?', [$id]); } $this->jsonSuccess(['saved' => $savedCount], "{$savedCount} risposte salvate"); } /** * POST /api/assessments/{id}/complete */ public function complete(int $id): void { $this->requireOrgRole(['org_admin', 'compliance_manager']); $assessment = $this->getAssessment($id); if ($assessment['status'] === 'completed') { $this->jsonError('Assessment già completato', 400, 'ALREADY_COMPLETED'); } // Calcola score $responses = Database::fetchAll( 'SELECT * FROM assessment_responses WHERE assessment_id = ?', [$id] ); $scores = $this->calculateScores($responses); Database::update('assessments', [ 'status' => 'completed', 'overall_score' => $scores['overall'], 'category_scores' => json_encode($scores['by_category']), 'completed_by' => $this->getCurrentUserId(), 'completed_at' => date('Y-m-d H:i:s'), ], 'id = ?', [$id]); $this->logAudit('assessment_completed', 'assessment', $id, [ 'overall_score' => $scores['overall'] ]); $this->jsonSuccess([ 'overall_score' => $scores['overall'], 'category_scores' => $scores['by_category'], ], 'Assessment completato'); } /** * GET /api/assessments/{id}/report */ public function getReport(int $id): void { $this->requireOrgAccess(); $assessment = $this->getAssessment($id); if ($assessment['status'] !== 'completed') { $this->jsonError('L\'assessment deve essere completato prima di generare il report', 400, 'NOT_COMPLETED'); } $responses = Database::fetchAll( 'SELECT ar.*, u.full_name as answered_by_name FROM assessment_responses ar LEFT JOIN users u ON u.id = ar.answered_by WHERE ar.assessment_id = ? ORDER BY ar.category, ar.question_code', [$id] ); $categoryScores = json_decode($assessment['category_scores'], true) ?? []; $this->jsonSuccess([ 'assessment' => $assessment, 'category_scores' => $categoryScores, 'responses' => $responses, 'ai_summary' => $assessment['ai_summary'], 'ai_recommendations' => json_decode($assessment['ai_recommendations'], true), ]); } /** * POST /api/assessments/{id}/ai-analyze * Richiede analisi AI dell'assessment */ public function aiAnalyze(int $id): void { $this->requireOrgRole(['org_admin', 'compliance_manager']); $assessment = $this->getAssessment($id); if ($assessment['status'] !== 'completed') { $this->jsonError('Completare l\'assessment prima dell\'analisi AI', 400, 'NOT_COMPLETED'); } // Carica organizzazione $org = Database::fetchOne('SELECT * FROM organizations WHERE id = ?', [$this->getCurrentOrgId()]); // Carica risposte $responses = Database::fetchAll( 'SELECT * FROM assessment_responses WHERE assessment_id = ?', [$id] ); try { $aiService = new AIService(); $analysis = $aiService->analyzeGapAssessment($org, $responses, (float) $assessment['overall_score']); // Salva risultati AI Database::update('assessments', [ 'ai_summary' => $analysis['executive_summary'] ?? json_encode($analysis), 'ai_recommendations' => json_encode($analysis), ], 'id = ?', [$id]); // Log interazione AI $aiService->logInteraction( $this->getCurrentOrgId(), $this->getCurrentUserId(), 'gap_analysis', "Gap analysis assessment #{$id}", substr(json_encode($analysis), 0, 500) ); $this->logAudit('ai_analysis_requested', 'assessment', $id); $this->jsonSuccess($analysis, 'Analisi AI completata'); } catch (Throwable $e) { error_log('[AI_ERROR] ' . $e->getMessage()); $this->jsonError('Errore durante l\'analisi AI: ' . $e->getMessage(), 500, 'AI_ERROR'); } } // ═══════════════════════════════════════════════════════════════════════ // METODI PRIVATI // ═══════════════════════════════════════════════════════════════════════ /** * Carica assessment verificando ownership */ private function getAssessment(int $id): array { $assessment = Database::fetchOne( 'SELECT * FROM assessments WHERE id = ? AND organization_id = ?', [$id, $this->getCurrentOrgId()] ); if (!$assessment) { $this->jsonError('Assessment non trovato', 404, 'ASSESSMENT_NOT_FOUND'); } return $assessment; } /** * Carica questionario da file JSON */ private function loadQuestionnaire(): array { $file = DATA_PATH . '/nis2_questionnaire.json'; if (!file_exists($file)) { $this->jsonError('Questionario NIS2 non disponibile', 500, 'QUESTIONNAIRE_MISSING'); } $data = json_decode(file_get_contents($file), true); if (!$data) { $this->jsonError('Errore caricamento questionario', 500, 'QUESTIONNAIRE_ERROR'); } return $data; } /** * Calcola score dell'assessment */ private function calculateScores(array $responses): array { $byCategory = []; $totalWeightedScore = 0; $totalWeight = 0; // Carica pesi dalle domande $questionnaire = $this->loadQuestionnaire(); $weights = []; foreach ($questionnaire['categories'] as $cat) { foreach ($cat['questions'] as $q) { $weights[$q['code']] = $q['weight'] ?? 1; } } foreach ($responses as $r) { $cat = $r['category'] ?? 'other'; $value = $r['response_value']; $weight = $weights[$r['question_code']] ?? 1; if (!isset($byCategory[$cat])) { $byCategory[$cat] = ['score' => 0, 'max' => 0, 'count' => 0]; } if ($value === 'not_applicable') continue; $score = match ($value) { 'implemented' => 100, 'partial' => 50, default => 0, }; $byCategory[$cat]['score'] += $score * $weight; $byCategory[$cat]['max'] += 100 * $weight; $byCategory[$cat]['count']++; $totalWeightedScore += $score * $weight; $totalWeight += 100 * $weight; } // Calcola percentuali per categoria $categoryScores = []; foreach ($byCategory as $cat => $data) { $categoryScores[$cat] = [ 'score' => $data['max'] > 0 ? round($data['score'] / $data['max'] * 100, 1) : 0, 'count' => $data['count'], ]; } $overallScore = $totalWeight > 0 ? round($totalWeightedScore / $totalWeight * 100, 1) : 0; return [ 'overall' => $overallScore, 'by_category' => $categoryScores, ]; } }