[FEAT] A4 Fase 4.3 — Matrice RACI + link m2m: raci_assignments + procedure_inventory/procedure_risk/inventory_risk/risk_measure (mig.043), RaciController org-scoped (anti-IDOR, validazione object/measure), raci.html+raci.js (Bootstrap Italia/AGID), routing+sidebar+api+help+i18n. Build+review adversariale via workflow (0 finding critical/major). Cache-buster ?v=20260619.
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
05dfba6fcd
commit
b4d47d58a2
@@ -254,6 +254,22 @@ class NIS2API {
|
||||
compGapGrid() { return this._acn(this.get('/competences/gap-grid')); }
|
||||
compOpenAction(roleSkillId) { return this._acn(this.post('/competences/open-action', { role_skill_id: roleSkillId })); }
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Matrice RACI (A4 Fase 4.3) — hub ruoli↔oggetti (R/A/C/I) + link m2m
|
||||
// (procedura↔inventario, procedura↔rischio, inventario↔rischio, rischio↔misura).
|
||||
// Stesso contratto _acn: ritornano `data`, lanciano su success=false.
|
||||
// NB: i DELETE non hanno body (del() in request() invia body solo per POST/PUT),
|
||||
// quindi i parametri compositi vanno passati come query string → il backend
|
||||
// li legge da getParam() ($_REQUEST).
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
raciMatrix() { return this._acn(this.get('/raci/matrix')); }
|
||||
raciAssign(d) { return this._acn(this.post('/raci/assign', d)); }
|
||||
raciUnassign(d) { return this._acn(this.del('/raci/assign?role_id=' + encodeURIComponent(d.role_id) + '&object_type=' + encodeURIComponent(d.object_type) + '&object_id=' + encodeURIComponent(d.object_id))); }
|
||||
raciObjects(type) { return this._acn(this.get('/raci/objects?type=' + encodeURIComponent(type))); }
|
||||
raciLinks(linkType, id) { return this._acn(this.get('/raci/links?link_type=' + encodeURIComponent(linkType) + '&id=' + encodeURIComponent(id))); }
|
||||
raciLink(d) { return this._acn(this.post('/raci/link', d)); }
|
||||
raciUnlink(d) { return this._acn(this.del('/raci/link?link_type=' + encodeURIComponent(d.link_type) + '&a_id=' + encodeURIComponent(d.a_id) + '&b_id=' + encodeURIComponent(d.b_id))); }
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Dashboard
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -197,6 +197,7 @@ function loadSidebar() {
|
||||
items: [
|
||||
{ name: 'Organigramma', href: 'organigramma.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path d="M8 2.5h4a.5.5 0 01.5.5v2a.5.5 0 01-.5.5h-1.25v2.5H15a.5.5 0 01.5.5v1.25H17a.5.5 0 01.5.5v3a.5.5 0 01-.5.5h-4a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h1.25V9.5H6v1.25H7.5a.5.5 0 01.5.5v3a.5.5 0 01-.5.5h-4a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5H4.5V9.5a.5.5 0 01.5-.5h4.75V6H8a.5.5 0 01-.5-.5V3a.5.5 0 01.5-.5z"/></svg>`, i18nKey: 'nav.org_chart' },
|
||||
{ name: 'Competenze', href: 'competenze.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path d="M5 2a1 1 0 011 1v1h1a1 1 0 010 2H6v1a1 1 0 11-2 0V6H3a1 1 0 010-2h1V3a1 1 0 011-1zm0 10a1 1 0 011 1v1h1a1 1 0 110 2H6v1a1 1 0 11-2 0v-1H3a1 1 0 110-2h1v-1a1 1 0 011-1zM12 2a1 1 0 01.967.744L14.146 7.2 17.5 8.134a1 1 0 010 1.732l-3.354.934-1.18 4.455a1 1 0 01-1.933 0L9.854 10.8 6.5 9.866a1 1 0 010-1.732l3.354-.934 1.179-4.456A1 1 0 0112 2z"/></svg>`, i18nKey: 'nav.competences' },
|
||||
{ name: 'Matrice RACI', href: 'raci.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zm0 6a1 1 0 011-1h5a1 1 0 011 1v6a1 1 0 01-1 1H4a1 1 0 01-1-1v-6zm9 0a1 1 0 011-1h3a1 1 0 011 1v6a1 1 0 01-1 1h-3a1 1 0 01-1-1v-6z" clip-rule="evenodd"/></svg>`, i18nKey: 'nav.raci' },
|
||||
{ name: 'Rischi', href: 'risks.html', icon: iconShieldExclamation(), i18nKey: 'nav.risks' },
|
||||
{ name: 'Incidenti', href: 'incidents.html', icon: iconBell(), i18nKey: 'nav.incidents' },
|
||||
{ name: 'Policy', href: 'policies.html', icon: iconDocumentText(), i18nKey: 'nav.policies' },
|
||||
|
||||
@@ -276,6 +276,47 @@ const HelpSystem = (function () {
|
||||
]
|
||||
},
|
||||
|
||||
// ─── Matrice RACI (A4 Fase 4.3) ──────────────────────────────
|
||||
'raci': {
|
||||
title: 'Guida - Matrice RACI',
|
||||
intro: 'La Matrice RACI è l\'hub del modello relazionale: collega i ruoli dell\'organigramma agli oggetti del sistema (inventario, procedure, rischi, fornitori) indicando chi è Responsabile (R), chi è Approvatore/responsabile finale (A), chi va Consultato (C) e chi va Informato (I). In più gestisce i collegamenti molti-a-molti fra procedure, inventario, rischi e misure, così da tracciare la rete delle dipendenze. E\' uno strumento di supporto, non un parere legale.',
|
||||
sections: [
|
||||
{
|
||||
heading: 'Cos\'è la RACI',
|
||||
items: [
|
||||
'Per ogni coppia <strong>ruolo × oggetto</strong> assegni una lettera: <strong>R</strong>esponsible (esegue), <strong>A</strong>ccountable (risponde del risultato, di norma uno solo), <strong>C</strong>onsulted (consultato prima), <strong>I</strong>nformed (informato dopo).',
|
||||
'Gli oggetti collegabili sono: <strong>Inventario</strong> (gli asset), <strong>Procedure</strong> (le policy), <strong>Rischi</strong> (il registro rischi) e <strong>Fornitori</strong> (la supply chain).',
|
||||
'Lasciando vuota una cella, il ruolo non ha responsabilità formalizzate su quell\'oggetto.'
|
||||
]
|
||||
},
|
||||
{
|
||||
heading: 'I collegamenti (link)',
|
||||
items: [
|
||||
'<strong>Procedura ↔ Inventario</strong>: quali asset sono coperti/governati da una procedura.',
|
||||
'<strong>Procedura ↔ Rischio</strong>: quali rischi una procedura contribuisce a trattare.',
|
||||
'<strong>Inventario ↔ Rischio</strong>: quali rischi insistono su un determinato asset.',
|
||||
'<strong>Rischio ↔ Misura</strong>: a quali misure ACN (per codice, es. GV.OC-04) un rischio è ricondotto.'
|
||||
]
|
||||
},
|
||||
{
|
||||
heading: 'Riferimenti normativi',
|
||||
items: [
|
||||
'<strong>GV.RR-02</strong>: ruoli, responsabilità e poteri per la cybersicurezza sono stabiliti e comunicati (base della RACI).',
|
||||
'<strong>Inventario</strong>: famiglia <strong>ID.AM</strong> (asset management) — censimento e responsabilità sugli asset.',
|
||||
'<strong>Procedure</strong>: <strong>GV.PO</strong> (policy/governance) — le procedure attuano le politiche.',
|
||||
'<strong>Rischi</strong>: <strong>art. 24 D.Lgs. 138/2024</strong> e famiglia <strong>ID.RA</strong> (risk assessment).'
|
||||
]
|
||||
}
|
||||
],
|
||||
references: [
|
||||
'NIST CSF 2.0 / GV.RR-02 - Ruoli, responsabilità e poteri stabiliti e comunicati',
|
||||
'Inventario: famiglia ID.AM (asset management)',
|
||||
'Procedure: GV.PO (policy/governance)',
|
||||
'Rischi: art. 24 D.Lgs. 138/2024 + famiglia ID.RA (risk assessment)',
|
||||
'NOTA: gli obblighi normativi in Italia derivano da NIS2 (Dir. UE 2022/2555) e dal D.Lgs. 138/2024; la matrice RACI e i framework NIST CSF/ACN sono strumenti di supporto, non un parere legale.'
|
||||
]
|
||||
},
|
||||
|
||||
// ─── Risk Management ─────────────────────────────────────────
|
||||
'risks': {
|
||||
title: 'Guida - Gestione Rischi',
|
||||
@@ -1118,6 +1159,8 @@ const HelpSystem = (function () {
|
||||
'organigramma': 'org',
|
||||
'competenze.html': 'competences',
|
||||
'competenze': 'competences',
|
||||
'raci.html': 'raci',
|
||||
'raci': 'raci',
|
||||
'risks.html': 'risks',
|
||||
'risks': 'risks',
|
||||
'incidents.html': 'incidents',
|
||||
|
||||
@@ -80,10 +80,12 @@ const I18n = (function () {
|
||||
'nav.management': { it: 'Gestione', en: 'Management' },
|
||||
'nav.org_chart': { it: 'Organigramma', en: 'Org Chart' },
|
||||
'nav.competences': { it: 'Competenze', en: 'Skills' },
|
||||
'nav.raci': { it: 'Matrice RACI', en: 'RACI Matrix' },
|
||||
'org.title': { it: 'Organigramma', en: 'Org Chart' },
|
||||
'org.subtitle': { it: 'Ruoli, responsabilità e organi di governance', en: 'Roles, responsibilities and governance bodies' },
|
||||
'org.new_role': { it: 'Nuovo ruolo', en: 'New role' },
|
||||
'comp.title': { it: 'Competenze', en: 'Skills' },
|
||||
'raci.title': { it: 'Matrice RACI', en: 'RACI Matrix' },
|
||||
'nav.risks': { it: 'Rischi', en: 'Risks' },
|
||||
'nav.incidents': { it: 'Incidenti', en: 'Incidents' },
|
||||
'nav.policies': { it: 'Policy', en: 'Policies' },
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* NIS2 Agile - Matrice RACI (A4 Fase 4.3)
|
||||
* 2 tab: Matrice RACI (ruoli × oggetti con celle R/A/C/I) · Collegamenti (link m2m).
|
||||
* Client api.raci*: ritorna `data`, lancia su success=false (api.js _acn).
|
||||
* I DELETE non hanno body → i parametri compositi viaggiano in query string (vedi api.js).
|
||||
* Ancoraggio: GV.RR-02 (ruoli/responsabilità), ID.AM (inventario), GV.PO (procedure),
|
||||
* art.24 D.Lgs. 138/2024 + ID.RA (rischi). Strumento di supporto, non un parere legale.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
let RACI = { roles: [], objects: [], assignments: {}, tab: 'matrix' };
|
||||
|
||||
function el(id) { return document.getElementById(id); }
|
||||
function esc(s) { const d = document.createElement('div'); d.textContent = (s == null ? '' : String(s)); return d.innerHTML; }
|
||||
|
||||
const OBJ_TYPE_LABELS = { inventory: 'Inventario', procedure: 'Procedura', risk: 'Rischio', supplier: 'Fornitore' };
|
||||
const RACI_LETTERS = ['R', 'A', 'C', 'I'];
|
||||
|
||||
// link_type → { label, aType, bType } (bType 'measure' = catalogo misure ACN).
|
||||
const LINK_TYPES = {
|
||||
procedure_inventory: { label: 'Procedura ↔ Inventario', aType: 'procedure', bType: 'inventory', aLabel: 'Procedura', bLabel: 'Inventario (asset)' },
|
||||
procedure_risk: { label: 'Procedura ↔ Rischio', aType: 'procedure', bType: 'risk', aLabel: 'Procedura', bLabel: 'Rischio' },
|
||||
inventory_risk: { label: 'Inventario ↔ Rischio', aType: 'inventory', bType: 'risk', aLabel: 'Inventario (asset)', bLabel: 'Rischio' },
|
||||
risk_measure: { label: 'Rischio ↔ Misura', aType: 'risk', bType: 'measure', aLabel: 'Rischio', bLabel: 'Misura ACN' }
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async function () {
|
||||
if (typeof checkAuth === 'function' && !checkAuth()) return;
|
||||
if (window.I18n && I18n.init) I18n.init('it');
|
||||
if (typeof loadSidebar === 'function') loadSidebar();
|
||||
if (window.HelpSystem && HelpSystem.init) HelpSystem.init();
|
||||
|
||||
await raciLoadMatrix();
|
||||
raciRenderMatrix();
|
||||
});
|
||||
|
||||
function raciKey(roleId, type, objId) { return roleId + '|' + type + '|' + objId; }
|
||||
|
||||
function raciTab(name) {
|
||||
RACI.tab = name;
|
||||
document.querySelectorAll('.cmp-tab').forEach(t => t.classList.toggle('active', t.dataset.tab === name));
|
||||
document.querySelectorAll('.cmp-panel').forEach(p => p.classList.remove('active'));
|
||||
const panel = el('panel-' + name);
|
||||
if (panel) panel.classList.add('active');
|
||||
if (name === 'links' && !RACI._linksInit) { RACI._linksInit = true; raciLinkTypeChanged(); }
|
||||
}
|
||||
|
||||
/* ════════════════ TAB 1 — Matrice RACI ════════════════ */
|
||||
async function raciLoadMatrix() {
|
||||
try {
|
||||
const m = await api.raciMatrix();
|
||||
RACI.roles = (m && m.roles) || [];
|
||||
RACI.objects = (m && m.objects) || [];
|
||||
RACI.assignments = {};
|
||||
((m && m.assignments) || []).forEach(function (a) {
|
||||
RACI.assignments[raciKey(a.role_id, a.object_type, a.object_id)] = a.raci;
|
||||
});
|
||||
} catch (e) {
|
||||
showNotification('Errore nel caricamento della matrice: ' + (e.message || e), 'error');
|
||||
RACI.roles = []; RACI.objects = []; RACI.assignments = {};
|
||||
}
|
||||
}
|
||||
|
||||
function raciRenderMatrix() {
|
||||
const wrap = el('raci-matrix');
|
||||
el('raci-matrix-summary').textContent = RACI.roles.length + ' ruoli · ' + RACI.objects.length + ' oggetti collegabili';
|
||||
if (!RACI.roles.length) {
|
||||
wrap.innerHTML = '<div class="cmp-empty">Nessun ruolo: creane nell\'Organigramma per popolare la matrice.</div>';
|
||||
return;
|
||||
}
|
||||
if (!RACI.objects.length) {
|
||||
wrap.innerHTML = '<div class="cmp-empty">Nessun oggetto collegabile (inventario, procedure, rischi, fornitori). Aggiungili nei rispettivi moduli.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Header: oggetti raggruppati per tipo
|
||||
let head = '<thead><tr><th class="raci-corner">Ruolo \\ Oggetto</th>';
|
||||
RACI.objects.forEach(function (o) {
|
||||
head += '<th class="raci-objhead"><span class="raci-objtype">' + esc(OBJ_TYPE_LABELS[o.type] || o.type) + '</span>' + esc(o.label) + '</th>';
|
||||
});
|
||||
head += '</tr></thead>';
|
||||
|
||||
let body = '<tbody>';
|
||||
RACI.roles.forEach(function (r) {
|
||||
body += '<tr><th class="raci-rolehead">' + esc(r.role_name) + (r.is_governance_body ? ' <span class="gap-role-sub">(governance)</span>' : '') + '</th>';
|
||||
RACI.objects.forEach(function (o) {
|
||||
const cur = RACI.assignments[raciKey(r.role_id, o.type, o.id)] || '';
|
||||
const cls = cur ? ' has-' + cur.toLowerCase() : '';
|
||||
let opts = '<option value="">—</option>';
|
||||
RACI_LETTERS.forEach(function (L) { opts += '<option value="' + L + '"' + (cur === L ? ' selected' : '') + '>' + L + '</option>'; });
|
||||
const aria = 'RACI per ruolo ' + (r.role_name || '') + ' su oggetto ' + (o.label || '');
|
||||
body += '<td><select class="raci-cell-sel' + cls + '" aria-label="' + esc(aria) + '" '
|
||||
+ 'onchange="raciSetCell(' + r.role_id + ',\'' + o.type + '\',' + o.id + ',this.value,this)">' + opts + '</select></td>';
|
||||
});
|
||||
body += '</tr>';
|
||||
});
|
||||
body += '</tbody>';
|
||||
|
||||
wrap.innerHTML = '<div class="raci-matrix-wrap"><table class="raci-matrix">' + head + body + '</table></div>';
|
||||
}
|
||||
|
||||
async function raciSetCell(roleId, type, objId, value, selEl) {
|
||||
const prev = RACI.assignments[raciKey(roleId, type, objId)] || '';
|
||||
try {
|
||||
if (value) {
|
||||
await api.raciAssign({ role_id: roleId, object_type: type, object_id: objId, raci: value });
|
||||
RACI.assignments[raciKey(roleId, type, objId)] = value;
|
||||
showNotification('Responsabilità ' + value + ' assegnata.', 'success');
|
||||
} else {
|
||||
await api.raciUnassign({ role_id: roleId, object_type: type, object_id: objId });
|
||||
delete RACI.assignments[raciKey(roleId, type, objId)];
|
||||
showNotification('Responsabilità rimossa.', 'success');
|
||||
}
|
||||
if (selEl) {
|
||||
selEl.classList.remove('has-r', 'has-a', 'has-c', 'has-i');
|
||||
if (value) selEl.classList.add('has-' + value.toLowerCase());
|
||||
}
|
||||
} catch (e) {
|
||||
// ripristina il valore precedente in caso di errore
|
||||
if (selEl) {
|
||||
selEl.value = prev;
|
||||
selEl.classList.remove('has-r', 'has-a', 'has-c', 'has-i');
|
||||
if (prev) selEl.classList.add('has-' + prev.toLowerCase());
|
||||
}
|
||||
showNotification(e.message || 'Errore.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/* ════════════════ TAB 2 — Collegamenti (link m2m) ════════════════ */
|
||||
function currentLinkType() { return el('raci-linktype').value; }
|
||||
|
||||
async function raciLinkTypeChanged() {
|
||||
const lt = currentLinkType();
|
||||
const cfg = LINK_TYPES[lt];
|
||||
if (!cfg) return;
|
||||
el('raci-alabel').textContent = cfg.aLabel;
|
||||
el('raci-blabel').textContent = cfg.bLabel;
|
||||
const aSel = el('raci-aside');
|
||||
const bSel = el('raci-bside');
|
||||
aSel.innerHTML = '<option value="">— Caricamento… —</option>';
|
||||
bSel.innerHTML = '<option value="">— Caricamento… —</option>';
|
||||
try {
|
||||
const [aList, bList] = await Promise.all([api.raciObjects(cfg.aType), api.raciObjects(cfg.bType)]);
|
||||
aSel.innerHTML = raciObjOptions(aList, cfg.aType);
|
||||
bSel.innerHTML = raciObjOptions(bList, cfg.bType);
|
||||
} catch (e) {
|
||||
showNotification('Errore nel caricamento degli oggetti: ' + (e.message || e), 'error');
|
||||
aSel.innerHTML = '<option value="">— Errore —</option>';
|
||||
bSel.innerHTML = '<option value="">— Errore —</option>';
|
||||
}
|
||||
await raciLoadLinks();
|
||||
}
|
||||
|
||||
function raciObjOptions(list, type) {
|
||||
list = Array.isArray(list) ? list : [];
|
||||
if (!list.length) return '<option value="">— Nessun elemento —</option>';
|
||||
// misure: value = code (string); altri: value = id (int)
|
||||
return list.map(function (o) {
|
||||
const v = (type === 'measure') ? o.code : o.id;
|
||||
const label = (type === 'measure') ? o.label : o.label;
|
||||
return '<option value="' + esc(v) + '">' + esc(label) + '</option>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function raciLoadLinks() {
|
||||
const lt = currentLinkType();
|
||||
const cfg = LINK_TYPES[lt];
|
||||
const aId = el('raci-aside').value;
|
||||
const wrap = el('raci-links');
|
||||
if (!aId) { wrap.innerHTML = '<div class="cmp-empty">Seleziona un elemento "Lato A" per vederne i collegamenti.</div>'; return; }
|
||||
wrap.innerHTML = '<div class="cmp-empty">Caricamento…</div>';
|
||||
try {
|
||||
const links = await api.raciLinks(lt, aId);
|
||||
const rows = (Array.isArray(links) ? links : []);
|
||||
if (!rows.length) { wrap.innerHTML = '<div class="cmp-empty">Nessun collegamento per questo elemento.</div>'; return; }
|
||||
const body = rows.map(function (lk) {
|
||||
// risk_measure: { link_id, measure_code, measure_label }; altri: { link_id, b_id, b_label }
|
||||
const bId = (lt === 'risk_measure') ? lk.measure_code : lk.b_id;
|
||||
const bLabel = (lt === 'risk_measure') ? lk.measure_label : lk.b_label;
|
||||
return '<tr>'
|
||||
+ '<td>' + esc(cfg.bLabel) + '</td>'
|
||||
+ '<td><strong>' + esc(bLabel) + '</strong></td>'
|
||||
+ '<td style="text-align:right;"><button class="btn btn-outline btn-sm" onclick="raciRemoveLink(\'' + esc(String(bId)) + '\')">Rimuovi</button></td>'
|
||||
+ '</tr>';
|
||||
}).join('');
|
||||
wrap.innerHTML = '<table class="raci-linklist"><thead><tr><th>Tipo</th><th>Elemento collegato</th><th></th></tr></thead><tbody>' + body + '</tbody></table>';
|
||||
} catch (e) {
|
||||
wrap.innerHTML = '<div class="cmp-empty">Errore: ' + esc(e.message || e) + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function raciAddLink() {
|
||||
const lt = currentLinkType();
|
||||
const aId = el('raci-aside').value;
|
||||
const bId = el('raci-bside').value;
|
||||
if (!aId || !bId) { showNotification('Seleziona entrambi gli elementi.', 'error'); return; }
|
||||
try {
|
||||
await api.raciLink({ link_type: lt, a_id: aId, b_id: bId });
|
||||
showNotification('Collegamento creato.', 'success');
|
||||
await raciLoadLinks();
|
||||
} catch (e) { showNotification(e.message || 'Errore.', 'error'); }
|
||||
}
|
||||
|
||||
async function raciRemoveLink(bId) {
|
||||
const lt = currentLinkType();
|
||||
const aId = el('raci-aside').value;
|
||||
if (!confirm('Rimuovere questo collegamento?')) return;
|
||||
try {
|
||||
await api.raciUnlink({ link_type: lt, a_id: aId, b_id: bId });
|
||||
showNotification('Collegamento rimosso.', 'success');
|
||||
await raciLoadLinks();
|
||||
} catch (e) { showNotification(e.message || 'Errore.', 'error'); }
|
||||
}
|
||||
Reference in New Issue
Block a user