[CORE] Initial project scaffold - NIS2 Agile Compliance Platform
Complete MVP implementation including: - PHP 8.4 backend with Front Controller pattern (80+ API endpoints) - Multi-tenant architecture with organization_id isolation - JWT authentication (HS256, 2h access + 7d refresh tokens) - 14 controllers: Auth, Organization, Assessment, Dashboard, Risk, Incident, Policy, SupplyChain, Training, Asset, Audit, Admin - AI Service integration (Anthropic Claude API) for gap analysis, risk suggestions, policy generation, incident classification - NIS2 gap analysis questionnaire (~80 questions, 10 categories) - MySQL schema (20 tables) with NIS2 Art. 21 compliance controls - NIS2 Art. 23 incident reporting workflow (24h/72h/30d) - Frontend: login, register, dashboard, assessment wizard, org setup - Docker configuration (PHP-FPM + Nginx + MySQL) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,315 @@
|
||||
<?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);
|
||||
|
||||
$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
|
||||
- Nome: {$organization['name']}
|
||||
- Settore: {$organization['sector']}
|
||||
- Tipo entità NIS2: {$organization['entity_type']}
|
||||
- Dipendenti: {$organization['employee_count']}
|
||||
- Fatturato annuo: EUR {$organization['annual_turnover_eur']}
|
||||
|
||||
## 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
|
||||
- Nome: {$organization['name']}
|
||||
- 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 ?? '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'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user