[FIX] Epic C / C5 hardening — esiti flotta di verifica multi-agente (mig.054)
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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
4f386faae5
commit
b917d2da14
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Attività stakeholder - NIS2 Agile</title>
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260628">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260629">
|
||||
<style>
|
||||
.sa-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:12px; font-size:.92rem; line-height:1.6; }
|
||||
.sa-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 16px; }
|
||||
@@ -86,19 +86,19 @@
|
||||
obblighi ai sensi dell'<strong>art. 24 D.Lgs. 138/2024</strong>. L'invio email è disattivato: i questionari si condividono tramite magic-link.
|
||||
</div>
|
||||
|
||||
<div class="sa-tabs">
|
||||
<button class="sa-tab active" id="tab-act" onclick="saTab('act')">Attività</button>
|
||||
<button class="sa-tab" id="tab-tpl" onclick="saTab('tpl')">Questionari tipo</button>
|
||||
<div class="sa-tabs" role="tablist" aria-label="Sezioni attività">
|
||||
<button class="sa-tab active" id="tab-act" role="tab" aria-selected="true" aria-controls="pane-act" onclick="saTab('act')">Attività</button>
|
||||
<button class="sa-tab" id="tab-tpl" role="tab" aria-selected="false" aria-controls="pane-tpl" onclick="saTab('tpl')">Questionari tipo</button>
|
||||
</div>
|
||||
|
||||
<!-- ATTIVITÀ -->
|
||||
<section id="pane-act">
|
||||
<section id="pane-act" role="tabpanel" aria-labelledby="tab-act">
|
||||
<div class="sa-toolbar"><button class="btn btn-primary btn-sm" onclick="actOpen()">+ Nuova attività</button></div>
|
||||
<div id="act-list" aria-live="polite"></div>
|
||||
</section>
|
||||
|
||||
<!-- QUESTIONARI TIPO -->
|
||||
<section id="pane-tpl" class="hidden">
|
||||
<section id="pane-tpl" class="hidden" role="tabpanel" aria-labelledby="tab-tpl">
|
||||
<div class="sa-toolbar"><button class="btn btn-primary btn-sm" onclick="tplOpen()">+ Nuovo questionario tipo</button></div>
|
||||
<div id="tpl-list" aria-live="polite"></div>
|
||||
</section>
|
||||
@@ -107,9 +107,9 @@
|
||||
</div>
|
||||
|
||||
<!-- Modale questionario tipo -->
|
||||
<div class="sm-overlay" id="tpl-modal" role="dialog" aria-modal="true">
|
||||
<div class="sm-overlay" id="tpl-modal" role="dialog" aria-modal="true" aria-labelledby="tpl-modal-title">
|
||||
<div class="sm-dialog">
|
||||
<div class="sm-head"><h3 id="tpl-modal-title">Nuovo questionario tipo</h3><button class="sm-close" type="button" onclick="tplClose()">×</button></div>
|
||||
<div class="sm-head"><h3 id="tpl-modal-title">Nuovo questionario tipo</h3><button class="sm-close" type="button" aria-label="Chiudi" onclick="tplClose()">×</button></div>
|
||||
<div class="sm-body">
|
||||
<input type="hidden" id="tpl-id">
|
||||
<div class="sm-row">
|
||||
@@ -140,9 +140,9 @@
|
||||
</div>
|
||||
|
||||
<!-- Modale attività -->
|
||||
<div class="sm-overlay" id="act-modal" role="dialog" aria-modal="true">
|
||||
<div class="sm-overlay" id="act-modal" role="dialog" aria-modal="true" aria-labelledby="act-modal-title">
|
||||
<div class="sm-dialog">
|
||||
<div class="sm-head"><h3 id="act-modal-title">Nuova attività</h3><button class="sm-close" type="button" onclick="actClose()">×</button></div>
|
||||
<div class="sm-head"><h3 id="act-modal-title">Nuova attività</h3><button class="sm-close" type="button" aria-label="Chiudi" onclick="actClose()">×</button></div>
|
||||
<div class="sm-body">
|
||||
<input type="hidden" id="act-id">
|
||||
<div class="sm-row">
|
||||
@@ -179,9 +179,9 @@
|
||||
</div>
|
||||
|
||||
<!-- Modale dettaglio attività (target + invio) -->
|
||||
<div class="sm-overlay" id="det-modal" role="dialog" aria-modal="true">
|
||||
<div class="sm-overlay" id="det-modal" role="dialog" aria-modal="true" aria-labelledby="det-title">
|
||||
<div class="sm-dialog">
|
||||
<div class="sm-head"><h3 id="det-title">Dettaglio attività</h3><button class="sm-close" type="button" onclick="detClose()">×</button></div>
|
||||
<div class="sm-head"><h3 id="det-title">Dettaglio attività</h3><button class="sm-close" type="button" aria-label="Chiudi" onclick="detClose()">×</button></div>
|
||||
<div class="sm-body" id="det-body"></div>
|
||||
<div class="sm-foot">
|
||||
<button class="btn btn-outline" type="button" onclick="detClose()">Chiudi</button>
|
||||
@@ -191,12 +191,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260628"></script>
|
||||
<script src="/js/common.js?v=20260628"></script>
|
||||
<script src="/js/api.js?v=20260629"></script>
|
||||
<script src="/js/common.js?v=20260629"></script>
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script src="/js/common-bi.js?v=20260628"></script>
|
||||
<script src="/js/i18n.js?v=20260628"></script>
|
||||
<script src="/js/help.js?v=20260628"></script>
|
||||
<script src="/js/common-bi.js?v=20260629"></script>
|
||||
<script src="/js/i18n.js?v=20260629"></script>
|
||||
<script src="/js/help.js?v=20260629"></script>
|
||||
<script>
|
||||
'use strict';
|
||||
/*
|
||||
@@ -205,6 +205,8 @@
|
||||
* con magic-link (email disattivate). Sotto-dashboard feedback completa in C5.2b.
|
||||
*/
|
||||
let SA = { activities: [], templates: [], pickers: { policies: [], misure: [], requisiti: [] }, types: [], stakeholders: [], detail: null };
|
||||
const ST_LABELS = { draft: 'Bozza', scheduled: 'Pianificata', sent: 'Inviata', in_progress: 'In corso', completed: 'Completata', cancelled: 'Annullata' };
|
||||
const TYPE_LABELS = { questionnaire: 'Questionario', read_ack: 'Firma lettura', action: 'Azione' };
|
||||
|
||||
function el(id) { return document.getElementById(id); }
|
||||
function esc(s) { const d = document.createElement('div'); d.textContent = (s == null ? '' : String(s)); return d.innerHTML; }
|
||||
@@ -222,6 +224,8 @@
|
||||
function saTab(which) {
|
||||
el('tab-act').classList.toggle('active', which === 'act');
|
||||
el('tab-tpl').classList.toggle('active', which === 'tpl');
|
||||
el('tab-act').setAttribute('aria-selected', which === 'act' ? 'true' : 'false');
|
||||
el('tab-tpl').setAttribute('aria-selected', which === 'tpl' ? 'true' : 'false');
|
||||
el('pane-act').classList.toggle('hidden', which !== 'act');
|
||||
el('pane-tpl').classList.toggle('hidden', which !== 'tpl');
|
||||
}
|
||||
@@ -271,16 +275,22 @@
|
||||
el('tpl-list').innerHTML = '<table class="sa-table"><thead><tr><th>Nome</th><th>Tipo</th><th>Domande</th><th>Collegamenti</th><th>Azioni</th></tr></thead><tbody>' + rows + '</tbody></table>';
|
||||
}
|
||||
|
||||
function qAdd(text, type, required) {
|
||||
function qAdd(text, type, required, options) {
|
||||
const allowed = ['text','yes_no','single_choice','multi_choice','scale_1_5','number'];
|
||||
const t = allowed.indexOf(type) !== -1 ? type : 'text';
|
||||
const div = document.createElement('div'); div.className = 'q-row';
|
||||
div.innerHTML = '<input type="text" placeholder="Testo della domanda" maxlength="500" value="' + esc(text || '').replace(/"/g,'"') + '">'
|
||||
+ '<select>' + allowed.map(a => '<option value="' + a + '"' + (a === t ? ' selected' : '') + '>' + ({text:'Testo',yes_no:'Sì/No',single_choice:'Scelta singola',multi_choice:'Scelta multipla',scale_1_5:'Scala 1-5',number:'Numero'})[a] + '</option>').join('') + '</select>'
|
||||
+ '<label style="font-size:.78rem;display:flex;align-items:center;gap:4px;white-space:nowrap;"><input type="checkbox"' + (required ? ' checked' : '') + '> obbl.</label>'
|
||||
+ '<button type="button" class="q-del" onclick="this.parentNode.remove()">✕</button>';
|
||||
const isChoice = (t === 'single_choice' || t === 'multi_choice');
|
||||
const div = document.createElement('div'); div.className = 'q-row'; div.style.flexWrap = 'wrap';
|
||||
div.innerHTML = '<input type="text" class="q-text-in" placeholder="Testo della domanda" maxlength="500" value="' + esc(text || '').replace(/"/g,'"') + '">'
|
||||
+ '<select class="q-type-in" onchange="qToggleOpts(this)">' + allowed.map(a => '<option value="' + a + '"' + (a === t ? ' selected' : '') + '>' + ({text:'Testo',yes_no:'Sì/No',single_choice:'Scelta singola',multi_choice:'Scelta multipla',scale_1_5:'Scala 1-5',number:'Numero'})[a] + '</option>').join('') + '</select>'
|
||||
+ '<label style="font-size:.78rem;display:flex;align-items:center;gap:4px;white-space:nowrap;"><input type="checkbox" class="q-req-in"' + (required ? ' checked' : '') + '> obbl.</label>'
|
||||
+ '<button type="button" class="q-del" onclick="this.closest(\'.q-row\').remove()">✕</button>'
|
||||
+ '<input type="text" class="q-opts-in" placeholder="Opzioni separate da | (es. Sì | No | In parte)" maxlength="500" style="flex:1 1 100%;margin-top:6px;' + (isChoice ? '' : 'display:none;') + '" value="' + esc((options || []).join(' | ')).replace(/"/g,'"') + '">';
|
||||
el('tpl-questions').appendChild(div);
|
||||
}
|
||||
function qToggleOpts(sel) {
|
||||
const row = sel.closest('.q-row'); const o = row.querySelector('.q-opts-in');
|
||||
o.style.display = (sel.value === 'single_choice' || sel.value === 'multi_choice') ? '' : 'none';
|
||||
}
|
||||
|
||||
function tplOnKind() {
|
||||
const k = el('tpl-kind').value;
|
||||
@@ -299,7 +309,7 @@
|
||||
const t = await api.stkActGetTemplate(id);
|
||||
el('tpl-name').value = t.name || ''; el('tpl-kind').value = t.kind || 'questionnaire';
|
||||
el('tpl-descr').value = t.description || ''; el('tpl-content').value = t.content || '';
|
||||
(t.questions || []).forEach(q => qAdd(q.text, q.type, q.required));
|
||||
(t.questions || []).forEach(q => qAdd(q.text, q.type, q.required, q.options));
|
||||
selMulti('tpl-proc', t.policy_ids || []); selMulti('tpl-mis', t.misura_codes || []); selMulti('tpl-req', t.requisito_ids || []);
|
||||
} catch (e) { el('tpl-err').textContent = e.message || 'Errore'; }
|
||||
} else {
|
||||
@@ -312,8 +322,14 @@
|
||||
|
||||
function collectQuestions() {
|
||||
return Array.prototype.map.call(el('tpl-questions').children, (row, i) => {
|
||||
const inp = row.querySelector('input[type=text]'); const sel = row.querySelector('select'); const chk = row.querySelector('input[type=checkbox]');
|
||||
return { code: 'Q' + (i + 1), text: inp.value.trim(), type: sel.value, required: chk.checked };
|
||||
const text = row.querySelector('.q-text-in').value.trim();
|
||||
const type = row.querySelector('.q-type-in').value;
|
||||
const required = row.querySelector('.q-req-in').checked;
|
||||
const q = { code: 'Q' + (i + 1), text: text, type: type, required: required };
|
||||
if (type === 'single_choice' || type === 'multi_choice') {
|
||||
q.options = (row.querySelector('.q-opts-in').value || '').split('|').map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
return q;
|
||||
}).filter(q => q.text !== '');
|
||||
}
|
||||
|
||||
@@ -348,10 +364,10 @@
|
||||
if (!SA.activities.length) { el('act-list').innerHTML = '<div class="sa-empty">Nessuna attività. Creane una con "+ Nuova attività".</div>'; return; }
|
||||
const rows = SA.activities.map(a =>
|
||||
'<tr><td><strong>' + esc(a.title) + '</strong>' + (a.template_name ? '<div style="font-size:.78rem;color:#6b7280;">' + esc(a.template_name) + '</div>' : '') + '</td>'
|
||||
+ '<td><span class="sa-badge k-' + a.type + '">' + ({questionnaire:'Questionario',read_ack:'Firma lettura',action:'Azione'})[a.type] + '</span></td>'
|
||||
+ '<td><span class="sa-badge k-' + a.type + '">' + (TYPE_LABELS[a.type] || a.type) + '</span></td>'
|
||||
+ '<td style="font-size:.82rem;">' + (a.assign_mode === 'by_code' ? ('Codice ' + esc(a.stak_code || '?')) : 'Singoli') + '</td>'
|
||||
+ '<td style="font-size:.82rem;">' + (a.due_date ? esc(a.due_date) : '—') + '</td>'
|
||||
+ '<td><span class="sa-badge s-' + a.status + '">' + esc(a.status) + '</span></td>'
|
||||
+ '<td><span class="sa-badge s-' + a.status + '">' + esc(ST_LABELS[a.status] || a.status) + '</span></td>'
|
||||
+ '<td style="font-size:.82rem;">' + a.n_done + '/' + a.n_targets + '</td>'
|
||||
+ '<td style="white-space:nowrap;"><button class="btn btn-outline" style="padding:3px 8px;font-size:.74rem;" onclick="detOpen(' + a.id + ')">Dettaglio</button> '
|
||||
+ '<button class="btn btn-outline" style="padding:3px 8px;font-size:.74rem;" onclick="actOpen(' + a.id + ')">Modifica</button> '
|
||||
@@ -445,7 +461,7 @@
|
||||
const fb = SA.feedback || { template: null, targets: [] };
|
||||
const tmap = {}; (fb.targets || []).forEach(t => { tmap[t.target_id] = t; });
|
||||
const targets = a.targets || [];
|
||||
let html = '<div style="font-size:.85rem;margin-bottom:10px;color:#374151;">Tipo: <strong>' + esc(a.type) + '</strong> · Scadenza: ' + esc(a.due_date || '—') + ' · Stato: ' + esc(a.status) + '</div>';
|
||||
let html = '<div id="det-answers"></div><div style="font-size:.85rem;margin-bottom:10px;color:#374151;">Tipo: <strong>' + esc(TYPE_LABELS[a.type] || a.type) + '</strong> · Scadenza: ' + esc(a.due_date || '—') + ' · Stato: ' + esc(ST_LABELS[a.status] || a.status) + '</div>';
|
||||
|
||||
// Destinatari + esito
|
||||
if (!targets.length) {
|
||||
@@ -489,12 +505,19 @@
|
||||
|
||||
function detAnswers(targetId) {
|
||||
const fb = SA.feedback || {}; const t = (fb.targets || []).find(x => x.target_id === targetId);
|
||||
if (!t || !t.answers) { showNotification('Nessuna risposta.', 'info'); return; }
|
||||
const box = el('det-answers'); if (!box) return;
|
||||
if (!t || !t.answers) { box.innerHTML = ''; showNotification('Nessuna risposta.', 'info'); return; }
|
||||
const qs = (fb.template && fb.template.questions) || [];
|
||||
const label = {}; qs.forEach(q => { label[q.code] = q.text; });
|
||||
let txt = 'Risposte di ' + (t.respondent_name || t.stakeholder_name) + ':\n\n';
|
||||
Object.keys(t.answers).forEach(k => { txt += (label[k] || k) + '\n→ ' + (Array.isArray(t.answers[k]) ? t.answers[k].join(', ') : t.answers[k]) + '\n\n'; });
|
||||
alert(txt);
|
||||
const rows = Object.keys(t.answers).map(k => {
|
||||
const v = t.answers[k]; const sv = Array.isArray(v) ? v.join(', ') : (v == null ? '' : v);
|
||||
return '<div style="padding:4px 0;border-top:1px solid #eef2ff;"><div style="font-weight:600;font-size:.82rem;">' + esc(label[k] || k) + '</div><div style="font-size:.86rem;">' + esc(sv || '—') + '</div></div>';
|
||||
}).join('');
|
||||
box.innerHTML = '<div style="background:#f8fafc;border:1px solid #e5e7eb;border-radius:8px;padding:10px 12px;margin-bottom:12px;">'
|
||||
+ '<div style="display:flex;justify-content:space-between;align-items:center;"><strong style="font-size:.85rem;">Risposte di ' + esc(t.respondent_name || t.stakeholder_name) + '</strong>'
|
||||
+ '<button class="btn btn-outline" style="padding:1px 8px;font-size:.72rem;" onclick="el(\'det-answers\').innerHTML=\'\'">Chiudi</button></div>'
|
||||
+ rows + '</div>';
|
||||
box.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
|
||||
async function detComment() {
|
||||
|
||||
Reference in New Issue
Block a user