[FEAT] ARIA data-aware: snapshot dati org dell'utente nel prompt (Fase B)
Richiesta: "AI deve sapere i contenuti dei dati a cui ha accesso l'utente". - AIService::orgDataSnapshotBlock($orgId): aggregati LIVE dell'org (compliance score, rischi aperti per livello + top 5, incidenti aperti, asset attivi/rilevanti NIS2, fornitori critici scoperti, NC aperte, formazione, policy per stato). Query con colonne reali (riuso DashboardController), ognuna in try/catch (degrada, non rompe). - Iniettato in askWithRag SOLO se org_data_ok. - SICUREZZA multi-tenant: AiController verifica membership in user_organizations (super_admin bypassa) → org_data_ok; scope rigoroso organization_id → niente leak cross-org da X-Organization-Id falsificato. PRIVACY: niente nome/fatturato (anonimizz.). Smoke: con membership ARIA cita score 80% + 14 rischi (org 996003); senza membership nega l'accesso ai dati. php-fpm reload. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
42529043d3
commit
fe7d0555c6
@@ -54,6 +54,22 @@ class AiController extends BaseController
|
||||
'page_help' => $pageHelp,
|
||||
];
|
||||
|
||||
// Membership verificata → ARIA può iniettare lo snapshot DATI dell'org
|
||||
// (anti-spoof X-Organization-Id; super_admin bypassa, come da modello ruoli).
|
||||
$orgId = (int) ($userContext['organization_id'] ?? 0);
|
||||
$userContext['org_data_ok'] = false;
|
||||
if ($orgId > 0) {
|
||||
if (($user['role'] ?? '') === 'super_admin') {
|
||||
$userContext['org_data_ok'] = true;
|
||||
} else {
|
||||
$member = Database::fetchOne(
|
||||
'SELECT 1 FROM user_organizations WHERE user_id = ? AND organization_id = ?',
|
||||
[$userId, $orgId]
|
||||
);
|
||||
$userContext['org_data_ok'] = (bool) $member;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$aiService = new AIService();
|
||||
$result = $aiService->askWithRag($question, $userContext);
|
||||
|
||||
@@ -106,6 +106,51 @@ class AIService
|
||||
return implode("\n", $lines) . "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Snapshot LIVE dei dati dell'organizzazione dell'utente, da iniettare nel
|
||||
* system prompt di ARIA così può rispondere su numeri SPECIFICI (rischi,
|
||||
* incidenti, asset, score...) e non solo su normativa/guida.
|
||||
*
|
||||
* SICUREZZA: chiamato SOLO dopo verifica membership (AiController) → scope
|
||||
* rigoroso `organization_id = $orgId`. PRIVACY: niente nome/fatturato dell'org
|
||||
* (coerente con l'anonimizzazione già in uso); solo aggregati + titoli operativi.
|
||||
* Ogni query è in try/catch: una colonna assente non rompe ARIA (degrada).
|
||||
*/
|
||||
private function orgDataSnapshotBlock(int $orgId): string
|
||||
{
|
||||
if ($orgId <= 0) return '';
|
||||
$q = function ($sql, $p = []) { try { return Database::fetchOne($sql, $p); } catch (\Throwable $e) { return null; } };
|
||||
$all = function ($sql, $p = []) { try { return Database::fetchAll($sql, $p); } catch (\Throwable $e) { return []; } };
|
||||
|
||||
$org = $q("SELECT sector, entity_type, voluntary_compliance FROM organizations WHERE id=?", [$orgId]);
|
||||
$asmt = $q("SELECT overall_score FROM assessments WHERE organization_id=? AND status='completed' ORDER BY completed_at DESC LIMIT 1", [$orgId]);
|
||||
$risk = $q("SELECT COUNT(*) tot, SUM(inherent_risk_score>=20) ch, SUM(inherent_risk_score BETWEEN 10 AND 19) med, SUM(inherent_risk_score<10) low FROM risks WHERE organization_id=? AND status!='closed'", [$orgId]);
|
||||
$top = $all("SELECT title, inherent_risk_score s FROM risks WHERE organization_id=? AND status!='closed' ORDER BY inherent_risk_score DESC LIMIT 5", [$orgId]);
|
||||
$inc = $q("SELECT COUNT(*) c FROM incidents WHERE organization_id=? AND status NOT IN ('closed','post_mortem')", [$orgId]);
|
||||
$asTot = $q("SELECT COUNT(*) c FROM assets WHERE organization_id=? AND status='active'", [$orgId]);
|
||||
$asRel = $q("SELECT COUNT(*) c FROM assets WHERE organization_id=? AND is_nis2_relevant=1", [$orgId]);
|
||||
$sup = $q("SELECT COUNT(*) c FROM suppliers WHERE organization_id=? AND criticality IN ('high','critical') AND security_requirements_met=0", [$orgId]);
|
||||
$nc = $q("SELECT COUNT(*) c FROM non_conformities WHERE organization_id=? AND status NOT IN ('closed','resolved','verified')", [$orgId]);
|
||||
$tr = $q("SELECT COUNT(*) tot, SUM(status='completed') done, SUM(status='overdue') ovd FROM training_assignments WHERE organization_id=?", [$orgId]);
|
||||
$pol = $all("SELECT status, COUNT(*) n FROM policies WHERE organization_id=? GROUP BY status", [$orgId]);
|
||||
|
||||
$L = [];
|
||||
$L[] = "\n## DATI DELL'ORGANIZZAZIONE DELL'UTENTE (snapshot live — SOLO la sua org)";
|
||||
$L[] = "Usa questi numeri per risposte specifiche sulla sua situazione. Sono dati riservati della sua organizzazione: non confrontarli con altre org. Se un dato è 0/assente, dillo con chiarezza.";
|
||||
if ($org) $L[] = "- Classificazione: " . ($org['entity_type'] ?: 'n/d') . (!empty($org['voluntary_compliance']) ? ' (adesione volontaria)' : '') . ($org['sector'] ? ", settore " . $org['sector'] : '');
|
||||
$L[] = "- Compliance score (ultimo assessment): " . ($asmt && $asmt['overall_score'] !== null ? round((float)$asmt['overall_score']) . '%' : 'nessun assessment completato');
|
||||
if ($risk) $L[] = "- Rischi aperti: " . (int)$risk['tot'] . " (critico/alto " . (int)$risk['ch'] . ", medio " . (int)$risk['med'] . ", basso " . (int)$risk['low'] . ")";
|
||||
if ($top) $L[] = " Top rischi: " . implode('; ', array_map(fn($x) => $x['title'] . ' (' . (int)$x['s'] . ')', $top));
|
||||
$L[] = "- Incidenti aperti: " . (int)($inc['c'] ?? 0);
|
||||
$L[] = "- Asset attivi: " . (int)($asTot['c'] ?? 0) . " (rilevanti NIS2: " . (int)($asRel['c'] ?? 0) . ")";
|
||||
$L[] = "- Fornitori critici con requisiti sicurezza NON soddisfatti: " . (int)($sup['c'] ?? 0);
|
||||
$L[] = "- Non conformità aperte: " . (int)($nc['c'] ?? 0);
|
||||
if ($tr && (int)$tr['tot'] > 0) $L[] = "- Formazione: " . (int)$tr['done'] . "/" . (int)$tr['tot'] . " completate, " . (int)$tr['ovd'] . " scadute";
|
||||
if ($pol) $L[] = "- Policy: " . implode(', ', array_map(fn($x) => $x['status'] . ' ' . $x['n'], $pol));
|
||||
|
||||
return implode("\n", $L) . "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Analizza risultati gap analysis e genera raccomandazioni
|
||||
*/
|
||||
@@ -787,6 +832,14 @@ PROMPT;
|
||||
$systemPrompt .= $this->relevanceGridBlock();
|
||||
}
|
||||
|
||||
// DATI ORG dell'utente: iniettati SOLO se la membership è stata verificata
|
||||
// (AiController imposta org_data_ok dopo il check user_organizations / super_admin)
|
||||
// → scope rigoroso, niente leak cross-org da X-Organization-Id falsificato.
|
||||
$snapOrgId = (int) ($userContext['organization_id'] ?? 0);
|
||||
if ($snapOrgId > 0 && !empty($userContext['org_data_ok'])) {
|
||||
$systemPrompt .= $this->orgDataSnapshotBlock($snapOrgId);
|
||||
}
|
||||
|
||||
if (!empty($contextBlock)) {
|
||||
$systemPrompt .= "\n## Contesto documentale (knowledge base)\n" . $contextBlock
|
||||
. "\n\nQuando rispondi, cita esplicitamente i numeri tra parentesi quadre [1], [2], ... che corrispondono ai documenti del contesto.";
|
||||
|
||||
Reference in New Issue
Block a user