[FIX] Gap Analysis ACN: bug da review avversariale (4 agenti)

#1 CRITICO aiAnalyze: askWithRag ritorna ['answer','sources','rag_used'], non
   una stringa. Ora estrae 'answer' (ai_summary) e salva 'sources' in
   ai_recommendations. Prima salvava il JSON intero in ai_summary.
#2 ALTO corpus RAG: acn_requirements.json aveva 188/203 testi TRONCATI alla
   prima riga PDF (es. GV.PO-01#1: 84 char invece di 838). Rigenerato dai testi
   INTEGRALI di acn_measures.json (87+116, zero troncamenti). Ri-ingest Qdrant.
#3 MEDIO catalog(): org non classificata dava entity_level=null + warning PHP
   $totals[null] + TypeError frontend. Ora 422 ENTITY_LEVEL_REQUIRED come create().
#4 MEDIO guida cap-5 GV.RR-04: "figure chiave dell'organigramma" era errato e
   auto-contraddittorio -> "personale autorizzato + amministratori di sistema,
   valutazione esperienza/capacita/affidabilita" (allineato testo ACN).
#5 BASSI: openAcn try/catch (no unhandled rejection su Riprendi); badge
   importante/essenziale IT/EN; overall_score=null (non 0.0) se tutti N/A.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile 2026-06-01 12:18:25 +02:00
parent 09f51795c1
commit d6924a30c1
4 changed files with 1667 additions and 1646 deletions

View File

@ -47,6 +47,15 @@ class AcnAssessmentController extends BaseController
{
$this->requireOrgAccess();
$level = $this->resolveEntityLevel();
if ($level === null) {
// Coerente con create(): senza classificazione non c'e perimetro.
// Il frontend intercetta ENTITY_LEVEL_REQUIRED e mostra l'invito a classificare.
$this->jsonError(
'Il soggetto non e classificato come importante o essenziale. Completa prima la classificazione NIS2 dell\'organizzazione.',
422,
'ENTITY_LEVEL_REQUIRED'
);
}
$grouped = $this->groupedRequirements($level, []);
$totals = $this->datasetTotals();
@ -385,18 +394,23 @@ class AcnAssessmentController extends BaseController
'organization_id' => $this->getCurrentOrgId(),
'consulting_firm_id' => $this->currentUser['consulting_firm_id'] ?? null,
];
$answer = $ai->askWithRag($question, $userContext);
// askWithRag ritorna ['answer'=>string, 'sources'=>array, 'rag_used'=>bool]:
// estraiamo il testo, NON salviamo l'intero array.
$result = $ai->askWithRag($question, $userContext);
$answer = is_array($result) ? (string) ($result['answer'] ?? '') : (string) $result;
$sources = (is_array($result) && isset($result['sources'])) ? $result['sources'] : [];
} catch (Throwable $e) {
error_log('[AcnAssessment] aiAnalyze fallita: ' . $e->getMessage());
$this->jsonError('Analisi AI temporaneamente non disponibile. Riprova piu tardi.', 503, 'AI_UNAVAILABLE');
}
Database::update('acn_assessments', [
'ai_summary' => is_string($answer) ? $answer : json_encode($answer, JSON_UNESCAPED_UNICODE),
'ai_summary' => $answer,
'ai_recommendations' => json_encode($sources, JSON_UNESCAPED_UNICODE),
], 'id = ?', [$id]);
$this->logAudit('acn_assessment_ai_analyzed', 'acn_assessment', $id, ['gaps' => count($gaps)]);
$this->jsonSuccess(['ai_summary' => $answer], 'Analisi AI completata');
$this->jsonSuccess(['ai_summary' => $answer, 'sources' => $sources], 'Analisi AI completata');
}
// ════════════════════════ HELPER ════════════════════════
@ -580,7 +594,9 @@ class AcnAssessmentController extends BaseController
$cnt++;
}
$overall = $cnt > 0 ? round($sum / $cnt, 2) : 0.0;
// Se nessun requisito e applicabile (tutti not_applicable) il punteggio
// non e definito: null, non 0.0 (che significherebbe "0% conforme").
$overall = $cnt > 0 ? round($sum / $cnt, 2) : null;
$funcScores = [];
foreach ($byFunc as $fc => $d) {
$funcScores[$fc] = $d['cnt'] > 0 ? round($d['sum'] / $d['cnt'], 2) : 0.0;

File diff suppressed because it is too large Load Diff

View File

@ -158,8 +158,8 @@
const cat = await api.acnCatalog();
ACN.level = cat.entity_level;
const lvlTxt = cat.entity_level === 'essential'
? '<span class="acn-level-badge acn-level-essential">Soggetto ESSENZIALE</span>'
: '<span class="acn-level-badge acn-level-important">Soggetto IMPORTANTE</span>';
? '<span class="acn-level-badge acn-level-essential">'+(lang()==='en'?'ESSENTIAL entity':'Soggetto ESSENZIALE')+'</span>'
: '<span class="acn-level-badge acn-level-important">'+(lang()==='en'?'IMPORTANT entity':'Soggetto IMPORTANTE')+'</span>';
el('acn-level-info').innerHTML = lvlTxt +
'<p class="text-muted" style="margin-top:10px;">' +
(lang()==='en'?'Applicable scope: ':'Perimetro applicabile: ') +
@ -228,6 +228,7 @@
}
async function openAcn(id, status){
try {
ACN.id = id;
const res = await api.acnRequirements(id);
ACN.level = res.assessment.entity_level;
@ -236,6 +237,9 @@
return;
}
renderWizard(res);
} catch(e){
alert((e && e.message) || (lang()==='en'?'Could not open the assessment.':'Impossibile aprire l\'assessment.'));
}
}
function renderWizard(res){
@ -245,8 +249,8 @@
el('acn-wizard').style.display='block';
el('btn-ai-analyze').style.display='none';
el('acn-wizard-level').innerHTML = ACN.level==='essential'
? '<span class="acn-level-badge acn-level-essential">ESSENZIALE</span>'
: '<span class="acn-level-badge acn-level-important">IMPORTANTE</span>';
? '<span class="acn-level-badge acn-level-essential">'+(lang()==='en'?'ESSENTIAL':'ESSENZIALE')+'</span>'
: '<span class="acn-level-badge acn-level-important">'+(lang()==='en'?'IMPORTANT':'IMPORTANTE')+'</span>';
ACN.answers = {}; ACN.total = 0;
let html = '';

View File

@ -458,9 +458,10 @@
</ul>
<div class="example-box">
<strong>Esempio (requisiti di governance).</strong> <code>GV.RR-04</code> richiede che la cybersicurezza
sia integrata nella gestione delle risorse umane: chi accede ai sistemi (utenti e amministratori) deve
avere <strong>competenze e affidabilità adeguate al ruolo</strong>, valutate e documentate — è il
"controllo di adeguatezza al ruolo" delle figure chiave dell'organigramma.
sia integrata nella gestione delle risorse umane: il <strong>personale autorizzato ad accedere</strong>
ai sistemi rilevanti e gli <strong>amministratori di sistema</strong> devono essere individuati previa
valutazione di <strong>esperienza, capacità e affidabilità</strong> (non necessariamente una certificazione),
e tale valutazione va documentata.
<code>GV.PO-01</code> richiede una <strong>policy di gestione del rischio cyber</strong> formalizzata,
comunicata e applicata.
</div>