[FEAT] Gestione Rischi: tabella 12 colonne (inerente+residuo) — integrazione prototipo "Modulo Azioni" (Simon)

Integra la spec di Simon per la pagina Rischi con le correzioni normative
(parere nis2-expert + iso-27001-expert) applicate come eccezioni motivate:
- 12 colonne: Codice (link → Misure e Requisiti), Descrizione (RISCHIO DEFAULT =
  cfg_nis2_rischi.risk_descr), VALUTAZIONE RISCHIO INERENTE (P/I/PxI), Alert,
  Rischio Inerente, VALUTAZIONE RISCHIO RESIDUO (P/I/PxI), Esito residuo, Rischio Residuo.
- Editing INLINE di Probabilità/Impatto (1-5), salvataggio automatico.
- Alert a bande (15-25 Prioritaria / 9-14 Critica / 3-8 Necessaria / 1-2 Suggerita)
  → popup azioni + "crea azione" (tabella requisito_actions, seme Modulo Azioni).
- Eccezione #1: il rischio RESIDUO NON aggiorna in automatico lo STATO DI CONFORMITÀ del
  requisito (piani distinti). Col.11 "Esito residuo" = flag Rivalutare/Adeguato; conformità esplicita.
- Eccezione #3: banda 13-14 inclusa in "Critica" (9-14). "Danno €" → "Impatto" (1-5).
  Soglie = metodo interno (ISO 27001 §6.1.2), non obbligo (help/UI lo dichiarano).

Backend: mig 067; derivedList (+risk_descr +residuo), setState (+residuo), requisitoActions list/create.
help.js + i18n IT/EN. Cache-buster api/i18n/help 20260730a. Prossima mig=068.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-07-30 10:49:52 +02:00
co-authored by Claude Opus 4.8
parent cf224480cc
commit 8a0bc5ca39
49 changed files with 535 additions and 180 deletions
+66 -1
View File
@@ -69,10 +69,13 @@ class RiskController extends BaseController
$rows = Database::fetchAll(
"SELECT q.id AS requisito_id, q.n, q.misura_code, q.requisito_code,
q.requisito_descr, q.applies_important, q.applies_essential,
q.risk_code AS default_risk_code, rk.risk_descr AS default_risk_descr,
m.misura_descr, m.nist_code, m.area_politica, m.ord,
s.stato, s.valutazione_rischio, s.note, s.likelihood, s.impact
s.stato, s.valutazione_rischio, s.note, s.likelihood, s.impact,
s.residual_likelihood, s.residual_impact
FROM cfg_nis2_requisiti q
JOIN cfg_nis2_misure m ON m.misura_code = q.misura_code
LEFT JOIN cfg_nis2_rischi rk ON rk.risk_code = q.risk_code
LEFT JOIN org_requisito_state s
ON s.requisito_id = q.id AND s.organization_id = ?
ORDER BY m.ord ASC, q.n ASC, q.id ASC",
@@ -100,15 +103,24 @@ class RiskController extends BaseController
'requisito_code' => $q['requisito_code'],
'comma' => $commaN,
'requisito_descr' => $q['requisito_descr'],
// RISCHIO(DEFAULT) del framework (cfg_nis2_rischi.risk_descr); fallback = requisito_descr.
'risk_descr' => ($q['default_risk_descr'] !== null && $q['default_risk_descr'] !== '')
? $q['default_risk_descr'] : $q['requisito_descr'],
'applies_important' => (int) $q['applies_important'],
'applies_essential' => (int) $q['applies_essential'],
'stato' => $q['stato'] ?? 'da_valutare',
'valutazione_rischio' => $q['valutazione_rischio'] ?? null,
'note' => $q['note'] ?? null,
// Rischio INERENTE (prima del trattamento) = likelihood/impact (mig 066)
'likelihood' => isset($q['likelihood']) && $q['likelihood'] !== null ? (int) $q['likelihood'] : null,
'impact' => isset($q['impact']) && $q['impact'] !== null ? (int) $q['impact'] : null,
'score' => (isset($q['likelihood'], $q['impact']) && $q['likelihood'] !== null && $q['impact'] !== null)
? (int) $q['likelihood'] * (int) $q['impact'] : null,
// Rischio RESIDUO (dopo il trattamento) = residual_* (mig 067)
'residual_likelihood' => isset($q['residual_likelihood']) && $q['residual_likelihood'] !== null ? (int) $q['residual_likelihood'] : null,
'residual_impact' => isset($q['residual_impact']) && $q['residual_impact'] !== null ? (int) $q['residual_impact'] : null,
'residual_score' => (isset($q['residual_likelihood'], $q['residual_impact']) && $q['residual_likelihood'] !== null && $q['residual_impact'] !== null)
? (int) $q['residual_likelihood'] * (int) $q['residual_impact'] : null,
];
}
@@ -611,4 +623,57 @@ class RiskController extends BaseController
{
return ($v === null || $v === '') ? null : (float) $v;
}
/**
* GET /api/risks/requisitoActions?requisito_id=NN
* Azioni collegate al rischio-da-requisito (seme del Modulo Azioni). Piu' recenti prima.
*/
public function requisitoActionsList(): void
{
$this->requireOrgAccess();
$orgId = $this->getCurrentOrgId();
$reqId = (int) $this->getParam('requisito_id', 0);
if ($reqId <= 0) { $this->jsonSuccess(['actions' => []]); return; }
$rows = Database::fetchAll(
'SELECT id, risk_code, description, status, created_at
FROM requisito_actions
WHERE organization_id = ? AND requisito_id = ?
ORDER BY created_at DESC, id DESC',
[$orgId, $reqId]
);
$this->jsonSuccess(['actions' => $rows]);
}
/**
* POST /api/risks/requisitoActions {requisito_id*, description*, risk_code?}
* Crea un'azione collegata al rischio-da-requisito. NON tocca lo stato di conformita'
* del requisito (org_requisito_state.stato): la conformita' resta atto di attuazione+evidenza.
*/
public function requisitoActionsCreate(): void
{
$this->requireOrgRole(['super_admin', 'org_admin', 'compliance_manager']);
$orgId = $this->getCurrentOrgId();
$reqId = (int) $this->getParam('requisito_id', 0);
$descr = trim((string) $this->getParam('description', ''));
$riskCode = $this->getParam('risk_code');
if ($reqId <= 0 || $descr === '') {
$this->jsonError('requisito_id e description sono obbligatori', 422);
return;
}
$id = Database::insert('requisito_actions', [
'organization_id' => $orgId,
'requisito_id' => $reqId,
'risk_code' => $riskCode ? substr((string) $riskCode, 0, 64) : null,
'description' => $descr,
'status' => 'bozza',
'created_by' => $this->getCurrentUserId(),
]);
$this->logAudit('requisito_action_created', 'requisito_action', $id);
$row = Database::fetchOne(
'SELECT id, risk_code, description, status, created_at FROM requisito_actions WHERE id = ?',
[$id]
);
$this->jsonSuccess(['action' => $row], 'Azione creata');
}
}