diff --git a/application/cli/migrate_067_requisito_residual_and_actions.php b/application/cli/migrate_067_requisito_residual_and_actions.php new file mode 100644 index 0000000..c296cf8 --- /dev/null +++ b/application/cli/migrate_067_requisito_residual_and_actions.php @@ -0,0 +1,64 @@ +prepare( + "SELECT 1 FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?" + ); + $st->execute([$table, $col]); + return (bool) $st->fetchColumn(); +} +function tblExists(PDO $pdo, string $table): bool { + $st = $pdo->prepare( + "SELECT 1 FROM information_schema.TABLES + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?" + ); + $st->execute([$table]); + return (bool) $st->fetchColumn(); +} + +$adds = [ + 'residual_likelihood' => "ADD COLUMN residual_likelihood TINYINT NULL COMMENT 'Probabilita residua 1-5' AFTER impact", + 'residual_impact' => "ADD COLUMN residual_impact TINYINT NULL COMMENT 'Impatto residuo 1-5' AFTER residual_likelihood", +]; +foreach ($adds as $col => $clause) { + if (colExists($pdo, 'org_requisito_state', $col)) { echo " - $col gia presente, skip\n"; continue; } + $pdo->exec("ALTER TABLE org_requisito_state $clause"); + echo " + aggiunta colonna org_requisito_state.$col\n"; +} + +if (tblExists($pdo, 'requisito_actions')) { + echo " - tabella requisito_actions gia presente, skip\n"; +} else { + $pdo->exec( + "CREATE TABLE requisito_actions ( + id INT AUTO_INCREMENT PRIMARY KEY, + organization_id INT NOT NULL, + requisito_id INT NOT NULL, + risk_code VARCHAR(64) NULL, + description TEXT NOT NULL, + status ENUM('bozza','in_corso','archiviata') NOT NULL DEFAULT 'bozza', + created_by INT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_reqact_org_req (organization_id, requisito_id, created_at), + INDEX idx_reqact_org (organization_id, created_at) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ); + echo " + creata tabella requisito_actions\n"; +} + +echo "Migrazione 067 — residuo + requisito_actions OK. Prossima mig=068.\n"; diff --git a/application/controllers/FrameworkController.php b/application/controllers/FrameworkController.php index 382906a..31e0da7 100644 --- a/application/controllers/FrameworkController.php +++ b/application/controllers/FrameworkController.php @@ -207,24 +207,29 @@ class FrameworkController extends BaseController }; $likelihood = array_key_exists('likelihood', $body) ? $scale($body['likelihood']) : null; $impact = array_key_exists('impact', $body) ? $scale($body['impact']) : null; + // Rischio RESIDUO (mig 067) — dopo il trattamento. Il client invia sempre la riga intera. + $resLikelihood = array_key_exists('residual_likelihood', $body) ? $scale($body['residual_likelihood']) : null; + $resImpact = array_key_exists('residual_impact', $body) ? $scale($body['residual_impact']) : null; // upsert org-scoped (UNIQUE org+requisito) Database::query( 'INSERT INTO org_requisito_state - (organization_id, requisito_id, stato, valutazione_rischio, note, likelihood, impact, updated_by) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + (organization_id, requisito_id, stato, valutazione_rischio, note, likelihood, impact, residual_likelihood, residual_impact, updated_by) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE stato = VALUES(stato), valutazione_rischio = VALUES(valutazione_rischio), note = VALUES(note), likelihood = VALUES(likelihood), impact = VALUES(impact), + residual_likelihood = VALUES(residual_likelihood), + residual_impact = VALUES(residual_impact), updated_by = VALUES(updated_by)', - [$orgId, $reqId, $stato ?? 'da_valutare', $valutazione, $note, $likelihood, $impact, $this->getCurrentUserId()] + [$orgId, $reqId, $stato ?? 'da_valutare', $valutazione, $note, $likelihood, $impact, $resLikelihood, $resImpact, $this->getCurrentUserId()] ); $row = Database::fetchOne( - 'SELECT requisito_id, stato, valutazione_rischio, note, likelihood, impact, updated_at + 'SELECT requisito_id, stato, valutazione_rischio, note, likelihood, impact, residual_likelihood, residual_impact, updated_at FROM org_requisito_state WHERE organization_id = ? AND requisito_id = ?', [$orgId, $reqId] ); @@ -235,6 +240,8 @@ class FrameworkController extends BaseController 'note' => $row['note'], 'likelihood' => $row['likelihood'] !== null ? (int) $row['likelihood'] : null, 'impact' => $row['impact'] !== null ? (int) $row['impact'] : null, + 'residual_likelihood' => $row['residual_likelihood'] !== null ? (int) $row['residual_likelihood'] : null, + 'residual_impact' => $row['residual_impact'] !== null ? (int) $row['residual_impact'] : null, 'updated_at' => $row['updated_at'], ], 'Valutazione salvata'); } diff --git a/application/controllers/RiskController.php b/application/controllers/RiskController.php index d870b54..7f73601 100644 --- a/application/controllers/RiskController.php +++ b/application/controllers/RiskController.php @@ -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'); + } } diff --git a/docs/sql/067_requisito_residual_and_actions.sql b/docs/sql/067_requisito_residual_and_actions.sql new file mode 100644 index 0000000..907df13 --- /dev/null +++ b/docs/sql/067_requisito_residual_and_actions.sql @@ -0,0 +1,25 @@ +-- 067_requisito_residual_and_actions.sql — Integrazione prototipo "Modulo Azioni" (Simon), Fase 1. +-- (1) org_requisito_state += valutazione del RISCHIO RESIDUO (dopo il trattamento): +-- residual_likelihood / residual_impact (1-5). L'inerente resta likelihood/impact (mig 066). +-- (2) requisito_actions: azioni collegate al rischio-da-requisito (seme del modulo Azioni). +-- NB normativo: NON tocca lo stato di conformità del requisito (org_requisito_state.stato), +-- che resta un atto esplicito di attuazione+evidenza. Vedi eccezione #1 (parere nis2-expert). +-- Additiva e idempotente. Runner: application/cli/migrate_067_requisito_residual_and_actions.php + +ALTER TABLE org_requisito_state + ADD COLUMN residual_likelihood TINYINT NULL COMMENT 'Probabilita residua 1-5 (dopo trattamento)' AFTER impact, + ADD COLUMN residual_impact TINYINT NULL COMMENT 'Impatto residuo 1-5 (dopo trattamento)' AFTER residual_likelihood; + +CREATE TABLE IF NOT EXISTS requisito_actions ( + id INT AUTO_INCREMENT PRIMARY KEY, + organization_id INT NOT NULL, + requisito_id INT NOT NULL, + risk_code VARCHAR(64) NULL, + description TEXT NOT NULL, + status ENUM('bozza','in_corso','archiviata') NOT NULL DEFAULT 'bozza', + created_by INT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_reqact_org_req (organization_id, requisito_id, created_at), + INDEX idx_reqact_org (organization_id, created_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/public/_app-bi-demo.html b/public/_app-bi-demo.html index ab338c7..50fa908 100644 --- a/public/_app-bi-demo.html +++ b/public/_app-bi-demo.html @@ -70,7 +70,7 @@ - + + - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + - - + + - + + - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + - + + - - + + + - - + + + - - + + - + + - - + + + - - + + + - - + + - + + - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + - - + +