apiKey = ANTHROPIC_API_KEY; $this->model = ANTHROPIC_MODEL; $this->maxTokens = ANTHROPIC_MAX_TOKENS; if (empty($this->apiKey) || $this->apiKey === 'sk-ant-xxxxx') { throw new RuntimeException('ANTHROPIC_API_KEY non configurata'); } } /** * Analizza risultati gap analysis e genera raccomandazioni */ public function analyzeGapAssessment(array $organization, array $responses, float $overallScore): array { $responseSummary = $this->summarizeResponses($responses); // Anonimizzazione: non inviare nome org né fatturato esatto ad API esterna $employeeRange = $this->employeeRange((int)($organization['employee_count'] ?? 0)); $prompt = <<callAPI($prompt); return $this->parseJsonResponse($response); } /** * Suggerisce rischi basati su settore e asset */ public function suggestRisks(array $organization, array $assets = []): array { $assetList = empty($assets) ? 'Non disponibile' : json_encode($assets, JSON_UNESCAPED_UNICODE); $prompt = <<callAPI($prompt); return $this->parseJsonResponse($response); } /** * Genera bozza di policy */ public function generatePolicy(string $category, array $organization, ?array $assessmentContext = null): array { $context = $assessmentContext ? json_encode($assessmentContext, JSON_UNESCAPED_UNICODE) : 'Non disponibile'; $prompt = <<callAPI($prompt); return $this->parseJsonResponse($response); } /** * Classifica un incidente e suggerisce severity */ public function classifyIncident(string $title, string $description, array $organization): array { $prompt = <<callAPI($prompt); return $this->parseJsonResponse($response); } /** * Chiama Anthropic API */ private function callAPI(string $prompt, ?string $systemPrompt = null): string { $system = $systemPrompt ?? 'Sei un esperto consulente di cybersecurity e compliance NIS2. Rispondi sempre in italiano in modo professionale e accurato.'; $body = [ 'model' => $this->model, 'max_tokens' => $this->maxTokens, 'system' => $system, 'messages' => [ ['role' => 'user', 'content' => $prompt], ], ]; $ch = curl_init($this->baseUrl); curl_setopt_array($ch, [ CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'x-api-key: ' . $this->apiKey, 'anthropic-version: 2023-06-01', ], CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($body), CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 120, ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $curlError = curl_error($ch); curl_close($ch); if ($curlError) { throw new RuntimeException('Errore connessione AI: ' . $curlError); } if ($httpCode !== 200) { $errorData = json_decode($response, true); $errorMessage = $errorData['error']['message'] ?? 'Errore API sconosciuto'; throw new RuntimeException("Errore API AI ({$httpCode}): {$errorMessage}"); } $data = json_decode($response, true); if (!isset($data['content'][0]['text'])) { throw new RuntimeException('Risposta AI non valida'); } return $data['content'][0]['text']; } /** * Converte numero dipendenti in range anonimizzato */ private function employeeRange(int $count): string { if ($count <= 0) return 'Non specificato'; if ($count <= 10) return 'Micro impresa (1-10 dipendenti)'; if ($count <= 50) return 'Piccola impresa (11-50 dipendenti)'; if ($count <= 250) return 'Media impresa (51-250 dipendenti)'; if ($count <= 1000) return 'Grande impresa (251-1000 dipendenti)'; return 'Grande organizzazione (>1000 dipendenti)'; } /** * Riassume le risposte dell'assessment per il prompt AI */ private function summarizeResponses(array $responses): string { $byCategory = []; foreach ($responses as $r) { $cat = $r['category'] ?? 'other'; if (!isset($byCategory[$cat])) { $byCategory[$cat] = ['implemented' => 0, 'partial' => 0, 'not_implemented' => 0, 'na' => 0, 'total' => 0]; } $byCategory[$cat]['total']++; match ($r['response_value']) { 'implemented' => $byCategory[$cat]['implemented']++, 'partial' => $byCategory[$cat]['partial']++, 'not_implemented' => $byCategory[$cat]['not_implemented']++, 'not_applicable' => $byCategory[$cat]['na']++, default => null, }; } $summary = ''; foreach ($byCategory as $cat => $counts) { $pct = $counts['total'] > 0 ? round(($counts['implemented'] * 100 + $counts['partial'] * 50) / (($counts['total'] - $counts['na']) * 100) * 100) : 0; $summary .= "- {$cat}: {$pct}% (implementati: {$counts['implemented']}, parziali: {$counts['partial']}, non implementati: {$counts['not_implemented']})\n"; } return $summary; } /** * Parsing robusto della risposta JSON dall'AI */ private function parseJsonResponse(string $response): array { // Rimuovi eventuale markdown code blocks $response = preg_replace('/^```(?:json)?\s*/m', '', $response); $response = preg_replace('/\s*```\s*$/m', '', $response); $response = trim($response); $data = json_decode($response, true); if (json_last_error() !== JSON_ERROR_NONE) { error_log('[AI] JSON parse error: ' . json_last_error_msg() . ' | Response: ' . substr($response, 0, 500)); return ['error' => 'Impossibile analizzare la risposta AI', 'raw' => substr($response, 0, 1000)]; } return $data; } /** * Analisi cross-organizzazione per consulenti (L4) * Riceve dati già aggregati e anonimizzati dal controller. * NON deve mai ricevere nomi org, P.IVA o dati identificativi. */ public function crossOrgAnalysis(int $orgCount, array $aggregated, string $question): array { $context = $this->buildCrossOrgContext($aggregated, $orgCount); $system = <<callAPI($prompt, $system); return $this->parseJsonResponse($response); } /** * Costruisce il contesto aggregato per il prompt cross-org */ private function buildCrossOrgContext(array $d, int $orgCount): string { $lines = []; // Distribuzione settoriale if (!empty($d['by_sector'])) { $lines[] = '### Distribuzione Settoriale'; foreach ($d['by_sector'] as $sector => $count) { $pct = round($count / $orgCount * 100); $lines[] = "- {$sector}: {$count} org ({$pct}%)"; } } // Classificazione NIS2 if (!empty($d['by_entity_type'])) { $lines[] = "\n### Classificazione NIS2"; foreach ($d['by_entity_type'] as $type => $count) { $lines[] = "- {$type}: {$count} org"; } } // Compliance score if (isset($d['avg_compliance_score'])) { $lines[] = "\n### Compliance Score Medio: {$d['avg_compliance_score']}%"; if (isset($d['score_distribution'])) { foreach ($d['score_distribution'] as $range => $count) { $lines[] = "- {$range}: {$count} org"; } } } // Assessment if (isset($d['assessments'])) { $a = $d['assessments']; $lines[] = "\n### Gap Assessment"; $lines[] = "- Org con assessment completato: {$a['with_completed']} / {$orgCount}"; if (!empty($a['avg_by_category'])) { $lines[] = "- Score medio per categoria:"; foreach ($a['avg_by_category'] as $cat => $score) { $lines[] = " - {$cat}: {$score}%"; } } } // Rischi if (isset($d['risks'])) { $r = $d['risks']; $lines[] = "\n### Rischi (aggregato)"; $lines[] = "- Totale rischi aperti: {$r['total_open']}"; $lines[] = "- Media rischi per org: {$r['avg_per_org']}"; if (!empty($r['by_severity'])) { foreach ($r['by_severity'] as $sev => $count) { $lines[] = "- Gravità {$sev}: {$count} rischi"; } } $lines[] = "- % con piano trattamento: {$r['pct_with_treatment']}%"; } // Policy if (isset($d['policies'])) { $p = $d['policies']; $lines[] = "\n### Policy"; $lines[] = "- Media policy approvate per org: {$p['avg_approved']}"; $lines[] = "- Org senza policy approvate: {$p['without_approved']}"; } // Formazione if (isset($d['training'])) { $t = $d['training']; $lines[] = "\n### Formazione"; $lines[] = "- Tasso completamento medio: {$t['avg_completion_rate']}%"; $lines[] = "- Org con completamento < 50%: {$t['below_50pct']}"; } // Controlli if (isset($d['controls'])) { $c = $d['controls']; $lines[] = "\n### Controlli ISO 27001 / NIS2"; $lines[] = "- % media implementazione: {$c['avg_implementation']}%"; if (!empty($c['weakest_categories'])) { $lines[] = "- Categorie più deboli: " . implode(', ', $c['weakest_categories']); } } // Incidenti if (isset($d['incidents'])) { $i = $d['incidents']; $lines[] = "\n### Incidenti (ultimi 12 mesi)"; $lines[] = "- Totale incidenti: {$i['total']}"; $lines[] = "- Org con almeno 1 incidente: {$i['orgs_with_incidents']}"; if (!empty($i['by_severity'])) { foreach ($i['by_severity'] as $sev => $count) { $lines[] = "- Gravità {$sev}: {$count}"; } } } return implode("\n", $lines); } /** * Registra interazione AI nel database */ public function logInteraction(int $orgId, int $userId, string $type, string $promptSummary, string $responseSummary, int $tokensUsed = 0): void { Database::insert('ai_interactions', [ 'organization_id' => $orgId, 'user_id' => $userId, 'interaction_type' => $type, 'prompt_summary' => substr($promptSummary, 0, 500), 'response_summary' => $responseSummary, 'tokens_used' => $tokensUsed, 'model_used' => $this->model, ]); } }