[FEAT] L4 AI Cross-Analysis — analisi aggregata multi-org per consulenti

- CrossAnalysisController.php: analyze/history/portfolio (k-anonymity min 2 org)
- AIService::crossOrgAnalysis(): aggregazione 9 dimensioni, zero PII nel prompt
- cross-analysis.html: chat UI purple theme, 3 tab, quick questions, portfolio stats
- index.php: routing /api/cross-analysis/{analyze,history,portfolio}
- common.js: link "AI Cross-Analysis" in sidebar sezione Gestione
- docs/AI_LEVELS_SCHEMA.md: schema architetturale L1-L5 con matrice privacy

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-03-09 08:17:53 +01:00
co-authored by Claude Sonnet 4.6
parent 89fd201bc2
commit 19a9e5622d
5 changed files with 1258 additions and 1 deletions
+148
View File
@@ -310,6 +310,154 @@ PROMPT;
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 = <<<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
*/