[CORE] Initial project scaffold - NIS2 Agile Compliance Platform
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>
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
/**
|
||||
* NIS2 Agile - Configurazione Principale
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/env.php';
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// APPLICAZIONE
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
define('APP_ENV', Env::get('APP_ENV', 'development'));
|
||||
define('APP_DEBUG', APP_ENV === 'development');
|
||||
define('APP_NAME', Env::get('APP_NAME', 'NIS2 Agile'));
|
||||
define('APP_VERSION', Env::get('APP_VERSION', '1.0.0'));
|
||||
define('APP_URL', Env::get('APP_URL', 'http://localhost:8080'));
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// PERCORSI
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
define('BASE_PATH', dirname(dirname(__DIR__)));
|
||||
define('APP_PATH', BASE_PATH . '/application');
|
||||
define('PUBLIC_PATH', BASE_PATH . '/public');
|
||||
define('UPLOAD_PATH', PUBLIC_PATH . '/uploads');
|
||||
define('DATA_PATH', APP_PATH . '/data');
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// AUTENTICAZIONE JWT
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
define('JWT_SECRET', APP_ENV === 'production'
|
||||
? Env::getRequired('JWT_SECRET')
|
||||
: Env::get('JWT_SECRET', 'nis2_dev_jwt_secret'));
|
||||
define('JWT_ALGORITHM', 'HS256');
|
||||
define('JWT_EXPIRES_IN', Env::int('JWT_EXPIRES_IN', 7200));
|
||||
define('JWT_REFRESH_EXPIRES_IN', Env::int('JWT_REFRESH_EXPIRES_IN', 604800));
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// PASSWORD POLICY
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
define('PASSWORD_MIN_LENGTH', Env::int('PASSWORD_MIN_LENGTH', 8));
|
||||
define('PASSWORD_REQUIRE_UPPERCASE', Env::bool('PASSWORD_REQUIRE_UPPERCASE', true));
|
||||
define('PASSWORD_REQUIRE_NUMBER', Env::bool('PASSWORD_REQUIRE_NUMBER', true));
|
||||
define('PASSWORD_REQUIRE_SPECIAL', Env::bool('PASSWORD_REQUIRE_SPECIAL', true));
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// CORS
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
define('CORS_ALLOWED_ORIGINS', array_filter([
|
||||
APP_URL,
|
||||
APP_ENV !== 'production' ? 'http://localhost:8080' : null,
|
||||
APP_ENV !== 'production' ? 'http://localhost:3000' : null,
|
||||
]));
|
||||
define('CORS_ALLOWED_METHODS', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
define('CORS_ALLOWED_HEADERS', 'Content-Type, Authorization, X-Organization-Id, X-Requested-With');
|
||||
define('CORS_MAX_AGE', '86400');
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// RATE LIMITING
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
define('RATE_LIMIT_AUTH_LOGIN', [
|
||||
['max' => 5, 'window_seconds' => 60],
|
||||
['max' => 20, 'window_seconds' => 3600],
|
||||
]);
|
||||
define('RATE_LIMIT_AUTH_REGISTER', [
|
||||
['max' => 3, 'window_seconds' => 600],
|
||||
]);
|
||||
define('RATE_LIMIT_AI', [
|
||||
['max' => 10, 'window_seconds' => 60],
|
||||
['max' => 100, 'window_seconds' => 3600],
|
||||
]);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// AI (ANTHROPIC)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
define('ANTHROPIC_API_KEY', Env::get('ANTHROPIC_API_KEY', ''));
|
||||
define('ANTHROPIC_MODEL', Env::get('ANTHROPIC_MODEL', 'claude-sonnet-4-5-20250929'));
|
||||
define('ANTHROPIC_MAX_TOKENS', Env::int('ANTHROPIC_MAX_TOKENS', 4096));
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// TIMEZONE
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
date_default_timezone_set('Europe/Rome');
|
||||
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
/**
|
||||
* NIS2 Agile - Configurazione Database
|
||||
*
|
||||
* Database dedicato: nis2_agile_db
|
||||
* Utente dedicato: nis2_user
|
||||
* COMPLETAMENTE ISOLATO dagli altri applicativi
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/env.php';
|
||||
|
||||
define('DB_HOST', Env::get('DB_HOST', 'localhost'));
|
||||
define('DB_PORT', Env::get('DB_PORT', '3306'));
|
||||
define('DB_NAME', Env::get('DB_NAME', 'nis2_agile_db'));
|
||||
define('DB_USER', Env::get('DB_USER', 'nis2_user'));
|
||||
define('DB_PASS', Env::getRequired('DB_PASS'));
|
||||
define('DB_CHARSET', 'utf8mb4');
|
||||
|
||||
class Database
|
||||
{
|
||||
private static ?PDO $instance = null;
|
||||
|
||||
/**
|
||||
* Ottiene l'istanza PDO (singleton)
|
||||
*/
|
||||
public static function getInstance(): PDO
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
$dsn = sprintf(
|
||||
'mysql:host=%s;port=%s;dbname=%s;charset=%s',
|
||||
DB_HOST,
|
||||
DB_PORT,
|
||||
DB_NAME,
|
||||
DB_CHARSET
|
||||
);
|
||||
|
||||
$options = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci",
|
||||
];
|
||||
|
||||
try {
|
||||
self::$instance = new PDO($dsn, DB_USER, DB_PASS, $options);
|
||||
} catch (PDOException $e) {
|
||||
if (defined('APP_DEBUG') && APP_DEBUG) {
|
||||
throw new Exception('Database connection failed: ' . $e->getMessage());
|
||||
} else {
|
||||
throw new Exception('Database connection failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Esegue una query con parametri
|
||||
*/
|
||||
public static function query(string $sql, array $params = []): PDOStatement
|
||||
{
|
||||
$stmt = self::getInstance()->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
return $stmt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ottiene una singola riga
|
||||
*/
|
||||
public static function fetchOne(string $sql, array $params = []): ?array
|
||||
{
|
||||
$result = self::query($sql, $params)->fetch();
|
||||
return $result ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ottiene tutte le righe
|
||||
*/
|
||||
public static function fetchAll(string $sql, array $params = []): array
|
||||
{
|
||||
return self::query($sql, $params)->fetchAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserisce e restituisce l'ID
|
||||
*/
|
||||
public static function insert(string $table, array $data): int
|
||||
{
|
||||
$columns = implode(', ', array_keys($data));
|
||||
$placeholders = implode(', ', array_fill(0, count($data), '?'));
|
||||
|
||||
$sql = "INSERT INTO {$table} ({$columns}) VALUES ({$placeholders})";
|
||||
self::query($sql, array_values($data));
|
||||
|
||||
return (int) self::getInstance()->lastInsertId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggiorna righe
|
||||
*/
|
||||
public static function update(string $table, array $data, string $where, array $whereParams = []): int
|
||||
{
|
||||
$setParts = [];
|
||||
foreach (array_keys($data) as $column) {
|
||||
$setParts[] = "{$column} = ?";
|
||||
}
|
||||
$setClause = implode(', ', $setParts);
|
||||
|
||||
$sql = "UPDATE {$table} SET {$setClause} WHERE {$where}";
|
||||
$params = array_merge(array_values($data), $whereParams);
|
||||
|
||||
return self::query($sql, $params)->rowCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Elimina righe
|
||||
*/
|
||||
public static function delete(string $table, string $where, array $params = []): int
|
||||
{
|
||||
$sql = "DELETE FROM {$table} WHERE {$where}";
|
||||
return self::query($sql, $params)->rowCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Conta righe
|
||||
*/
|
||||
public static function count(string $table, string $where = '1=1', array $params = []): int
|
||||
{
|
||||
$sql = "SELECT COUNT(*) as cnt FROM {$table} WHERE {$where}";
|
||||
$result = self::fetchOne($sql, $params);
|
||||
return (int) ($result['cnt'] ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inizia una transazione
|
||||
*/
|
||||
public static function beginTransaction(): bool
|
||||
{
|
||||
return self::getInstance()->beginTransaction();
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit transazione
|
||||
*/
|
||||
public static function commit(): bool
|
||||
{
|
||||
return self::getInstance()->commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback transazione
|
||||
*/
|
||||
public static function rollback(): bool
|
||||
{
|
||||
return self::getInstance()->rollBack();
|
||||
}
|
||||
|
||||
/**
|
||||
* Restituisce l'ultimo ID inserito
|
||||
*/
|
||||
public static function lastInsertId(): int
|
||||
{
|
||||
return (int) self::getInstance()->lastInsertId();
|
||||
}
|
||||
|
||||
private function __clone() {}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
/**
|
||||
* NIS2 Agile - Environment Loader
|
||||
*
|
||||
* Carica variabili d'ambiente dal file .env
|
||||
*/
|
||||
|
||||
class Env
|
||||
{
|
||||
private static bool $loaded = false;
|
||||
private static array $vars = [];
|
||||
|
||||
/**
|
||||
* Carica il file .env
|
||||
*/
|
||||
public static function load(?string $path = null): void
|
||||
{
|
||||
if (self::$loaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
$path = $path ?? dirname(__DIR__, 2) . '/.env';
|
||||
|
||||
if (!file_exists($path)) {
|
||||
self::$loaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if (empty($line) || str_starts_with($line, '#')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strpos($line, '=') !== false) {
|
||||
[$key, $value] = explode('=', $line, 2);
|
||||
$key = trim($key);
|
||||
$value = trim($value);
|
||||
$value = trim($value, '"\'');
|
||||
|
||||
self::$vars[$key] = $value;
|
||||
putenv("{$key}={$value}");
|
||||
$_ENV[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
self::$loaded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ottiene una variabile d'ambiente
|
||||
*/
|
||||
public static function get(string $key, mixed $default = null): mixed
|
||||
{
|
||||
self::load();
|
||||
|
||||
if (isset(self::$vars[$key]) && self::$vars[$key] !== '') {
|
||||
return self::$vars[$key];
|
||||
}
|
||||
|
||||
$value = getenv($key);
|
||||
if ($value !== false && $value !== '') {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (isset($_ENV[$key]) && $_ENV[$key] !== '') {
|
||||
return $_ENV[$key];
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se una variabile esiste e non e' vuota
|
||||
*/
|
||||
public static function has(string $key): bool
|
||||
{
|
||||
$value = self::get($key);
|
||||
return $value !== null && $value !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Ottiene una variabile OBBLIGATORIA
|
||||
*/
|
||||
public static function getRequired(string $key): string
|
||||
{
|
||||
$value = self::get($key);
|
||||
|
||||
if ($value === null || $value === '') {
|
||||
throw new RuntimeException(
|
||||
"Variabile d'ambiente obbligatoria '{$key}' non configurata. " .
|
||||
"Aggiungerla al file .env"
|
||||
);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ottiene come booleano
|
||||
*/
|
||||
public static function bool(string $key, bool $default = false): bool
|
||||
{
|
||||
$value = self::get($key);
|
||||
|
||||
if ($value === null) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return in_array(strtolower($value), ['true', '1', 'yes', 'on'], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ottiene come intero
|
||||
*/
|
||||
public static function int(string $key, int $default = 0): int
|
||||
{
|
||||
$value = self::get($key);
|
||||
return $value !== null ? (int)$value : $default;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-carica all'inclusione
|
||||
Env::load();
|
||||
Reference in New Issue
Block a user