nis2-agile/application/services/AIService.php
DevEnv nis2-agile c52766953d [FEAT] Help online feedback, traduzioni IT/EN, AI system prompt aggiornato
- help.js: nuova sezione 'feedback' con 6 sotto-sezioni (come usare FAB,
  risposta AI, password gate, le mie segnalazioni, worker autonomo, consigli)
- i18n.js: 30 chiavi IT/EN per tutto il sistema feedback
- AIService::callAPI: system prompt esteso con lista completa moduli NIS2 Agile
- AIService::classifyFeedback: system prompt NIS2-aware

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 09:05:12 +01:00

568 lines
20 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* NIS2 Agile - AI Service
*
* Integrazione con Anthropic Claude API per:
* - Analisi gap analysis
* - Suggerimenti rischi
* - Generazione policy
* - Classificazione incidenti
* - Q&A NIS2
*/
class AIService
{
private string $apiKey;
private string $model;
private int $maxTokens;
private string $baseUrl = 'https://api.anthropic.com/v1/messages';
public function __construct()
{
$this->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 = <<<PROMPT
Sei un esperto consulente di cybersecurity specializzato nella Direttiva NIS2 (EU 2022/2555) e nel D.Lgs. 138/2024 italiano.
Analizza i risultati della gap analysis per l'organizzazione seguente e fornisci raccomandazioni dettagliate.
## Organizzazione (dati anonimizzati)
- Settore: {$organization['sector']}
- Tipo entità NIS2: {$organization['entity_type']}
- Dimensione: {$employeeRange}
## Risultati Assessment (Score: {$overallScore}%)
{$responseSummary}
## Istruzioni
Fornisci la tua analisi in formato JSON con questa struttura:
{
"executive_summary": "Riepilogo esecutivo (3-4 frasi)",
"risk_level": "low|medium|high|critical",
"top_priorities": [
{
"area": "Nome area",
"nis2_article": "21.2.x",
"current_status": "Breve descrizione stato attuale",
"recommendation": "Azione raccomandata specifica",
"effort": "low|medium|high",
"timeline": "Tempistica suggerita"
}
],
"strengths": ["Punto di forza 1", "Punto di forza 2"],
"quick_wins": ["Azione rapida 1", "Azione rapida 2"],
"compliance_roadmap": [
{"phase": 1, "title": "Titolo fase", "actions": ["Azione 1"], "duration": "X mesi"}
]
}
Rispondi SOLO con il JSON, senza testo aggiuntivo.
PROMPT;
$response = $this->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 = <<<PROMPT
Sei un esperto di cybersecurity risk assessment. Genera una lista di rischi cyber per questa organizzazione.
## Organizzazione
- Settore: {$organization['sector']}
- Tipo entità NIS2: {$organization['entity_type']}
- Dipendenti: {$organization['employee_count']}
## Asset IT/OT
{$assetList}
Fornisci 10 rischi in formato JSON:
[
{
"title": "Titolo rischio",
"description": "Descrizione dettagliata",
"category": "cyber|operational|compliance|supply_chain|physical|human",
"threat_source": "Fonte della minaccia",
"vulnerability": "Vulnerabilità sfruttata",
"likelihood": 1-5,
"impact": 1-5,
"nis2_article": "21.2.x",
"suggested_treatment": "mitigate|accept|transfer|avoid",
"mitigation_actions": ["Azione 1", "Azione 2"]
}
]
Rispondi SOLO con il JSON array.
PROMPT;
$response = $this->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 = <<<PROMPT
Sei un esperto di information security policy writing. Genera una policy aziendale per la categoria "{$category}" conforme alla Direttiva NIS2.
## Organizzazione (dati anonimizzati)
- Settore: {$organization['sector']}
- Tipo entità NIS2: {$organization['entity_type']}
## Contesto Assessment
{$context}
Genera la policy in formato JSON:
{
"title": "Titolo completo della policy",
"version": "1.0",
"category": "{$category}",
"nis2_article": "21.2.x",
"content": "Contenuto completo della policy in formato Markdown con sezioni: 1. Scopo, 2. Ambito di applicazione, 3. Responsabilità, 4. Definizioni, 5. Policy (regole specifiche), 6. Procedure operative, 7. Controlli, 8. Non conformità, 9. Revisione",
"review_period_months": 12
}
La policy deve essere in italiano, professionale, specifica per il settore dell'organizzazione, e conforme ai requisiti NIS2.
Rispondi SOLO con il JSON.
PROMPT;
$response = $this->callAPI($prompt);
return $this->parseJsonResponse($response);
}
/**
* Classifica un incidente e suggerisce severity
*/
public function classifyIncident(string $title, string $description, array $organization): array
{
$prompt = <<<PROMPT
Sei un analista di incident response. Classifica il seguente incidente di sicurezza secondo i criteri NIS2.
## Incidente
- Titolo: {$title}
- Descrizione: {$description}
## Organizzazione
- Settore: {$organization['sector']}
- Tipo entità: {$organization['entity_type']}
Rispondi in formato JSON:
{
"classification": "cyber_attack|data_breach|system_failure|human_error|natural_disaster|supply_chain|other",
"severity": "low|medium|high|critical",
"is_significant": true/false,
"significance_reason": "Motivo se significativo secondo NIS2",
"requires_csirt_notification": true/false,
"suggested_actions": ["Azione immediata 1", "Azione immediata 2"],
"potential_impact": "Descrizione impatto potenziale",
"iocs_to_check": ["Indicatore 1", "Indicatore 2"]
}
Rispondi SOLO con il JSON.
PROMPT;
$response = $this->callAPI($prompt);
return $this->parseJsonResponse($response);
}
/**
* Chiama Anthropic API
*/
private function callAPI(string $prompt, ?string $systemPrompt = null): string
{
$system = $systemPrompt ?? <<<SYSTEM
Sei un esperto consulente di cybersecurity e compliance NIS2, integrato nella piattaforma SaaS "NIS2 Agile".
Moduli della piattaforma che conosci:
- Gap Analysis (80 domande, 10 categorie Art.21)
- Risk Management (registro rischi ISO 27005, matrice 5×5)
- Incident Management (flussi Art.23: early warning 24h, notifica 72h, report 30d)
- Policy Management (generazione e approvazione policy NIS2)
- Supply Chain (valutazione fornitori, risk scoring)
- Training (assegnazione corsi, compliance Art.20)
- Asset Inventory (asset critici, dipendenze)
- Audit & Report (controlli, evidenze, export CSV)
- NCR/CAPA (non conformità, azioni correttive)
- Whistleblowing (segnalazioni anomalie Art.32)
- Normative Updates (feed NIS2/ACN/DORA con ACK)
- Segnalazioni Feedback (bug reporting con risoluzione autonoma AI)
- Services API (endpoint pubblici per integrazioni B2B)
Rispondi sempre in italiano, in modo professionale, preciso e conciso.
Non includere dati identificativi dell'organizzazione nelle risposte.
SYSTEM;
$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;
}
/**
* Classifica una segnalazione feedback/bug per il sistema di ticketing interno
*
* @param string $tipo Tipo segnalazione (bug|ux|funzionalita|domanda|altro)
* @param string $descrizione Testo della segnalazione (non contiene PII org)
* @return array ['categoria', 'priorita', 'suggerimento', 'risposta_utente']
*/
public function classifyFeedback(string $tipo, string $descrizione): array
{
$prompt = <<<PROMPT
Sei un assistente tecnico per una piattaforma SaaS di compliance NIS2.
Analizza la seguente segnalazione e rispondi SOLO con un oggetto JSON valido.
Tipo segnalazione: {$tipo}
Descrizione: {$descrizione}
Rispondi con questo JSON (senza markdown, solo JSON puro):
{
"categoria": "una tra: autenticazione, dashboard, rischi, incidenti, policy, assessment, formazione, fornitori, asset, report, impostazioni, performance, interfaccia, documentazione, altro",
"priorita": "una tra: alta, media, bassa",
"suggerimento": "suggerimento tecnico breve per il team di sviluppo (max 150 caratteri)",
"risposta_utente": "risposta leggibile e rassicurante per l'utente finale in italiano (max 200 caratteri)"
}
Criteri priorità:
- alta: blocca funzionalità critica (compliance, accesso, salvataggio dati)
- media: degrada l'esperienza ma esiste un workaround
- bassa: miglioria estetica o funzionalità secondaria
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([
'model' => $this->model,
'max_tokens' => 500,
'system' => 'Sei l\'assistente tecnico di NIS2 Agile, piattaforma SaaS per compliance NIS2. Conosci tutti i moduli: gap analysis, rischi, incidenti, policy, supply chain, formazione, asset, audit, NCR/CAPA, whistleblowing, normative, feedback. Rispondi SEMPRE con JSON puro, senza markdown né testo aggiuntivo.',
'messages' => [['role' => 'user', 'content' => $prompt]],
]),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if ($curlError || $httpCode !== 200) {
throw new RuntimeException("classifyFeedback AI error [{$httpCode}]: {$curlError}");
}
$data = json_decode($response, true);
$text = $data['content'][0]['text'] ?? '';
$result = $this->parseJsonResponse($text);
// Normalizza priorità
$validP = ['alta', 'media', 'bassa'];
if (!in_array($result['priorita'] ?? '', $validP)) {
$result['priorita'] = 'media';
}
return $result;
}
/**
* 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 = <<<SYSTEM
Sei un analista di cybersecurity e compliance NIS2 specializzato nell'analisi comparativa multi-organizzazione.
Rispondi sempre in italiano, in modo professionale e sintetico.
REGOLA FONDAMENTALE DI PRIVACY (non derogabile):
- Non fare mai riferimento a organizzazioni specifiche o identificabili.
- Rispondi SOLO con statistiche aggregate e trend generali.
- Se una risposta richiederebbe identificare una singola organizzazione, rifiuta con: "Dato non disponibile per protezione della privacy".
- Non inventare dati non presenti nel contesto.
SYSTEM;
$prompt = <<<PROMPT
## Portfolio analizzato: {$orgCount} organizzazioni (dati aggregati e anonimizzati)
{$context}
## Domanda del consulente
{$question}
Rispondi in formato JSON:
{
"answer": "Risposta principale dettagliata (3-6 paragrafi)",
"key_findings": ["Risultato chiave 1", "Risultato chiave 2"],
"recommendations": ["Raccomandazione pratica 1", "Raccomandazione pratica 2"],
"risk_areas": ["Area critica 1", "Area critica 2"],
"benchmark_note": "Nota comparativa settoriale se rilevante, altrimenti null",
"privacy_note": null
}
Rispondi SOLO con il JSON.
PROMPT;
$response = $this->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,
]);
}
}