[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:
DevEnv nis2-agile 2026-05-31 08:07:38 +02:00
parent 6671d51969
commit 59205d05fb
5 changed files with 1800 additions and 0 deletions

View File

@ -470,4 +470,98 @@ class AuditController extends BaseController
'recent_evidence' => $recent, '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');
}
} }

File diff suppressed because it is too large Load Diff

View 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;

View File

@ -320,6 +320,8 @@ $actionMap = [
'GET:iso27001Mapping' => 'getIsoMapping', 'GET:iso27001Mapping' => 'getIsoMapping',
'GET:nistCsfMapping' => 'getNistCsfMapping', 'GET:nistCsfMapping' => 'getNistCsfMapping',
'GET:controlsMonitoring' => 'controlsMonitoring', 'GET:controlsMonitoring' => 'controlsMonitoring',
'GET:acnRequirements' => 'acnRequirements',
'PUT:acnRequirements/{subId}' => 'updateAcnRequirement',
'GET:executiveReport' => 'executiveReport', 'GET:executiveReport' => 'executiveReport',
'GET:relevantSystemsRegister' => 'relevantSystemsRegister', 'GET:relevantSystemsRegister' => 'relevantSystemsRegister',
'GET:export' => 'export', 'GET:export' => 'export',

View 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";