nis2-agile/application/controllers/AuditController.php
DevEnv nis2-agile 5c545ea3d0 [FEAT] Integrazione analisi docs/nis2 v1.7.0 — scoring asset, tassonomia incidenti, PIR, NIST CSF, fonti certe
Fase 1 - Asset Relevance Scoring NIS2 (GV.OC-04): metodologia 0-100 a 6 criteri,
  AssetScoringService + endpoint scoringGrid/score/relevantSystems + UI assets.html + registro stampabile.
Fase 2 - Tassonomia incidenti Determina ACN 164179/2025: IS-1..4 + regime essenziale/importante (Allegati 3/4).
Fase 3 - Post-Incident Review (5-Whys) + metriche TTD/TTC/TTR + timestamp di fase.
Fase 4 - Mapping NIST CSF 2.0 (43 controlli) reference-only.
Fonti certe: registry config/nis2_sources.php + grounding AI (vieta riferimenti inventati) +
  citazioni help.js + ingest PDF normativi nella KB RAG (scripts/ingest-nis2-sources.php).
Migrazioni 020/021/022 (additive idempotenti). Fix VectorService IP Qdrant (drift .5->.3).
Analisi concorrenza Evix (docs/EVIX_ANALISI_CONCORRENZA.html, gap-driven).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 17:15:13 +02:00

410 lines
17 KiB
PHP

