[FEAT] Gap Analysis estesa ai requisiti ACN (specifiche di base 164179/2025)
Allinea il PRODOTTO alla guida/normativa portando la compliance dal livello 10 misure Art.21 al livello operativo dei requisiti ACN (Framework Nazionale 2025). - Migrazione 031: acn_requirements (catalogo) + org_acn_requirement_status (stato per-org) - Seed da Allegati 1-2 ACN (fonte certa, parsing verificato): 87 importanti + 116 essenziali = 203 requisiti reali - AuditController: acnRequirements (GET, per entity_type org: importanti 87 / essenziali 116, summary per funzione GV/ID/PR/DE/RS/RC, % compliance) + updateAcnRequirement (PUT stato+evidenza) - Route audit/acnRequirements GET/PUT - guida.html: fix refuso cap-5 (residuo 'otto categorie...no' -> '10 categorie x 8, quattro modalita') E2E prod: org importante -> 87 req; PUT implemented -> compliance aggiornata. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6671d51969
commit
59205d05fb
@ -470,4 +470,98 @@ class AuditController extends BaseController
|
||||
'recent_evidence' => $recent,
|
||||
]);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
// REQUISITI ACN (specifiche di base, Determina 164179/2025)
|
||||
// Gap Analysis a livello requisiti: 87 (importanti) / 116 (essenziali).
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
/** Mappa entity_type org (essential/important/...) -> entity catalogo ACN. */
|
||||
private function acnEntityFor(int $orgId): string
|
||||
{
|
||||
$org = Database::fetchOne('SELECT entity_type FROM organizations WHERE id = ?', [$orgId]);
|
||||
// I soggetti essenziali hanno il set esteso (116). Tutti gli altri (importanti o
|
||||
// non classificati) usano il set base importanti (87).
|
||||
return ($org && ($org['entity_type'] ?? '') === 'essential') ? 'essenziale' : 'importante';
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/audit/acnRequirements
|
||||
* Requisiti ACN applicabili all'org (per entity_type) con stato + sommario per funzione.
|
||||
*/
|
||||
public function acnRequirements(): void
|
||||
{
|
||||
$this->requireOrgAccess();
|
||||
$orgId = $this->getCurrentOrgId();
|
||||
$entity = $this->acnEntityFor($orgId);
|
||||
|
||||
$rows = Database::fetchAll(
|
||||
"SELECT r.id, r.function_name, r.subcategory, r.subcategory_text, r.req_index, r.requirement,
|
||||
COALESCE(s.status,'not_started') AS status, s.evidence_note, s.updated_at
|
||||
FROM acn_requirements r
|
||||
LEFT JOIN org_acn_requirement_status s
|
||||
ON s.requirement_id = r.id AND s.organization_id = ?
|
||||
WHERE r.entity = ?
|
||||
ORDER BY r.subcategory, r.req_index",
|
||||
[$orgId, $entity]
|
||||
);
|
||||
|
||||
$summary = ['not_started'=>0,'in_progress'=>0,'implemented'=>0,'not_applicable'=>0];
|
||||
$byFunc = [];
|
||||
foreach ($rows as $r) {
|
||||
$st = $r['status'];
|
||||
if (isset($summary[$st])) $summary[$st]++;
|
||||
$f = $r['function_name'];
|
||||
if (!isset($byFunc[$f])) $byFunc[$f] = ['total'=>0,'implemented'=>0];
|
||||
$byFunc[$f]['total']++;
|
||||
if ($st === 'implemented') $byFunc[$f]['implemented']++;
|
||||
}
|
||||
$total = count($rows);
|
||||
$applicable = $total - $summary['not_applicable'];
|
||||
$compliance = $applicable > 0 ? (int) round($summary['implemented'] * 100 / $applicable) : 0;
|
||||
|
||||
$this->jsonSuccess([
|
||||
'entity' => $entity,
|
||||
'total' => $total,
|
||||
'summary' => $summary,
|
||||
'by_function' => $byFunc,
|
||||
'compliance_percent'=> $compliance,
|
||||
'requirements' => $rows,
|
||||
'source' => 'Determina ACN 164179/2025, Allegati 1-2 (Framework Nazionale 2025)',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /api/audit/acnRequirements/{id}
|
||||
* Aggiorna lo stato di un requisito ACN per l'org. Body: { status, evidence_note? }
|
||||
*/
|
||||
public function updateAcnRequirement(int $id): void
|
||||
{
|
||||
$this->requireOrgRole(['org_admin', 'compliance_manager', 'auditor']);
|
||||
$orgId = $this->getCurrentOrgId();
|
||||
$entity = $this->acnEntityFor($orgId);
|
||||
|
||||
// Il requisito deve appartenere al set applicabile all'org
|
||||
$req = Database::fetchOne('SELECT id FROM acn_requirements WHERE id = ? AND entity = ?', [$id, $entity]);
|
||||
if (!$req) {
|
||||
$this->jsonError('Requisito non trovato o non applicabile a questa organizzazione', 404, 'NOT_FOUND');
|
||||
}
|
||||
|
||||
$status = $this->getParam('status');
|
||||
$valid = ['not_started','in_progress','implemented','not_applicable'];
|
||||
if (!in_array($status, $valid, true)) {
|
||||
$this->jsonError('Stato non valido', 422, 'INVALID_STATUS');
|
||||
}
|
||||
$note = $this->getParam('evidence_note');
|
||||
|
||||
Database::query(
|
||||
'INSERT INTO org_acn_requirement_status (organization_id, requirement_id, status, evidence_note, updated_by)
|
||||
VALUES (?,?,?,?,?)
|
||||
ON DUPLICATE KEY UPDATE status=VALUES(status), evidence_note=VALUES(evidence_note),
|
||||
updated_by=VALUES(updated_by), updated_at=NOW()',
|
||||
[$orgId, $id, $status, $note, $this->getCurrentUserId()]
|
||||
);
|
||||
$this->logAudit('acn_requirement_updated', 'acn_requirement', $id, ['status' => $status]);
|
||||
$this->jsonSuccess(['id' => $id, 'status' => $status], 'Requisito aggiornato');
|
||||
}
|
||||
}
|
||||
|
||||
1630
docs/nis2/allegati_acn/acn_requirements.json
Normal file
1630
docs/nis2/allegati_acn/acn_requirements.json
Normal file
File diff suppressed because it is too large
Load Diff
49
docs/sql/031_acn_requirements.sql
Normal file
49
docs/sql/031_acn_requirements.sql
Normal file
@ -0,0 +1,49 @@
|
||||
-- ============================================================================
|
||||
-- Migration 031 - Requisiti ACN (specifiche di base, Determina 164179/2025)
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- Porta la Gap Analysis dal livello "10 misure Art.21" al livello operativo
|
||||
-- dei requisiti ACN (Framework Nazionale 2025): 87 per soggetti importanti,
|
||||
-- 116 per essenziali. Additiva: nuove tabelle, non tocca compliance_controls.
|
||||
--
|
||||
-- acn_requirements = catalogo globale (fonte: Allegati 1-2 ACN)
|
||||
-- org_acn_requirement_status = stato di implementazione per organizzazione
|
||||
--
|
||||
-- Idempotente. mysql -h localhost nis2_agile_db -e "source docs/sql/031_acn_requirements.sql"
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS acn_requirements (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
entity ENUM('importante','essenziale') NOT NULL,
|
||||
function_name VARCHAR(40) NOT NULL COMMENT 'Governance/Identificazione/Protezione/Rilevazione/Risposta/Ripristino',
|
||||
subcategory VARCHAR(20) NOT NULL COMMENT 'Codice sottocategoria framework, es. GV.OC-4',
|
||||
subcategory_text TEXT NULL,
|
||||
req_index INT NOT NULL COMMENT 'Numero del requisito entro la sottocategoria',
|
||||
requirement TEXT NOT NULL COMMENT 'Testo del requisito (fonte: Allegato ACN)',
|
||||
art21_letter CHAR(1) NULL COMMENT 'Mapping indicativo a Art.21.2 (a..j), opzionale',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_acn_req (entity, subcategory, req_index),
|
||||
KEY idx_acn_entity (entity),
|
||||
KEY idx_acn_func (function_name)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='Catalogo requisiti specifiche di base ACN (Determina 164179/2025, Allegati 1-2)';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS org_acn_requirement_status (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
organization_id INT NOT NULL,
|
||||
requirement_id INT NOT NULL,
|
||||
status ENUM('not_started','in_progress','implemented','not_applicable') NOT NULL DEFAULT 'not_started',
|
||||
evidence_note TEXT NULL,
|
||||
updated_by INT NULL,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_org_req (organization_id, requirement_id),
|
||||
KEY idx_oar_org (organization_id),
|
||||
KEY idx_oar_req (requirement_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='Stato di implementazione dei requisiti ACN per organizzazione';
|
||||
|
||||
-- ROLLBACK:
|
||||
-- DROP TABLE IF EXISTS org_acn_requirement_status;
|
||||
-- DROP TABLE IF EXISTS acn_requirements;
|
||||
@ -320,6 +320,8 @@ $actionMap = [
|
||||
'GET:iso27001Mapping' => 'getIsoMapping',
|
||||
'GET:nistCsfMapping' => 'getNistCsfMapping',
|
||||
'GET:controlsMonitoring' => 'controlsMonitoring',
|
||||
'GET:acnRequirements' => 'acnRequirements',
|
||||
'PUT:acnRequirements/{subId}' => 'updateAcnRequirement',
|
||||
'GET:executiveReport' => 'executiveReport',
|
||||
'GET:relevantSystemsRegister' => 'relevantSystemsRegister',
|
||||
'GET:export' => 'export',
|
||||
|
||||
25
scripts/seed_acn_requirements.php
Normal file
25
scripts/seed_acn_requirements.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
// Seeder catalogo requisiti ACN da docs/nis2/allegati_acn/acn_requirements.json (fonte: Allegati 1-2 ACN).
|
||||
// Idempotente: ON DUPLICATE KEY UPDATE sul vincolo (entity, subcategory, req_index).
|
||||
chdir(dirname(__DIR__));
|
||||
require_once 'application/config/config.php';
|
||||
require_once 'application/config/database.php';
|
||||
|
||||
$json = json_decode(file_get_contents('docs/nis2/allegati_acn/acn_requirements.json'), true);
|
||||
if (!$json) { fwrite(STDERR, "JSON non leggibile\n"); exit(1); }
|
||||
|
||||
$n = 0;
|
||||
foreach (['importante','essenziale'] as $entity) {
|
||||
foreach ($json[$entity] as $r) {
|
||||
Database::query(
|
||||
'INSERT INTO acn_requirements (entity, function_name, subcategory, subcategory_text, req_index, requirement)
|
||||
VALUES (?,?,?,?,?,?)
|
||||
ON DUPLICATE KEY UPDATE function_name=VALUES(function_name), subcategory_text=VALUES(subcategory_text), requirement=VALUES(requirement)',
|
||||
[$entity, $r['function'], $r['subcategory'], $r['subcategory_text'] ?? null, $r['req_index'], $r['requirement']]
|
||||
);
|
||||
$n++;
|
||||
}
|
||||
}
|
||||
$imp = Database::fetchOne("SELECT COUNT(*) c FROM acn_requirements WHERE entity='importante'")['c'];
|
||||
$ess = Database::fetchOne("SELECT COUNT(*) c FROM acn_requirements WHERE entity='essenziale'")['c'];
|
||||
echo "Seed completato: processati $n | DB importanti=$imp essenziali=$ess\n";
|
||||
Loading…
Reference in New Issue
Block a user