nis2-agile/docs/sql/001_initial_schema.sql
Cristiano Benassati ae78a2f7f4 [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>
2026-02-17 17:50:18 +01:00

491 lines
22 KiB
SQL

-- ═══════════════════════════════════════════════════════════════════════════
-- NIS2 Agile - Schema Database Iniziale
-- Database: nis2_agile_db
-- Versione: 1.0.0
-- Data: 2026-02-17
-- ═══════════════════════════════════════════════════════════════════════════
CREATE DATABASE IF NOT EXISTS nis2_agile_db
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE nis2_agile_db;
-- ═══════════════════════════════════════════════════════════════════════════
-- CORE: Organizzazioni e Utenti
-- ═══════════════════════════════════════════════════════════════════════════
CREATE TABLE organizations (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
vat_number VARCHAR(20),
fiscal_code VARCHAR(16),
sector ENUM(
'energy','transport','banking','health','water','digital_infra',
'public_admin','manufacturing','postal','chemical','food',
'waste','ict_services','digital_providers','space','research','other'
) NOT NULL DEFAULT 'other',
entity_type ENUM('essential','important','not_applicable') DEFAULT 'not_applicable',
employee_count INT,
annual_turnover_eur DECIMAL(15,2),
country VARCHAR(2) DEFAULT 'IT',
city VARCHAR(100),
address VARCHAR(255),
website VARCHAR(255),
contact_email VARCHAR(255),
contact_phone VARCHAR(30),
subscription_plan ENUM('free','professional','enterprise') DEFAULT 'free',
is_active TINYINT(1) DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_sector (sector),
INDEX idx_entity_type (entity_type),
INDEX idx_subscription (subscription_plan)
) ENGINE=InnoDB;
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
full_name VARCHAR(255) NOT NULL,
phone VARCHAR(30),
role ENUM(
'super_admin','org_admin','compliance_manager',
'board_member','auditor','employee','consultant'
) NOT NULL DEFAULT 'employee',
preferred_language VARCHAR(5) DEFAULT 'it',
is_active TINYINT(1) DEFAULT 1,
email_verified_at DATETIME,
last_login_at DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_email (email),
INDEX idx_role (role),
INDEX idx_active (is_active)
) ENGINE=InnoDB;
CREATE TABLE user_organizations (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
organization_id INT NOT NULL,
role ENUM('org_admin','compliance_manager','board_member','auditor','employee') NOT NULL DEFAULT 'employee',
is_primary TINYINT(1) DEFAULT 0,
joined_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
UNIQUE KEY uk_user_org (user_id, organization_id),
INDEX idx_user (user_id),
INDEX idx_org (organization_id)
) ENGINE=InnoDB;
CREATE TABLE refresh_tokens (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
token VARCHAR(255) NOT NULL,
expires_at DATETIME NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_token (token),
INDEX idx_expires (expires_at)
) ENGINE=InnoDB;
-- ═══════════════════════════════════════════════════════════════════════════
-- GAP ANALYSIS & ASSESSMENT
-- ═══════════════════════════════════════════════════════════════════════════
CREATE TABLE assessments (
id INT AUTO_INCREMENT PRIMARY KEY,
organization_id INT NOT NULL,
title VARCHAR(255) NOT NULL,
assessment_type ENUM('initial','periodic','post_incident') DEFAULT 'initial',
status ENUM('draft','in_progress','completed') DEFAULT 'draft',
overall_score DECIMAL(5,2),
category_scores JSON,
completed_by INT,
completed_at DATETIME,
ai_summary TEXT,
ai_recommendations JSON,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
FOREIGN KEY (completed_by) REFERENCES users(id) ON DELETE SET NULL,
INDEX idx_org (organization_id),
INDEX idx_status (status)
) ENGINE=InnoDB;
CREATE TABLE assessment_responses (
id INT AUTO_INCREMENT PRIMARY KEY,
assessment_id INT NOT NULL,
question_code VARCHAR(50) NOT NULL,
nis2_article VARCHAR(20),
iso27001_control VARCHAR(20),
category VARCHAR(100),
question_text TEXT NOT NULL,
response_value ENUM('not_implemented','partial','implemented','not_applicable'),
maturity_level TINYINT,
evidence_description TEXT,
notes TEXT,
answered_by INT,
answered_at DATETIME,
FOREIGN KEY (assessment_id) REFERENCES assessments(id) ON DELETE CASCADE,
FOREIGN KEY (answered_by) REFERENCES users(id) ON DELETE SET NULL,
INDEX idx_assessment (assessment_id),
INDEX idx_category (category),
UNIQUE KEY uk_assessment_question (assessment_id, question_code)
) ENGINE=InnoDB;
-- ═══════════════════════════════════════════════════════════════════════════
-- RISK MANAGEMENT
-- ═══════════════════════════════════════════════════════════════════════════
CREATE TABLE risks (
id INT AUTO_INCREMENT PRIMARY KEY,
organization_id INT NOT NULL,
risk_code VARCHAR(20),
title VARCHAR(255) NOT NULL,
description TEXT,
category ENUM('cyber','operational','compliance','supply_chain','physical','human') NOT NULL,
threat_source VARCHAR(255),
vulnerability VARCHAR(255),
affected_assets JSON,
likelihood TINYINT,
impact TINYINT,
inherent_risk_score TINYINT,
treatment ENUM('mitigate','accept','transfer','avoid') DEFAULT 'mitigate',
residual_likelihood TINYINT,
residual_impact TINYINT,
residual_risk_score TINYINT,
status ENUM('identified','analyzing','treating','monitored','closed') DEFAULT 'identified',
owner_user_id INT,
review_date DATE,
nis2_article VARCHAR(20),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
FOREIGN KEY (owner_user_id) REFERENCES users(id) ON DELETE SET NULL,
INDEX idx_org (organization_id),
INDEX idx_status (status),
INDEX idx_category (category),
UNIQUE KEY uk_org_risk_code (organization_id, risk_code)
) ENGINE=InnoDB;
CREATE TABLE risk_treatments (
id INT AUTO_INCREMENT PRIMARY KEY,
risk_id INT NOT NULL,
action_description TEXT NOT NULL,
responsible_user_id INT,
due_date DATE,
status ENUM('planned','in_progress','completed','overdue') DEFAULT 'planned',
completion_date DATE,
notes TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (risk_id) REFERENCES risks(id) ON DELETE CASCADE,
FOREIGN KEY (responsible_user_id) REFERENCES users(id) ON DELETE SET NULL,
INDEX idx_risk (risk_id),
INDEX idx_status (status)
) ENGINE=InnoDB;
-- ═══════════════════════════════════════════════════════════════════════════
-- INCIDENT MANAGEMENT (Art. 23)
-- ═══════════════════════════════════════════════════════════════════════════
CREATE TABLE incidents (
id INT AUTO_INCREMENT PRIMARY KEY,
organization_id INT NOT NULL,
incident_code VARCHAR(20),
title VARCHAR(255) NOT NULL,
description TEXT,
classification ENUM(
'cyber_attack','data_breach','system_failure',
'human_error','natural_disaster','supply_chain','other'
) NOT NULL,
severity ENUM('low','medium','high','critical') NOT NULL,
is_significant TINYINT(1) DEFAULT 0,
status ENUM(
'detected','analyzing','containing','eradicating',
'recovering','closed','post_mortem'
) DEFAULT 'detected',
detected_at DATETIME NOT NULL,
-- NIS2 Art.23 milestones
early_warning_due DATETIME,
early_warning_sent_at DATETIME,
notification_due DATETIME,
notification_sent_at DATETIME,
final_report_due DATETIME,
final_report_sent_at DATETIME,
-- Impact
affected_services TEXT,
affected_users_count INT,
cross_border_impact TINYINT(1) DEFAULT 0,
malicious_action TINYINT(1) DEFAULT 0,
-- Resolution
root_cause TEXT,
remediation_actions TEXT,
lessons_learned TEXT,
reported_by INT,
assigned_to INT,
closed_at DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
FOREIGN KEY (reported_by) REFERENCES users(id) ON DELETE SET NULL,
FOREIGN KEY (assigned_to) REFERENCES users(id) ON DELETE SET NULL,
INDEX idx_org (organization_id),
INDEX idx_status (status),
INDEX idx_severity (severity),
UNIQUE KEY uk_org_incident_code (organization_id, incident_code)
) ENGINE=InnoDB;
CREATE TABLE incident_timeline (
id INT AUTO_INCREMENT PRIMARY KEY,
incident_id INT NOT NULL,
event_type ENUM('detection','escalation','notification','action','update','resolution') NOT NULL,
description TEXT NOT NULL,
created_by INT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (incident_id) REFERENCES incidents(id) ON DELETE CASCADE,
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
INDEX idx_incident (incident_id)
) ENGINE=InnoDB;
-- ═══════════════════════════════════════════════════════════════════════════
-- POLICY & PROCEDURE MANAGEMENT
-- ═══════════════════════════════════════════════════════════════════════════
CREATE TABLE policies (
id INT AUTO_INCREMENT PRIMARY KEY,
organization_id INT NOT NULL,
title VARCHAR(255) NOT NULL,
category ENUM(
'information_security','access_control','incident_response',
'business_continuity','supply_chain','encryption','hr_security',
'asset_management','network_security','vulnerability_management'
) NOT NULL,
nis2_article VARCHAR(20),
version VARCHAR(10) DEFAULT '1.0',
status ENUM('draft','review','approved','published','archived') DEFAULT 'draft',
content LONGTEXT,
approved_by INT,
approved_at DATETIME,
next_review_date DATE,
ai_generated TINYINT(1) DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
FOREIGN KEY (approved_by) REFERENCES users(id) ON DELETE SET NULL,
INDEX idx_org (organization_id),
INDEX idx_status (status),
INDEX idx_category (category)
) ENGINE=InnoDB;
-- ═══════════════════════════════════════════════════════════════════════════
-- SUPPLY CHAIN SECURITY (Art. 21.2.d)
-- ═══════════════════════════════════════════════════════════════════════════
CREATE TABLE suppliers (
id INT AUTO_INCREMENT PRIMARY KEY,
organization_id INT NOT NULL,
name VARCHAR(255) NOT NULL,
vat_number VARCHAR(20),
contact_email VARCHAR(255),
contact_name VARCHAR(255),
service_type VARCHAR(255),
service_description TEXT,
criticality ENUM('low','medium','high','critical') DEFAULT 'medium',
risk_score TINYINT,
last_assessment_date DATE,
next_assessment_date DATE,
contract_start_date DATE,
contract_expiry_date DATE,
security_requirements_met TINYINT(1) DEFAULT 0,
assessment_responses JSON,
notes TEXT,
status ENUM('active','under_review','suspended','terminated') DEFAULT 'active',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
INDEX idx_org (organization_id),
INDEX idx_criticality (criticality),
INDEX idx_status (status)
) ENGINE=InnoDB;
-- ═══════════════════════════════════════════════════════════════════════════
-- TRAINING & AWARENESS (Art. 20)
-- ═══════════════════════════════════════════════════════════════════════════
CREATE TABLE training_courses (
id INT AUTO_INCREMENT PRIMARY KEY,
organization_id INT,
title VARCHAR(255) NOT NULL,
description TEXT,
target_role ENUM('all','board_member','compliance_manager','employee','technical') DEFAULT 'all',
nis2_article VARCHAR(20),
is_mandatory TINYINT(1) DEFAULT 0,
duration_minutes INT,
content JSON,
quiz JSON,
passing_score TINYINT DEFAULT 70,
is_active TINYINT(1) DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
INDEX idx_org (organization_id),
INDEX idx_role (target_role)
) ENGINE=InnoDB;
CREATE TABLE training_assignments (
id INT AUTO_INCREMENT PRIMARY KEY,
course_id INT NOT NULL,
user_id INT NOT NULL,
organization_id INT NOT NULL,
status ENUM('assigned','in_progress','completed','overdue') DEFAULT 'assigned',
due_date DATE,
started_at DATETIME,
completed_at DATETIME,
quiz_score TINYINT,
certificate_url VARCHAR(255),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (course_id) REFERENCES training_courses(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
INDEX idx_user (user_id),
INDEX idx_org (organization_id),
INDEX idx_status (status),
UNIQUE KEY uk_course_user (course_id, user_id)
) ENGINE=InnoDB;
-- ═══════════════════════════════════════════════════════════════════════════
-- ASSET MANAGEMENT (Art. 21.2.i)
-- ═══════════════════════════════════════════════════════════════════════════
CREATE TABLE assets (
id INT AUTO_INCREMENT PRIMARY KEY,
organization_id INT NOT NULL,
name VARCHAR(255) NOT NULL,
asset_type ENUM('hardware','software','network','data','service','personnel','facility') NOT NULL,
category VARCHAR(100),
description TEXT,
criticality ENUM('low','medium','high','critical') DEFAULT 'medium',
owner_user_id INT,
location VARCHAR(255),
ip_address VARCHAR(45),
vendor VARCHAR(255),
version VARCHAR(50),
serial_number VARCHAR(100),
purchase_date DATE,
warranty_expiry DATE,
status ENUM('active','maintenance','decommissioned') DEFAULT 'active',
dependencies JSON,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
FOREIGN KEY (owner_user_id) REFERENCES users(id) ON DELETE SET NULL,
INDEX idx_org (organization_id),
INDEX idx_type (asset_type),
INDEX idx_criticality (criticality),
INDEX idx_status (status)
) ENGINE=InnoDB;
-- ═══════════════════════════════════════════════════════════════════════════
-- AUDIT & COMPLIANCE TRACKING
-- ═══════════════════════════════════════════════════════════════════════════
CREATE TABLE compliance_controls (
id INT AUTO_INCREMENT PRIMARY KEY,
organization_id INT NOT NULL,
control_code VARCHAR(50) NOT NULL,
framework ENUM('nis2','iso27001','both') DEFAULT 'nis2',
title VARCHAR(255) NOT NULL,
description TEXT,
status ENUM('not_started','in_progress','implemented','verified') DEFAULT 'not_started',
implementation_percentage TINYINT DEFAULT 0,
evidence_description TEXT,
responsible_user_id INT,
last_verified_at DATETIME,
next_review_date DATE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
FOREIGN KEY (responsible_user_id) REFERENCES users(id) ON DELETE SET NULL,
INDEX idx_org (organization_id),
INDEX idx_framework (framework),
INDEX idx_status (status),
UNIQUE KEY uk_org_control (organization_id, control_code)
) ENGINE=InnoDB;
CREATE TABLE evidence_files (
id INT AUTO_INCREMENT PRIMARY KEY,
organization_id INT NOT NULL,
control_id INT,
entity_type VARCHAR(50),
entity_id INT,
file_name VARCHAR(255) NOT NULL,
file_path VARCHAR(500) NOT NULL,
file_size INT,
mime_type VARCHAR(100),
uploaded_by INT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
FOREIGN KEY (control_id) REFERENCES compliance_controls(id) ON DELETE SET NULL,
FOREIGN KEY (uploaded_by) REFERENCES users(id) ON DELETE SET NULL,
INDEX idx_org (organization_id),
INDEX idx_entity (entity_type, entity_id)
) ENGINE=InnoDB;
CREATE TABLE audit_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT,
organization_id INT,
action VARCHAR(255) NOT NULL,
entity_type VARCHAR(100),
entity_id INT,
details JSON,
ip_address VARCHAR(45),
user_agent VARCHAR(500),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user (user_id),
INDEX idx_org (organization_id),
INDEX idx_action (action),
INDEX idx_entity (entity_type, entity_id),
INDEX idx_created (created_at)
) ENGINE=InnoDB;
-- ═══════════════════════════════════════════════════════════════════════════
-- AI INTERACTIONS LOG
-- ═══════════════════════════════════════════════════════════════════════════
CREATE TABLE ai_interactions (
id INT AUTO_INCREMENT PRIMARY KEY,
organization_id INT NOT NULL,
user_id INT NOT NULL,
interaction_type ENUM(
'gap_analysis','risk_suggestion','policy_draft',
'incident_classification','qa','report_generation'
) NOT NULL,
prompt_summary VARCHAR(500),
response_summary TEXT,
tokens_used INT,
model_used VARCHAR(50),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_org (organization_id),
INDEX idx_type (interaction_type)
) ENGINE=InnoDB;
-- ═══════════════════════════════════════════════════════════════════════════
-- RATE LIMITING
-- ═══════════════════════════════════════════════════════════════════════════
CREATE TABLE rate_limits (
id INT AUTO_INCREMENT PRIMARY KEY,
rate_key VARCHAR(255) NOT NULL,
ip_address VARCHAR(45) NOT NULL,
attempts INT DEFAULT 1,
window_start DATETIME NOT NULL,
INDEX idx_key_ip (rate_key, ip_address),
INDEX idx_window (window_start)
) ENGINE=InnoDB;