<?php
/**
* NIS2 Agile - Audit Controller
*
* Controlli compliance, evidenze, audit logs, mapping ISO 27001.
*/
require_once __DIR__ . '/BaseController.php';
require_once APP_PATH . '/services/ReportService.php';
class AuditController extends BaseController
{
public function listControls(): void
{
$this->requireOrgAccess();
$controls = Database::fetchAll(
'SELECT cc.*, u.full_name as responsible_name
FROM compliance_controls cc
LEFT JOIN users u ON u.id = cc.responsible_user_id
WHERE cc.organization_id = ?
ORDER BY cc.control_code',
[$this->getCurrentOrgId()]
);
$this->jsonSuccess($controls);
}
public function updateControl(int $id): void
{
$this->requireOrgRole(['org_admin', 'compliance_manager', 'auditor']);
$updates = [];
foreach (['status', 'implementation_percentage', 'evidence_description', 'responsible_user_id', 'next_review_date'] as $field) {
if ($this->hasParam($field)) {
$updates[$field] = $this->getParam($field);
}
}
if (isset($updates['status']) && $updates['status'] === 'verified') {
$updates['last_verified_at'] = date('Y-m-d H:i:s');
}
if (!empty($updates)) {
Database::update('compliance_controls', $updates, 'id = ? AND organization_id = ?', [$id, $this->getCurrentOrgId()]);
$this->logAudit('control_updated', 'compliance_control', $id, $updates);
}
$this->jsonSuccess($updates, 'Controllo aggiornato');
}
public function uploadEvidence(): void
{
$this->requireOrgRole(['org_admin', 'compliance_manager', 'auditor']);
if (!isset($_FILES['file'])) {
$this->jsonError('File non fornito', 400, 'NO_FILE');
}
$file = $_FILES['file'];
$maxSize = 10 * 1024 * 1024; // 10MB
if ($file['size'] > $maxSize) {
$this->jsonError('File troppo grande (max 10MB)', 400, 'FILE_TOO_LARGE');
}
$orgId = $this->getCurrentOrgId();
$uploadDir = UPLOAD_PATH . "/evidence/{$orgId}";
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$filename = uniqid('ev_') . '.' . $ext;
$filePath = $uploadDir . '/' . $filename;
if (!move_uploaded_file($file['tmp_name'], $filePath)) {
$this->jsonError('Errore caricamento file', 500, 'UPLOAD_ERROR');
}
$evidenceId = Database::insert('evidence_files', [
'organization_id' => $orgId,
'control_id' => $this->getParam('control_id'),
'entity_type' => $this->getParam('entity_type'),
'entity_id' => $this->getParam('entity_id'),
'file_name' => $file['name'],
'file_path' => "evidence/{$orgId}/{$filename}",
'file_size' => $file['size'],
'mime_type' => $file['type'],
'uploaded_by' => $this->getCurrentUserId(),
]);
$this->logAudit('evidence_uploaded', 'evidence', $evidenceId);
$this->jsonSuccess(['id' => $evidenceId], 'Evidenza caricata', 201);
}
public function listEvidence(): void
{
$this->requireOrgAccess();
$where = 'organization_id = ?';
$params = [$this->getCurrentOrgId()];
if ($this->hasParam('control_id')) {
$where .= ' AND control_id = ?';
$params[] = $this->getParam('control_id');
}
if ($this->hasParam('entity_type') && $this->hasParam('entity_id')) {
$where .= ' AND entity_type = ? AND entity_id = ?';
$params[] = $this->getParam('entity_type');
$params[] = $this->getParam('entity_id');
}
$evidence = Database::fetchAll(
"SELECT ef.*, u.full_name as uploaded_by_name
FROM evidence_files ef
LEFT JOIN users u ON u.id = ef.uploaded_by
WHERE ef.{$where}
ORDER BY ef.created_at DESC",
$params
);
$this->jsonSuccess($evidence);
}
public function generateReport(): void
{
$this->requireOrgAccess();
$orgId = $this->getCurrentOrgId();
$org = Database::fetchOne('SELECT * FROM organizations WHERE id = ?', [$orgId]);
$controls = Database::fetchAll('SELECT * FROM compliance_controls WHERE organization_id = ? ORDER BY control_code', [$orgId]);
$lastAssessment = Database::fetchOne('SELECT * FROM assessments WHERE organization_id = ? AND status = "completed" ORDER BY completed_at DESC LIMIT 1', [$orgId]);
$riskCount = Database::count('risks', 'organization_id = ? AND status != "closed"', [$orgId]);
$incidentCount = Database::count('incidents', 'organization_id = ?', [$orgId]);
$policyCount = Database::count('policies', 'organization_id = ? AND status IN ("approved","published")', [$orgId]);
$totalControls = count($controls);
$implemented = count(array_filter($controls, fn($c) => in_array($c['status'], ['implemented', 'verified'])));
$this->jsonSuccess([
'organization' => $org,
'report_date' => date('Y-m-d H:i:s'),
'compliance_summary' => [
'total_controls' => $totalControls,
'implemented_controls' => $implemented,
'compliance_percentage' => $totalControls > 0 ? round($implemented / $totalControls * 100) : 0,
],
'controls' => $controls,
'last_assessment' => $lastAssessment,
'risk_count' => $riskCount,
'incident_count' => $incidentCount,
'policy_count' => $policyCount,
]);
}
public function getAuditLogs(): void
{
$this->requireOrgRole(['org_admin', 'auditor']);
$pagination = $this->getPagination(50);
$total = Database::count('audit_logs', 'organization_id = ?', [$this->getCurrentOrgId()]);
$logs = Database::fetchAll(
"SELECT al.*, u.full_name
FROM audit_logs al
LEFT JOIN users u ON u.id = al.user_id
WHERE al.organization_id = ?
ORDER BY al.created_at DESC
LIMIT {$pagination['per_page']} OFFSET {$pagination['offset']}",
[$this->getCurrentOrgId()]
);
$this->jsonPaginated($logs, $total, $pagination['page'], $pagination['per_page']);
}
public function getIsoMapping(): void
{
$this->requireOrgAccess();
$mapping = [
['nis2' => '21.2.a', 'iso27001' => 'A.5.1, A.5.2, A.8.1, A.8.2', 'title' => 'Risk analysis and security policies'],
['nis2' => '21.2.b', 'iso27001' => 'A.5.24, A.5.25, A.5.26, A.6.8', 'title' => 'Incident handling'],
['nis2' => '21.2.c', 'iso27001' => 'A.5.29, A.5.30', 'title' => 'Business continuity'],
['nis2' => '21.2.d', 'iso27001' => 'A.5.19, A.5.20, A.5.21, A.5.22', 'title' => 'Supply chain security'],
['nis2' => '21.2.e', 'iso27001' => 'A.8.25, A.8.26, A.8.27, A.8.28', 'title' => 'System acquisition and development'],
['nis2' => '21.2.f', 'iso27001' => 'A.5.35, A.5.36', 'title' => 'Effectiveness assessment'],
['nis2' => '21.2.g', 'iso27001' => 'A.6.3, A.6.6', 'title' => 'Cyber hygiene and training'],
['nis2' => '21.2.h', 'iso27001' => 'A.8.24', 'title' => 'Cryptography and encryption'],
['nis2' => '21.2.i', 'iso27001' => 'A.5.15, A.5.16, A.5.17, A.5.18, A.6.1, A.6.2', 'title' => 'HR security, access control, asset management'],
['nis2' => '21.2.j', 'iso27001' => 'A.8.5', 'title' => 'Multi-factor authentication'],
];
$this->jsonSuccess($mapping);
}
/**
* GET /api/audit/nistCsfMapping
* Layer di mapping NIST CSF 2.0 (43 controlli) -> NIS2 Art.21 / D.Lgs.138/2024 -> modulo piattaforma.
* Reference-only (nessuna persistenza): arricchisce l'assessment Art.21 con i codici controllo
* NIST CSF 2.0 usati come standard de-facto. Fonte mapping: NIST CSF 2.0 + Direttiva (UE) 2022/2555.
*/
public function getNistCsfMapping(): void
{
$this->requireOrgAccess();
// [code, function, nis2, module]
$rows = [
// GOVERN
['GV.OC-04', 'Govern', '21.1', 'Asset - Sistemi rilevanti (GV.OC-04)'],
['GV.RM-03', 'Govern', '21.2.a', 'Risk Management'],
['GV.RR-02', 'Govern', '20', 'Organizzazione - Ruoli e responsabilita'],
['GV.RR-04', 'Govern', '20', 'Organizzazione - Risorse cybersecurity'],
['GV.PO-01', 'Govern', '21.2.a', 'Policy - Politica di sicurezza'],
['GV.PO-02', 'Govern', '21.2.a', 'Policy - Revisione politiche'],
['GV.SC-01', 'Govern', '21.2.d', 'Supply Chain - Strategia'],
['GV.SC-02', 'Govern', '21.2.d', 'Supply Chain - Ruoli fornitori'],
['GV.SC-04', 'Govern', '21.2.d', 'Supply Chain - Valutazione fornitori'],
['GV.SC-05', 'Govern', '21.2.d', 'Supply Chain - Requisiti contrattuali'],
['GV.SC-07', 'Govern', '21.2.d', 'Supply Chain - Monitoraggio rischio fornitori'],
// IDENTIFY
['ID.AM-01', 'Identify', '21.2.i', 'Asset - Inventario hardware'],
['ID.AM-02', 'Identify', '21.2.i', 'Asset - Inventario software'],
['ID.AM-03', 'Identify', '21.2.i', 'Asset - Diagrammi flussi/rete (essenziali)'],
['ID.AM-04', 'Identify', '21.2.i', 'Asset - Catalogo servizi'],
['ID.RA-01', 'Identify', '21.2.a', 'Risk Management - Vulnerabilita'],
['ID.RA-05', 'Identify', '21.2.a', 'Risk Management - Valutazione rischio'],
['ID.RA-06', 'Identify', '21.2.a', 'Risk Management - Trattamento rischio'],
['ID.RA-08', 'Identify', '21.2.e', 'Risk Management - Gestione vulnerabilita/disclosure'],
['ID.IM-01', 'Identify', '21.2.f', 'Audit - Miglioramento da valutazioni'],
['ID.IM-04', 'Identify', '21.2.c', 'Incidenti - Piani BC/DR e test'],
// PROTECT
['PR.AA-01', 'Protect', '21.2.i', 'Asset/Access - Gestione identita'],
['PR.AA-03', 'Protect', '21.2.i', 'Access - Autenticazione'],
['PR.AA-05', 'Protect', '21.2.i', 'Access - Privilegi e accessi'],
['PR.AA-06', 'Protect', '21.2.i', 'Access - Accesso fisico'],
['PR.AT-01', 'Protect', '21.2.g', 'Training - Awareness'],
['PR.AT-02', 'Protect', '21.2.g', 'Training - Ruoli privilegiati'],
['PR.DS-01', 'Protect', '21.2.h', 'Policy - Protezione dati a riposo'],
['PR.DS-02', 'Protect', '21.2.h', 'Policy - Protezione dati in transito'],
['PR.DS-11', 'Protect', '21.2.c', 'Incidenti - Backup'],
['PR.PS-01', 'Protect', '21.2.e', 'Policy - Configurazione sicura'],
['PR.PS-02', 'Protect', '21.2.e', 'Asset - Gestione software'],
['PR.PS-03', 'Protect', '21.2.e', 'Asset - Gestione hardware'],
['PR.PS-04', 'Protect', '21.2.b', 'Audit - Log generation'],
['PR.PS-06', 'Protect', '21.2.e', 'Policy - Secure development lifecycle'],
['PR.IR-01', 'Protect', '21.2.i', 'Asset - Protezione reti'],
['PR.IR-03', 'Protect', '21.2.c', 'Incidenti - Resilienza/ridondanza'],
// DETECT
['DE.CM-01', 'Detect', '21.2.b', 'Incidenti - Monitoraggio reti'],
['DE.CM-09', 'Detect', '21.2.b', 'Incidenti - Monitoraggio asset/sistemi'],
// RESPOND / RECOVER
['RS.MA-01', 'Respond', '21.2.b / 23', 'Incidenti - Gestione incidenti'],
['RS.CO-02', 'Respond', '23', 'Incidenti - Notifica CSIRT'],
['RC.RP-01', 'Recover', '21.2.c', 'Incidenti - Piano di ripristino'],
['RC.CO-03', 'Recover', '21.2.c', 'Incidenti - Post-Incident Review'],
];
$mapping = array_map(fn($r) => [
'csf_code' => $r[0],
'function' => $r[1],
'nis2_art' => $r[2],
'module' => $r[3],
], $rows);
$this->jsonSuccess([
'mapping' => $mapping,
'count' => count($mapping),
'functions' => ['Govern', 'Identify', 'Protect', 'Detect', 'Respond', 'Recover'],
'source' => 'NIST Cybersecurity Framework 2.0 + Direttiva (UE) 2022/2555 (NIS2) Art.20-21-23 / D.Lgs. 138/2024',
]);
}
/**
* GET /api/audit/executive-report
* Genera report esecutivo HTML (stampabile come PDF)
*/
public function executiveReport(): void
{
$this->requireOrgRole(['org_admin', 'compliance_manager', 'board_member']);
$reportService = new ReportService();
$html = $reportService->generateExecutiveReport($this->getCurrentOrgId());
header('Content-Type: text/html; charset=utf-8');
echo $html;
exit;
}
/**
* GET /api/audit/relevantSystemsRegister
* Registro formale "Sistemi Rilevanti NIS2" (GV.OC-04), HTML stampabile.
*/
public function relevantSystemsRegister(): void
{
$this->requireOrgRole(['org_admin', 'compliance_manager', 'board_member', 'auditor']);
$reportService = new ReportService();
$html = $reportService->generateRelevantSystemsRegister($this->getCurrentOrgId());
header('Content-Type: text/html; charset=utf-8');
echo $html;
exit;
}
/**
* GET /api/audit/export/{type}
* Esporta dati in CSV
*/
public function export(int $type = 0): void
{
$this->requireOrgRole(['org_admin', 'compliance_manager', 'auditor']);
$exportType = $_GET['type'] ?? 'controls';
$orgId = $this->getCurrentOrgId();
$reportService = new ReportService();
$csv = match ($exportType) {
'risks' => $reportService->exportRisksCSV($orgId),
'incidents' => $reportService->exportIncidentsCSV($orgId),
'controls' => $reportService->exportControlsCSV($orgId),
'assets' => $reportService->exportAssetsCSV($orgId),
default => $reportService->exportControlsCSV($orgId),
};
$filename = "nis2_{$exportType}_" . date('Y-m-d') . '.csv';
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="' . $filename . '"');
echo $csv;
exit;
}
/**
* GET /api/audit/chain-verify
* Verifica l'integrità dell'hash chain per l'organizzazione corrente.
* Risponde con: valid, total, hashed, coverage_pct, broken_at, last_hash
*/
public function chainVerify(): void
{
$this->requireOrgRole(['org_admin', 'auditor']);
$orgId = $this->getCurrentOrgId();
$db = Database::getInstance();
$result = AuditService::verifyChain($db, $orgId);
// Se la catena è rotta, registra la violazione
if (!$result['valid'] && $result['broken_at'] !== null) {
$performer = $this->currentUser['email'] ?? 'system';
try {
$ins = $db->prepare(
"INSERT INTO audit_violations
(organization_id, detected_by, broken_at_id, chain_length, notes)
VALUES (:org, :by, :bid, :len, :notes)"
);
$ins->execute([
'org' => $orgId,
'by' => $performer,
'bid' => $result['broken_at'],
'len' => $result['total'],
'notes' => 'Violazione rilevata tramite API chain-verify',
]);
} catch (Throwable $e) {
error_log('[AuditController] chain violation log error: ' . $e->getMessage());
}
$this->logAudit('audit.chain_broken', 'audit_logs', $result['broken_at'], [
'total' => $result['total'],
'coverage_pct' => $result['coverage_pct'],
]);
}
$this->jsonSuccess($result);
}
/**
* GET /api/audit/export-certified
* Genera un export JSON certificato con hash SHA-256 dell'intero contenuto.
* Adatto per ispezioni ACN, audit NIS2 Art.32, certificazione ISO 27001.
*/
public function exportCertified(): void
{
$this->requireOrgRole(['org_admin', 'auditor']);
$orgId = $this->getCurrentOrgId();
$userId = $this->getCurrentUserId();
$email = $this->currentUser['email'] ?? 'system';
$db = Database::getInstance();
$result = AuditService::exportCertified(
$db,
$orgId,
$userId,
$email,
$_GET['purpose'] ?? 'export_certificato',
$_GET['from'] ?? null,
$_GET['to'] ?? null
);
$this->logAudit('audit.export_certified', 'audit_logs', null, [
'records_count' => $result['records_count'],
'chain_valid' => $result['chain_valid'],
'export_hash' => $result['export_hash'],
]);
header('Content-Disposition: attachment; filename="nis2_audit_certified_' . date('Y-m-d') . '.json"');
$this->jsonSuccess($result);
}
}