Corretti i finding confermati dalla verifica multi-agente (5 major + minori): SICUREZZA - Stored XSS allegati: da blocklist a ALLOWLIST di estensioni innocue (no html/svg/js renderizzabili same-origin) in StakeholderPortalController::attachment e StakeholderActivityController::storeUpload; nome file randomizzato (random_bytes). - Magic-link: scadenza (mig.054 stk_activity_targets.token_expires_at; send() imposta scadenza attività+30gg o +90gg; resolveTarget() → 410 TOKEN_EXPIRED se scaduto); rate-limit per-IP sugli endpoint del portale; comment/attachment bloccati su attività chiusa. CORRETTEZZA - send(): NON rigenera token né azzera lo stato dei destinatari già responded/acknowledged (prima ne perdeva l'esito); imposta token_expires_at. - assign individuale: semantica "replace" (rimuove i deselezionati non ancora conclusi) + guard su lista vuota (evita 'IN ()'). - update(): conserva assign_mode esistente quando si modifica solo stak_code. UI/UX/A11Y - Editor opzioni per domande a scelta singola/multipla (prima degradavano a testo nel portale). - Etichette stato/tipo localizzate; risposte mostrate inline (no alert()); escAttr nel portale (escape virgolette negli attributi); ARIA su modali/tab; voce sidebar anche in common.js. OPEN ITEM (NON regressione C5, pre-esistente e ambientale): l'upload allegati restituisce UPLOAD_ERROR in prod — move_uploaded_file/is_uploaded_file fallisce nella topologia proxy→fastcgi (stesso pattern di evidence_files/AuditController, mai funzionato: la dir uploads/evidence non esiste). rename/copy come www-data funzionano. Da investigare lato infra. Il resto di C5 (questionari, firma-lettura, commenti, calendario, portale) è pienamente operativo. Smoke prod OK: allowlist (.html→422, struttura ok), opzioni scelta nel portale, send no-reset, replace individuale, token_expires_at presente. Additivo. v1.21.1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
142 lines
8.4 KiB
PHP
142 lines
8.4 KiB
PHP
<?php
|
|
/**
|
|
* NIS2 Agile — DDL idempotente Attività stakeholder (Epic C / C5.2a, mig.052).
|
|
* ----------------------------------------------------------------------------
|
|
* Crea (se mancanti) le tabelle del modulo Attività stakeholder ed estende
|
|
* l'ENUM entity_type di review_schedule con 'stakeholder_activity' (calendario NIS2).
|
|
* NESSUN dato di sistema: i questionari tipo sono per-org.
|
|
*
|
|
* Idempotente: CREATE TABLE IF NOT EXISTS + ALTER MODIFY (re-eseguibile senza
|
|
* errore). Allineato a docs/sql/052_stakeholder_activities.sql.
|
|
*
|
|
* Uso (dentro il container app):
|
|
* docker exec nis2-app php /var/www/nis2-agile/application/cli/seed_stakeholder_activities.php
|
|
*/
|
|
|
|
if (PHP_SAPI !== 'cli') { http_response_code(403); exit("Solo da CLI.\n"); }
|
|
|
|
require_once __DIR__ . '/../config/env.php';
|
|
require_once __DIR__ . '/../config/database.php';
|
|
|
|
$pdo = Database::getInstance();
|
|
$pdo->exec("SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci");
|
|
|
|
$ddl = [
|
|
"CREATE TABLE IF NOT EXISTS stk_questionnaire_templates (
|
|
id INT NOT NULL AUTO_INCREMENT, organization_id INT NOT NULL,
|
|
name VARCHAR(255) NOT NULL, kind ENUM('questionnaire','read_ack') NOT NULL DEFAULT 'questionnaire',
|
|
description TEXT NULL, content TEXT NULL, questions JSON NULL,
|
|
status ENUM('draft','active','archived') NOT NULL DEFAULT 'active',
|
|
created_by INT NULL,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
PRIMARY KEY (id), KEY idx_stkqt_org (organization_id),
|
|
CONSTRAINT fk_stkqt_org FOREIGN KEY (organization_id) REFERENCES organizations (id) ON DELETE CASCADE,
|
|
CONSTRAINT fk_stkqt_user FOREIGN KEY (created_by) REFERENCES users (id) ON DELETE SET NULL
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
|
|
|
"CREATE TABLE IF NOT EXISTS stk_template_procedures (
|
|
template_id INT NOT NULL, policy_id INT NOT NULL,
|
|
PRIMARY KEY (template_id, policy_id), KEY idx_stktp_policy (policy_id),
|
|
CONSTRAINT fk_stktp_tpl FOREIGN KEY (template_id) REFERENCES stk_questionnaire_templates (id) ON DELETE CASCADE,
|
|
CONSTRAINT fk_stktp_policy FOREIGN KEY (policy_id) REFERENCES policies (id) ON DELETE CASCADE
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
|
|
|
"CREATE TABLE IF NOT EXISTS stk_template_misure (
|
|
template_id INT NOT NULL, misura_code VARCHAR(16) NOT NULL,
|
|
PRIMARY KEY (template_id, misura_code), KEY idx_stktm_misura (misura_code),
|
|
CONSTRAINT fk_stktm_tpl FOREIGN KEY (template_id) REFERENCES stk_questionnaire_templates (id) ON DELETE CASCADE,
|
|
CONSTRAINT fk_stktm_misura FOREIGN KEY (misura_code) REFERENCES cfg_nis2_misure (misura_code) ON DELETE CASCADE
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
|
|
|
"CREATE TABLE IF NOT EXISTS stk_template_requisiti (
|
|
template_id INT NOT NULL, requisito_id INT NOT NULL,
|
|
PRIMARY KEY (template_id, requisito_id), KEY idx_stktr_req (requisito_id),
|
|
CONSTRAINT fk_stktr_tpl FOREIGN KEY (template_id) REFERENCES stk_questionnaire_templates (id) ON DELETE CASCADE,
|
|
CONSTRAINT fk_stktr_req FOREIGN KEY (requisito_id) REFERENCES cfg_nis2_requisiti (id) ON DELETE CASCADE
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
|
|
|
"CREATE TABLE IF NOT EXISTS stk_activities (
|
|
id INT NOT NULL AUTO_INCREMENT, organization_id INT NOT NULL,
|
|
title VARCHAR(255) NOT NULL, type ENUM('questionnaire','read_ack','action') NOT NULL DEFAULT 'questionnaire',
|
|
template_id INT NULL, description TEXT NULL,
|
|
assign_mode ENUM('by_code','individual') NOT NULL DEFAULT 'individual', stak_code VARCHAR(16) NULL,
|
|
planned_date DATE NULL, due_date DATE NULL,
|
|
status ENUM('draft','scheduled','sent','in_progress','completed','cancelled') NOT NULL DEFAULT 'draft',
|
|
created_by INT NULL,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
PRIMARY KEY (id), KEY idx_stka_org (organization_id), KEY idx_stka_tpl (template_id),
|
|
CONSTRAINT fk_stka_org FOREIGN KEY (organization_id) REFERENCES organizations (id) ON DELETE CASCADE,
|
|
CONSTRAINT fk_stka_tpl FOREIGN KEY (template_id) REFERENCES stk_questionnaire_templates (id) ON DELETE SET NULL,
|
|
CONSTRAINT fk_stka_user FOREIGN KEY (created_by) REFERENCES users (id) ON DELETE SET NULL
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
|
|
|
"CREATE TABLE IF NOT EXISTS stk_activity_targets (
|
|
id INT NOT NULL AUTO_INCREMENT, activity_id INT NOT NULL, stakeholder_id INT NOT NULL,
|
|
access_token_hash CHAR(64) NULL,
|
|
state ENUM('pending','sent','responded','acknowledged','expired') NOT NULL DEFAULT 'pending',
|
|
sent_at DATETIME NULL, responded_at DATETIME NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
PRIMARY KEY (id), UNIQUE KEY uq_stkat (activity_id, stakeholder_id),
|
|
KEY idx_stkat_stk (stakeholder_id), KEY idx_stkat_token (access_token_hash),
|
|
CONSTRAINT fk_stkat_act FOREIGN KEY (activity_id) REFERENCES stk_activities (id) ON DELETE CASCADE,
|
|
CONSTRAINT fk_stkat_stk FOREIGN KEY (stakeholder_id) REFERENCES stakeholders (id) ON DELETE CASCADE
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
|
|
|
"CREATE TABLE IF NOT EXISTS stk_activity_procedures (
|
|
activity_id INT NOT NULL, policy_id INT NOT NULL,
|
|
PRIMARY KEY (activity_id, policy_id), KEY idx_stkap_policy (policy_id),
|
|
CONSTRAINT fk_stkap_act FOREIGN KEY (activity_id) REFERENCES stk_activities (id) ON DELETE CASCADE,
|
|
CONSTRAINT fk_stkap_policy FOREIGN KEY (policy_id) REFERENCES policies (id) ON DELETE CASCADE
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
|
|
|
// C5.2b (mig.053) — feedback: risposte + commenti
|
|
"CREATE TABLE IF NOT EXISTS stk_activity_responses (
|
|
id INT NOT NULL AUTO_INCREMENT, target_id INT NOT NULL,
|
|
answers JSON NULL, acknowledged_at DATETIME NULL,
|
|
respondent_name VARCHAR(255) NULL, respondent_email VARCHAR(255) NULL, ip_address VARCHAR(45) NULL,
|
|
submitted_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
PRIMARY KEY (id), UNIQUE KEY uq_stkar_target (target_id),
|
|
CONSTRAINT fk_stkar_target FOREIGN KEY (target_id) REFERENCES stk_activity_targets (id) ON DELETE CASCADE
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
|
|
|
"CREATE TABLE IF NOT EXISTS stk_activity_comments (
|
|
id INT NOT NULL AUTO_INCREMENT, activity_id INT NOT NULL, body TEXT NOT NULL,
|
|
author_kind ENUM('internal','external') NOT NULL DEFAULT 'internal',
|
|
author_user_id INT NULL, author_label VARCHAR(255) NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
PRIMARY KEY (id), KEY idx_stkac_act (activity_id),
|
|
CONSTRAINT fk_stkac_act FOREIGN KEY (activity_id) REFERENCES stk_activities (id) ON DELETE CASCADE,
|
|
CONSTRAINT fk_stkac_user FOREIGN KEY (author_user_id) REFERENCES users (id) ON DELETE SET NULL
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
|
];
|
|
foreach ($ddl as $stmt) {
|
|
try { $pdo->exec($stmt); }
|
|
catch (PDOException $e) { if (!in_array($e->errorInfo[1] ?? 0, [1050, 1061], true)) { throw $e; } }
|
|
}
|
|
|
|
// Estende l'ENUM del calendario (review_schedule). Idempotente in effetto.
|
|
try {
|
|
$pdo->exec("ALTER TABLE review_schedule
|
|
MODIFY COLUMN entity_type ENUM('role','skill','inventory','procedure','risk','supplier','measure','custom','stakeholder_activity') NOT NULL");
|
|
} catch (PDOException $e) {
|
|
fwrite(STDERR, "WARN ALTER review_schedule: " . $e->getMessage() . "\n");
|
|
}
|
|
|
|
// mig.054 — scadenza magic-link (idempotente: ignora 1060 colonna già esistente).
|
|
try {
|
|
$pdo->exec("ALTER TABLE stk_activity_targets ADD COLUMN token_expires_at DATETIME NULL DEFAULT NULL AFTER access_token_hash");
|
|
} catch (PDOException $e) {
|
|
if (($e->errorInfo[1] ?? 0) !== 1060) { fwrite(STDERR, "WARN ALTER stk_activity_targets: " . $e->getMessage() . "\n"); }
|
|
}
|
|
|
|
$counts = [];
|
|
foreach (['stk_questionnaire_templates','stk_template_procedures','stk_template_misure','stk_template_requisiti',
|
|
'stk_activities','stk_activity_targets','stk_activity_procedures',
|
|
'stk_activity_responses','stk_activity_comments'] as $t) {
|
|
$counts[$t] = (int) $pdo->query("SELECT COUNT(*) FROM $t")->fetchColumn();
|
|
}
|
|
$enum = $pdo->query("SELECT COLUMN_TYPE FROM information_schema.COLUMNS
|
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'review_schedule' AND COLUMN_NAME = 'entity_type'")->fetchColumn();
|
|
echo "OK seed-stakeholder-activities — " . json_encode($counts, JSON_UNESCAPED_UNICODE) . "\n";
|
|
echo "review_schedule.entity_type has stakeholder_activity: " . (str_contains((string) $enum, 'stakeholder_activity') ? 'YES' : 'NO') . "\n";
|