Complete MVP implementation including: - PHP 8.4 backend with Front Controller pattern (80+ API endpoints) - Multi-tenant architecture with organization_id isolation - JWT authentication (HS256, 2h access + 7d refresh tokens) - 14 controllers: Auth, Organization, Assessment, Dashboard, Risk, Incident, Policy, SupplyChain, Training, Asset, Audit, Admin - AI Service integration (Anthropic Claude API) for gap analysis, risk suggestions, policy generation, incident classification - NIS2 gap analysis questionnaire (~80 questions, 10 categories) - MySQL schema (20 tables) with NIS2 Art. 21 compliance controls - NIS2 Art. 23 incident reporting workflow (24h/72h/30d) - Frontend: login, register, dashboard, assessment wizard, org setup - Docker configuration (PHP-FPM + Nginx + MySQL) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
385 lines
16 KiB
PHP
385 lines
16 KiB
PHP
<?php
|
|
/**
|
|
* NIS2 Agile - Front Controller / Router
|
|
*
|
|
* Tutte le richieste API passano da qui.
|
|
* URL Pattern: /nis2/api/{controller}/{action}/{id?}
|
|
*/
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// BOOTSTRAP
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
require_once __DIR__ . '/../application/config/config.php';
|
|
require_once __DIR__ . '/../application/config/database.php';
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// CORS HEADERS
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
|
|
|
if (in_array($origin, CORS_ALLOWED_ORIGINS)) {
|
|
header("Access-Control-Allow-Origin: {$origin}");
|
|
} elseif (APP_DEBUG) {
|
|
header("Access-Control-Allow-Origin: *");
|
|
}
|
|
|
|
header("Access-Control-Allow-Methods: " . CORS_ALLOWED_METHODS);
|
|
header("Access-Control-Allow-Headers: " . CORS_ALLOWED_HEADERS);
|
|
header("Access-Control-Max-Age: " . CORS_MAX_AGE);
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
|
http_response_code(204);
|
|
exit;
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// ROUTING
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
$requestUri = $_SERVER['REQUEST_URI'] ?? '/';
|
|
$basePath = '/nis2';
|
|
|
|
$path = parse_url($requestUri, PHP_URL_PATH);
|
|
$path = preg_replace("#^{$basePath}#", '', $path);
|
|
$path = trim($path, '/');
|
|
|
|
// Se non è una richiesta API, servi file statici o index.html
|
|
if (!preg_match('#^api/#', $path)) {
|
|
$staticFile = __DIR__ . '/' . $path;
|
|
if ($path && file_exists($staticFile) && is_file($staticFile)) {
|
|
return false;
|
|
}
|
|
|
|
if (empty($path) || $path === 'index.html') {
|
|
include __DIR__ . '/index.html';
|
|
exit;
|
|
}
|
|
|
|
// Prova a servire come HTML page
|
|
$htmlFile = __DIR__ . '/' . $path;
|
|
if (!str_ends_with($path, '.html')) {
|
|
$htmlFile = __DIR__ . '/' . $path . '.html';
|
|
}
|
|
if (file_exists($htmlFile) && is_file($htmlFile)) {
|
|
include $htmlFile;
|
|
exit;
|
|
}
|
|
|
|
http_response_code(404);
|
|
echo json_encode(['success' => false, 'message' => 'Not Found']);
|
|
exit;
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// API ROUTING
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
$parts = explode('/', $path);
|
|
array_shift($parts); // Rimuovi "api"
|
|
|
|
$controllerName = $parts[0] ?? 'index';
|
|
$actionName = $parts[1] ?? 'index';
|
|
$resourceId = isset($parts[2]) ? $parts[2] : null;
|
|
$subAction = $parts[3] ?? null;
|
|
$subResourceId = isset($parts[4]) ? $parts[4] : null;
|
|
|
|
// Mappa controller
|
|
$controllerMap = [
|
|
'auth' => 'AuthController',
|
|
'organizations' => 'OrganizationController',
|
|
'assessments' => 'AssessmentController',
|
|
'dashboard' => 'DashboardController',
|
|
'risks' => 'RiskController',
|
|
'incidents' => 'IncidentController',
|
|
'policies' => 'PolicyController',
|
|
'supply-chain' => 'SupplyChainController',
|
|
'training' => 'TrainingController',
|
|
'assets' => 'AssetController',
|
|
'audit' => 'AuditController',
|
|
'admin' => 'AdminController',
|
|
];
|
|
|
|
if (!isset($controllerMap[$controllerName])) {
|
|
http_response_code(404);
|
|
header('Content-Type: application/json');
|
|
echo json_encode([
|
|
'success' => false,
|
|
'message' => 'Endpoint non trovato',
|
|
'error_code' => 'NOT_FOUND',
|
|
]);
|
|
exit;
|
|
}
|
|
|
|
$controllerClass = $controllerMap[$controllerName];
|
|
$controllerFile = APP_PATH . "/controllers/{$controllerClass}.php";
|
|
|
|
if (!file_exists($controllerFile)) {
|
|
http_response_code(500);
|
|
header('Content-Type: application/json');
|
|
echo json_encode([
|
|
'success' => false,
|
|
'message' => 'Controller non disponibile',
|
|
]);
|
|
exit;
|
|
}
|
|
|
|
require_once $controllerFile;
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// MAPPA AZIONI PER METODO HTTP
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
$method = $_SERVER['REQUEST_METHOD'];
|
|
|
|
// Converti action name con trattini in camelCase
|
|
$actionName = str_replace('-', '', lcfirst(ucwords($actionName, '-')));
|
|
|
|
$actionMap = [
|
|
// ── AuthController ──────────────────────────────
|
|
'auth' => [
|
|
'POST:register' => 'register',
|
|
'POST:login' => 'login',
|
|
'POST:logout' => 'logout',
|
|
'POST:refresh' => 'refresh',
|
|
'GET:me' => 'me',
|
|
'PUT:profile' => 'updateProfile',
|
|
'POST:changePassword' => 'changePassword',
|
|
],
|
|
|
|
// ── OrganizationController ──────────────────────
|
|
'organizations' => [
|
|
'POST:create' => 'create',
|
|
'GET:current' => 'getCurrent',
|
|
'GET:list' => 'list',
|
|
'PUT:{id}' => 'update',
|
|
'GET:{id}/members' => 'listMembers',
|
|
'POST:{id}/invite' => 'inviteMember',
|
|
'DELETE:{id}/members/{subId}' => 'removeMember',
|
|
'POST:classify' => 'classifyEntity',
|
|
],
|
|
|
|
// ── AssessmentController ────────────────────────
|
|
'assessments' => [
|
|
'GET:list' => 'list',
|
|
'POST:create' => 'create',
|
|
'GET:{id}' => 'get',
|
|
'PUT:{id}' => 'update',
|
|
'GET:{id}/questions' => 'getQuestions',
|
|
'POST:{id}/respond' => 'saveResponse',
|
|
'POST:{id}/complete' => 'complete',
|
|
'GET:{id}/report' => 'getReport',
|
|
'POST:{id}/aiAnalyze' => 'aiAnalyze',
|
|
],
|
|
|
|
// ── DashboardController ─────────────────────────
|
|
'dashboard' => [
|
|
'GET:overview' => 'overview',
|
|
'GET:complianceScore' => 'complianceScore',
|
|
'GET:upcomingDeadlines' => 'deadlines',
|
|
'GET:recentActivity' => 'recentActivity',
|
|
'GET:riskHeatmap' => 'riskHeatmap',
|
|
],
|
|
|
|
// ── RiskController ──────────────────────────────
|
|
'risks' => [
|
|
'GET:list' => 'list',
|
|
'POST:create' => 'create',
|
|
'GET:{id}' => 'get',
|
|
'PUT:{id}' => 'update',
|
|
'DELETE:{id}' => 'delete',
|
|
'POST:{id}/treatments' => 'addTreatment',
|
|
'PUT:treatments/{subId}' => 'updateTreatment',
|
|
'GET:matrix' => 'getRiskMatrix',
|
|
'POST:aiSuggest' => 'aiSuggestRisks',
|
|
],
|
|
|
|
// ── IncidentController ──────────────────────────
|
|
'incidents' => [
|
|
'GET:list' => 'list',
|
|
'POST:create' => 'create',
|
|
'GET:{id}' => 'get',
|
|
'PUT:{id}' => 'update',
|
|
'POST:{id}/timeline' => 'addTimelineEvent',
|
|
'POST:{id}/earlyWarning' => 'sendEarlyWarning',
|
|
'POST:{id}/notification' => 'sendNotification',
|
|
'POST:{id}/finalReport' => 'sendFinalReport',
|
|
'POST:{id}/aiClassify' => 'aiClassify',
|
|
],
|
|
|
|
// ── PolicyController ────────────────────────────
|
|
'policies' => [
|
|
'GET:list' => 'list',
|
|
'POST:create' => 'create',
|
|
'GET:{id}' => 'get',
|
|
'PUT:{id}' => 'update',
|
|
'DELETE:{id}' => 'delete',
|
|
'POST:{id}/approve' => 'approve',
|
|
'POST:aiGenerate' => 'aiGeneratePolicy',
|
|
'GET:templates' => 'getTemplates',
|
|
],
|
|
|
|
// ── SupplyChainController ───────────────────────
|
|
'supply-chain' => [
|
|
'GET:list' => 'list',
|
|
'POST:create' => 'create',
|
|
'GET:{id}' => 'get',
|
|
'PUT:{id}' => 'update',
|
|
'DELETE:{id}' => 'delete',
|
|
'POST:{id}/assess' => 'assessSupplier',
|
|
'GET:riskOverview' => 'riskOverview',
|
|
],
|
|
|
|
// ── TrainingController ──────────────────────────
|
|
'training' => [
|
|
'GET:courses' => 'listCourses',
|
|
'POST:courses' => 'createCourse',
|
|
'GET:assignments' => 'myAssignments',
|
|
'POST:assign' => 'assignCourse',
|
|
'PUT:assignments/{subId}' => 'updateAssignment',
|
|
'GET:complianceStatus' => 'complianceStatus',
|
|
],
|
|
|
|
// ── AssetController ─────────────────────────────
|
|
'assets' => [
|
|
'GET:list' => 'list',
|
|
'POST:create' => 'create',
|
|
'GET:{id}' => 'get',
|
|
'PUT:{id}' => 'update',
|
|
'DELETE:{id}' => 'delete',
|
|
'GET:dependencyMap' => 'dependencyMap',
|
|
],
|
|
|
|
// ── AuditController ─────────────────────────────
|
|
'audit' => [
|
|
'GET:controls' => 'listControls',
|
|
'PUT:controls/{subId}' => 'updateControl',
|
|
'POST:evidence/upload' => 'uploadEvidence',
|
|
'GET:evidence/list' => 'listEvidence',
|
|
'GET:report' => 'generateReport',
|
|
'GET:logs' => 'getAuditLogs',
|
|
'GET:iso27001Mapping' => 'getIsoMapping',
|
|
],
|
|
|
|
// ── AdminController ─────────────────────────────
|
|
'admin' => [
|
|
'GET:organizations' => 'listOrganizations',
|
|
'GET:users' => 'listUsers',
|
|
'GET:stats' => 'platformStats',
|
|
],
|
|
];
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// RISOLUZIONE AZIONE
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
$actions = $actionMap[$controllerName] ?? [];
|
|
$resolvedAction = null;
|
|
|
|
// Costruisci combinazioni di pattern da verificare (ordine di specificità)
|
|
$patterns = [];
|
|
|
|
if ($subResourceId !== null && $subAction !== null) {
|
|
// METHOD:action/{id}/subAction/{subId}
|
|
$patterns[] = "{$method}:{$actionName}/{$subAction}/{subId}";
|
|
// METHOD:{id}/subAction/{subId}
|
|
$patterns[] = "{$method}:{id}/{$subAction}/{subId}";
|
|
}
|
|
|
|
if ($subAction !== null && $resourceId !== null) {
|
|
// METHOD:{id}/subAction
|
|
$patterns[] = "{$method}:{id}/{$subAction}";
|
|
// METHOD:action/{subId}
|
|
$patterns[] = "{$method}:{$actionName}/{subId}";
|
|
}
|
|
|
|
if ($resourceId !== null && $subAction === null) {
|
|
// METHOD:action/{id} (actionName è in realtà l'ID numerico)
|
|
if (is_numeric($actionName)) {
|
|
$patterns[] = "{$method}:{id}";
|
|
$resourceId = (int) $actionName;
|
|
} else {
|
|
// METHOD:{id}
|
|
$patterns[] = "{$method}:{id}";
|
|
}
|
|
}
|
|
|
|
// METHOD:action/subAction
|
|
if ($resourceId !== null && !is_numeric($actionName)) {
|
|
$patterns[] = "{$method}:{$actionName}/{$resourceId}";
|
|
}
|
|
|
|
// METHOD:action
|
|
$patterns[] = "{$method}:{$actionName}";
|
|
|
|
// Cerca match
|
|
foreach ($patterns as $pattern) {
|
|
if (isset($actions[$pattern])) {
|
|
$resolvedAction = $actions[$pattern];
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Se il primo segmento è numerico, l'azione è basata sull'ID
|
|
if (!$resolvedAction && is_numeric($actionName)) {
|
|
$resourceId = (int) $actionName;
|
|
$pattern = "{$method}:{id}";
|
|
if (isset($actions[$pattern])) {
|
|
$resolvedAction = $actions[$pattern];
|
|
}
|
|
}
|
|
|
|
if (!$resolvedAction) {
|
|
http_response_code(404);
|
|
header('Content-Type: application/json');
|
|
echo json_encode([
|
|
'success' => false,
|
|
'message' => "Azione non trovata: {$method} /{$controllerName}/{$actionName}",
|
|
'error_code' => 'ACTION_NOT_FOUND',
|
|
]);
|
|
exit;
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// ESECUZIONE
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
try {
|
|
$controller = new $controllerClass();
|
|
|
|
if (!method_exists($controller, $resolvedAction)) {
|
|
http_response_code(501);
|
|
header('Content-Type: application/json');
|
|
echo json_encode([
|
|
'success' => false,
|
|
'message' => "Metodo '{$resolvedAction}' non implementato",
|
|
]);
|
|
exit;
|
|
}
|
|
|
|
// Chiama con gli argomenti appropriati
|
|
if ($subResourceId !== null) {
|
|
$controller->$resolvedAction((int) $resourceId, (int) $subResourceId);
|
|
} elseif ($resourceId !== null && !is_numeric($actionName)) {
|
|
$controller->$resolvedAction((int) $resourceId);
|
|
} elseif (is_numeric($actionName)) {
|
|
$controller->$resolvedAction((int) $resourceId);
|
|
} else {
|
|
$controller->$resolvedAction();
|
|
}
|
|
} catch (PDOException $e) {
|
|
error_log('[DB_ERROR] ' . $e->getMessage());
|
|
http_response_code(500);
|
|
header('Content-Type: application/json');
|
|
echo json_encode([
|
|
'success' => false,
|
|
'message' => APP_DEBUG ? 'Errore database: ' . $e->getMessage() : 'Errore interno del server',
|
|
]);
|
|
} catch (Throwable $e) {
|
|
error_log('[ERROR] ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine());
|
|
http_response_code(500);
|
|
header('Content-Type: application/json');
|
|
echo json_encode([
|
|
'success' => false,
|
|
'message' => APP_DEBUG ? $e->getMessage() : 'Errore interno del server',
|
|
]);
|
|
}
|