[FEAT] Services API, Webhook, Whistleblowing, Normative + integrazioni

Sprint completo — prodotto presentation-ready:

Services API (read-only, API Key + scope):
- GET /api/services/status|compliance-summary|risks-feed|incidents-feed
- GET /api/services/controls-status|assets-critical|suppliers-risk|policies-approved
- GET /api/services/openapi (spec OpenAPI 3.0.3 JSON)

Webhook Outbound (Stripe-like HMAC-SHA256):
- CRUD api_keys + webhook_subscriptions (Settings → 2 nuovi tab)
- WebhookService: retry 3x backoff (0s/5min/30min), delivery log
- Trigger auto in IncidentController, RiskController, PolicyController
- Delivery log, test ping, processRetry

Nuovi moduli:
- WhistleblowingController (Art.32 NIS2): anonimato garantito, timeline, token tracking
- NormativeController: feed NIS2/ACN/DORA con ACK tracciato per audit

Frontend:
- whistleblowing.html: form submit anonimo/firmato + gestione CISO
- normative.html: feed con presa visione documentata + progress bar ACK
- public/docs/api.html: documentazione API dark theme (Swagger-like)
- settings.html: tab API Keys + tab Webhook
- integrations/: guide per lg231, SustainAI, AllRisk, SIEM (widget + codice)
- Sidebar: Segnalazioni + Normative aggiunte a common.js

DB: migration 007 (api_keys, webhook_subscriptions, webhook_deliveries),
    008 (whistleblowing_reports + timeline),
    009 (normative_updates + normative_ack + seed NIS2/ACN/DORA/ISO)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-03-07 13:20:24 +01:00
co-authored by Claude Sonnet 4.6
parent 3f4b451e2a
commit 86e9bdded2
22 changed files with 5080 additions and 10 deletions
+710
View File
@@ -0,0 +1,710 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NIS2 Agile — API Reference</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0f172a; --bg2: #1e293b; --bg3: #334155;
--primary: #06b6d4; --primary-dark: #0891b2;
--green: #10b981; --yellow: #f59e0b; --red: #ef4444; --purple: #8b5cf6;
--text: #e2e8f0; --text-muted: #94a3b8;
--border: #334155;
--radius: 6px;
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
--mono: 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
}
body { background: var(--bg); color: var(--text); font-family: var(--font); line-height: 1.6; }
/* Layout */
.layout { display: flex; min-height: 100vh; }
.sidebar { width: 280px; flex-shrink: 0; background: var(--bg2); border-right: 1px solid var(--border); position: sticky; top: 0; height: 100vh; overflow-y: auto; padding: 24px 0; }
.content { flex: 1; max-width: 900px; padding: 40px 48px; }
/* Sidebar */
.sidebar-logo { padding: 0 20px 24px; border-bottom: 1px solid var(--border); margin-bottom: 16px; }
.sidebar-logo h1 { font-size: 1.125rem; font-weight: 700; color: var(--primary); }
.sidebar-logo p { font-size: 0.75rem; color: var(--text-muted); margin-top: 2px; }
.sidebar-section { padding: 8px 20px 4px; font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted); font-weight: 700; margin-top: 12px; }
.sidebar-link { display: flex; align-items: center; gap: 10px; padding: 7px 20px; font-size: 0.8125rem; color: var(--text-muted); text-decoration: none; transition: all 0.15s; cursor: pointer; border-left: 2px solid transparent; }
.sidebar-link:hover { color: var(--text); background: rgba(255,255,255,0.04); }
.sidebar-link.active { color: var(--primary); border-left-color: var(--primary); background: rgba(6,182,212,0.08); }
.method-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
.dot-get { background: var(--green); }
.dot-post { background: var(--yellow); }
.dot-put { background: var(--purple); }
.dot-delete { background: var(--red); }
/* Header */
.page-header { margin-bottom: 48px; padding-bottom: 32px; border-bottom: 1px solid var(--border); }
.page-header h1 { font-size: 2rem; font-weight: 800; color: var(--text); margin-bottom: 8px; }
.page-header p { color: var(--text-muted); font-size: 1rem; }
.version-badge { display: inline-flex; align-items: center; gap: 8px; padding: 4px 12px; background: rgba(6,182,212,0.15); border: 1px solid rgba(6,182,212,0.3); border-radius: 20px; font-size: 0.75rem; color: var(--primary); margin-top: 12px; }
/* Section */
.section { margin-bottom: 60px; }
.section-title { font-size: 1.375rem; font-weight: 700; color: var(--text); margin-bottom: 8px; padding-bottom: 12px; border-bottom: 1px solid var(--border); }
.section-desc { color: var(--text-muted); font-size: 0.875rem; margin-bottom: 24px; }
/* Endpoint */
.endpoint { margin-bottom: 28px; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
.endpoint-header { display: flex; align-items: center; gap: 12px; padding: 14px 18px; background: var(--bg2); cursor: pointer; user-select: none; }
.endpoint-header:hover { background: var(--bg3); }
.method-badge { padding: 3px 10px; border-radius: 4px; font-size: 0.6875rem; font-weight: 800; font-family: var(--mono); letter-spacing: 0.05em; flex-shrink: 0; }
.method-get { background: rgba(16,185,129,0.15); color: var(--green); border: 1px solid rgba(16,185,129,0.3); }
.method-post { background: rgba(245,158,11,0.15); color: var(--yellow); border: 1px solid rgba(245,158,11,0.3); }
.method-put { background: rgba(139,92,246,0.15); color: var(--purple); border: 1px solid rgba(139,92,246,0.3); }
.method-delete { background: rgba(239,68,68,0.15); color: var(--red); border: 1px solid rgba(239,68,68,0.3); }
.endpoint-path { font-family: var(--mono); font-size: 0.875rem; color: var(--text); flex: 1; }
.endpoint-path span { color: var(--primary); }
.endpoint-summary { font-size: 0.8125rem; color: var(--text-muted); margin-left: auto; }
.endpoint-body { padding: 20px 18px; border-top: 1px solid var(--border); display: none; }
.endpoint-body.open { display: block; }
.endpoint-desc { font-size: 0.875rem; color: var(--text-muted); margin-bottom: 16px; }
/* Auth tag */
.auth-tag { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: 20px; font-size: 0.7rem; font-weight: 600; margin-left: 8px; }
.auth-public { background: rgba(16,185,129,0.1); color: var(--green); border: 1px solid rgba(16,185,129,0.2); }
.auth-apikey { background: rgba(6,182,212,0.1); color: var(--primary); border: 1px solid rgba(6,182,212,0.2); }
.auth-jwt { background: rgba(139,92,246,0.1); color: var(--purple); border: 1px solid rgba(139,92,246,0.2); }
/* Params table */
.params-title { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-muted); font-weight: 700; margin-bottom: 10px; margin-top: 16px; }
.params-table { width: 100%; border-collapse: collapse; font-size: 0.8125rem; }
.params-table th { padding: 8px 12px; background: var(--bg3); color: var(--text-muted); font-weight: 600; text-align: left; font-size: 0.75rem; }
.params-table td { padding: 8px 12px; border-bottom: 1px solid var(--border); vertical-align: top; }
.param-name { font-family: var(--mono); color: var(--primary); font-size: 0.8rem; }
.param-required { color: var(--red); font-size: 0.7rem; margin-left: 4px; }
.param-type { font-family: var(--mono); color: var(--text-muted); font-size: 0.75rem; }
/* Code */
pre { background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; overflow-x: auto; font-size: 0.8125rem; font-family: var(--mono); line-height: 1.7; margin-top: 8px; }
code { font-family: var(--mono); font-size: 0.85em; background: rgba(255,255,255,0.06); padding: 1px 6px; border-radius: 3px; }
.json-key { color: #7dd3fc; }
.json-str { color: #86efac; }
.json-num { color: #fbbf24; }
.json-bool { color: #c4b5fd; }
.json-null { color: #94a3b8; }
/* Info boxes */
.info-box { padding: 14px 16px; border-radius: var(--radius); margin-bottom: 16px; font-size: 0.8125rem; }
.info-box.warning { background: rgba(245,158,11,0.1); border-left: 3px solid var(--yellow); color: #fde68a; }
.info-box.info { background: rgba(6,182,212,0.1); border-left: 3px solid var(--primary); color: #a5f3fc; }
.info-box.success { background: rgba(16,185,129,0.1); border-left: 3px solid var(--green); color: #a7f3d0; }
/* Base URL */
.base-url { padding: 12px 16px; background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); font-family: var(--mono); font-size: 0.875rem; color: var(--primary); margin-bottom: 24px; }
/* Scope table */
.scope-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 10px; }
.scope-card { padding: 12px 14px; background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); }
.scope-name { font-family: var(--mono); font-size: 0.8rem; color: var(--primary); margin-bottom: 4px; }
.scope-desc { font-size: 0.75rem; color: var(--text-muted); }
/* Responsive */
@media (max-width: 900px) {
.sidebar { display: none; }
.content { padding: 24px; }
}
</style>
</head>
<body>
<div class="layout">
<!-- Sidebar -->
<nav class="sidebar">
<div class="sidebar-logo">
<h1>NIS2 Agile API</h1>
<p>Reference v1.0.0</p>
</div>
<div class="sidebar-section">Introduzione</div>
<a class="sidebar-link active" onclick="scrollTo('intro')">Panoramica</a>
<a class="sidebar-link" onclick="scrollTo('auth')">Autenticazione</a>
<a class="sidebar-link" onclick="scrollTo('errors')">Errori</a>
<div class="sidebar-section">Services API</div>
<a class="sidebar-link" onclick="scrollTo('svc-status')"><span class="method-dot dot-get"></span>Status</a>
<a class="sidebar-link" onclick="scrollTo('svc-compliance')"><span class="method-dot dot-get"></span>Compliance Summary</a>
<a class="sidebar-link" onclick="scrollTo('svc-risks')"><span class="method-dot dot-get"></span>Risks Feed</a>
<a class="sidebar-link" onclick="scrollTo('svc-incidents')"><span class="method-dot dot-get"></span>Incidents Feed</a>
<a class="sidebar-link" onclick="scrollTo('svc-controls')"><span class="method-dot dot-get"></span>Controls Status</a>
<a class="sidebar-link" onclick="scrollTo('svc-assets')"><span class="method-dot dot-get"></span>Assets Critical</a>
<a class="sidebar-link" onclick="scrollTo('svc-suppliers')"><span class="method-dot dot-get"></span>Suppliers Risk</a>
<a class="sidebar-link" onclick="scrollTo('svc-policies')"><span class="method-dot dot-get"></span>Policies Approved</a>
<div class="sidebar-section">API Keys</div>
<a class="sidebar-link" onclick="scrollTo('keys-list')"><span class="method-dot dot-get"></span>Lista Keys</a>
<a class="sidebar-link" onclick="scrollTo('keys-create')"><span class="method-dot dot-post"></span>Crea Key</a>
<a class="sidebar-link" onclick="scrollTo('keys-delete')"><span class="method-dot dot-delete"></span>Revoca Key</a>
<div class="sidebar-section">Webhook</div>
<a class="sidebar-link" onclick="scrollTo('wh-list')"><span class="method-dot dot-get"></span>Lista Subscriptions</a>
<a class="sidebar-link" onclick="scrollTo('wh-create')"><span class="method-dot dot-post"></span>Crea Subscription</a>
<a class="sidebar-link" onclick="scrollTo('wh-update')"><span class="method-dot dot-put"></span>Aggiorna</a>
<a class="sidebar-link" onclick="scrollTo('wh-delete')"><span class="method-dot dot-delete"></span>Elimina</a>
<a class="sidebar-link" onclick="scrollTo('wh-test')"><span class="method-dot dot-post"></span>Test Ping</a>
<a class="sidebar-link" onclick="scrollTo('wh-deliveries')"><span class="method-dot dot-get"></span>Delivery Log</a>
<div class="sidebar-section">Webhook Events</div>
<a class="sidebar-link" onclick="scrollTo('wh-events')">Catalogo eventi</a>
<a class="sidebar-link" onclick="scrollTo('wh-signature')">Verifica firma</a>
</nav>
<!-- Content -->
<main class="content">
<!-- INTRO -->
<div id="intro" class="page-header">
<h1>NIS2 Agile API</h1>
<p>API REST per integrare NIS2 Agile con SIEM, GRC, piattaforme ESG e strumenti di compliance.</p>
<div class="version-badge">
<svg viewBox="0 0 20 20" fill="currentColor" width="12" height="12"><path fill-rule="evenodd" d="M10 2a8 8 0 100 16A8 8 0 0010 2zm0 14a6 6 0 110-12 6 6 0 010 12zm-1-5a1 1 0 012 0v2a1 1 0 11-2 0v-2zm0-4a1 1 0 112 0 1 1 0 01-2 0z" clip-rule="evenodd"/></svg>
v1.0.0 — Produzione: https://nis2.certisource.it/
</div>
</div>
<div class="section">
<h2 class="section-title">Panoramica</h2>
<p class="section-desc">NIS2 Agile espone due famiglie di API: <strong>Services API</strong> per lettura dati di compliance in tempo reale, e <strong>Webhook API</strong> per notifiche push su eventi NIS2.</p>
<div class="base-url">Base URL: https://nis2.certisource.it/api</div>
<div class="info-box info">
<strong>Rate Limiting:</strong> Services API: 100 richieste/ora per API Key. Webhook delivery: max 1000/ora per subscription.
</div>
<div class="info-box success">
<strong>Formato risposta:</strong> Tutte le risposte sono JSON con envelope <code>{ "success": bool, "data": {}, "message": "..." }</code>
</div>
</div>
<!-- AUTH -->
<div id="auth" class="section">
<h2 class="section-title">Autenticazione</h2>
<p class="section-desc">Le Services API usano API Keys con scope granulari. Le API di gestione (webhook, key management) usano JWT Bearer token della sessione utente.</p>
<h3 class="params-title">API Key — 3 modalità</h3>
<pre><code class="json-str"># Header (raccomandato)
X-API-Key: nis2_abc123def456...
# Authorization Bearer
Authorization: Bearer nis2_abc123def456...
# Query string (sconsigliato in prod)
GET /api/services/risks-feed?api_key=nis2_abc123def456...</code></pre>
<h3 class="params-title">JWT Bearer — Per gestione API</h3>
<pre><code class="json-str">Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...</code></pre>
<h3 class="params-title" style="margin-top:20px;">Scope disponibili</h3>
<div class="scope-grid">
<div class="scope-card"><div class="scope-name">read:all</div><div class="scope-desc">Accesso completo in lettura a tutti i dati</div></div>
<div class="scope-card"><div class="scope-name">read:compliance</div><div class="scope-desc">Compliance score e controlli Art.21</div></div>
<div class="scope-card"><div class="scope-name">read:risks</div><div class="scope-desc">Risk register e matrice rischi ISO 27005</div></div>
<div class="scope-card"><div class="scope-name">read:incidents</div><div class="scope-desc">Incidenti e timeline Art.23</div></div>
<div class="scope-card"><div class="scope-name">read:assets</div><div class="scope-desc">Inventario asset critici</div></div>
<div class="scope-card"><div class="scope-name">read:supply_chain</div><div class="scope-desc">Supply chain e rischio fornitori</div></div>
<div class="scope-card"><div class="scope-name">read:policies</div><div class="scope-desc">Policy approvate</div></div>
</div>
<h3 class="params-title" style="margin-top:20px;">Header obbligatorio</h3>
<pre><code class="json-str">X-Organization-Id: 42 # ID organizzazione target</code></pre>
</div>
<!-- ERRORS -->
<div id="errors" class="section">
<h2 class="section-title">Gestione Errori</h2>
<table class="params-table">
<thead><tr><th>HTTP</th><th>error_code</th><th>Descrizione</th></tr></thead>
<tbody>
<tr><td>400</td><td><code>VALIDATION_ERROR</code></td><td>Parametri obbligatori mancanti o non validi</td></tr>
<tr><td>401</td><td><code>UNAUTHORIZED</code></td><td>API Key assente, scaduta o non valida</td></tr>
<tr><td>403</td><td><code>INSUFFICIENT_SCOPE</code></td><td>Scope insufficiente per questa risorsa</td></tr>
<tr><td>404</td><td><code>NOT_FOUND</code></td><td>Risorsa non trovata</td></tr>
<tr><td>429</td><td><code>RATE_LIMITED</code></td><td>Limite richieste superato</td></tr>
<tr><td>500</td><td><code>INTERNAL_ERROR</code></td><td>Errore interno del server</td></tr>
</tbody>
</table>
<pre><span class="json-key">{
"success"</span><span class="json-str">: false</span>,
<span class="json-key">"message"</span>: <span class="json-str">"Scope insufficiente: richiesto read:risks"</span>,
<span class="json-key">"error_code"</span>: <span class="json-str">"INSUFFICIENT_SCOPE"</span>
}</pre>
</div>
<!-- SERVICES API -->
<div class="section">
<h2 class="section-title">Services API</h2>
<p class="section-desc">Endpoint GET per leggere dati di compliance da sistemi esterni. Autenticazione tramite API Key con scope granulari.</p>
<!-- Status -->
<div id="svc-status" class="endpoint">
<div class="endpoint-header" onclick="toggle(this)">
<span class="method-badge method-get">GET</span>
<span class="endpoint-path">/services/status</span>
<span class="auth-tag auth-public">Public</span>
<span class="endpoint-summary">Health check piattaforma</span>
</div>
<div class="endpoint-body">
<p class="endpoint-desc">Stato della piattaforma, versione API e timestamp. Non richiede autenticazione.</p>
<h4 class="params-title">Risposta 200</h4>
<pre><span class="json-key">{
"status"</span>: <span class="json-str">"ok"</span>,
<span class="json-key">"version"</span>: <span class="json-str">"1.0.0"</span>,
<span class="json-key">"timestamp"</span>: <span class="json-num">1709900400</span>,
<span class="json-key">"platform"</span>: <span class="json-str">"NIS2 Agile"</span>
}</pre>
</div>
</div>
<!-- Compliance Summary -->
<div id="svc-compliance" class="endpoint">
<div class="endpoint-header" onclick="toggle(this)">
<span class="method-badge method-get">GET</span>
<span class="endpoint-path">/services/compliance-summary</span>
<span class="auth-tag auth-apikey">read:compliance</span>
</div>
<div class="endpoint-body">
<p class="endpoint-desc">Score di conformità NIS2 aggregato per dominio Art.21, con statistiche rischi, incidenti aperti e policy approvate.</p>
<h4 class="params-title">Risposta 200</h4>
<pre><span class="json-key">{
"overall_score"</span>: <span class="json-num">73</span>,
<span class="json-key">"label"</span>: <span class="json-str">"substantially_compliant"</span>,
<span class="json-key">"domain_scores"</span>: [
{ <span class="json-key">"category"</span>: <span class="json-str">"governance"</span>, <span class="json-key">"score"</span>: <span class="json-num">80</span>, <span class="json-key">"answered"</span>: <span class="json-num">8</span>, <span class="json-key">"total"</span>: <span class="json-num">10</span> }
],
<span class="json-key">"risks"</span>: { <span class="json-key">"total"</span>: <span class="json-num">24</span>, <span class="json-key">"critical"</span>: <span class="json-num">2</span>, <span class="json-key">"high"</span>: <span class="json-num">5</span> },
<span class="json-key">"incidents"</span>: { <span class="json-key">"open"</span>: <span class="json-num">3</span>, <span class="json-key">"significant"</span>: <span class="json-num">1</span> },
<span class="json-key">"policies"</span>: { <span class="json-key">"approved"</span>: <span class="json-num">12</span>, <span class="json-key">"draft"</span>: <span class="json-num">4</span> }
}</pre>
</div>
</div>
<!-- Risks Feed -->
<div id="svc-risks" class="endpoint">
<div class="endpoint-header" onclick="toggle(this)">
<span class="method-badge method-get">GET</span>
<span class="endpoint-path">/services/risks-feed</span>
<span class="auth-tag auth-apikey">read:risks</span>
</div>
<div class="endpoint-body">
<p class="endpoint-desc">Feed rischi con filtri su livello, area NIS2, stato e data. Include deadline di trattamento.</p>
<h4 class="params-title">Query Parameters</h4>
<table class="params-table">
<thead><tr><th>Parametro</th><th>Tipo</th><th>Descrizione</th></tr></thead>
<tbody>
<tr><td><code class="param-name">level</code></td><td><code class="param-type">string</code></td><td>Filtro livello: <code>critical</code>, <code>high</code>, <code>medium</code>, <code>low</code></td></tr>
<tr><td><code class="param-name">area</code></td><td><code class="param-type">string</code></td><td>Categoria rischio (es. <code>network_security</code>)</td></tr>
<tr><td><code class="param-name">status</code></td><td><code class="param-type">string</code></td><td>Stato: <code>open</code>, <code>in_treatment</code>, <code>closed</code></td></tr>
<tr><td><code class="param-name">from</code></td><td><code class="param-type">datetime</code></td><td>Filtra da data ISO (es. <code>2026-01-01T00:00:00</code>)</td></tr>
<tr><td><code class="param-name">limit</code></td><td><code class="param-type">integer</code></td><td>Max risultati (default 50, max 200)</td></tr>
</tbody>
</table>
<h4 class="params-title">Risposta 200</h4>
<pre><span class="json-key">{
"risks"</span>: [
{
<span class="json-key">"id"</span>: <span class="json-num">15</span>,
<span class="json-key">"risk_code"</span>: <span class="json-str">"RSK-2026-015"</span>,
<span class="json-key">"title"</span>: <span class="json-str">"Vulnerabilità VPN senza MFA"</span>,
<span class="json-key">"category"</span>: <span class="json-str">"network_security"</span>,
<span class="json-key">"risk_level"</span>: <span class="json-str">"high"</span>,
<span class="json-key">"risk_score"</span>: <span class="json-num">15</span>,
<span class="json-key">"treatment"</span>: <span class="json-str">"mitigate"</span>,
<span class="json-key">"nis2_article"</span>: <span class="json-str">"21.2.i"</span>,
<span class="json-key">"created_at"</span>: <span class="json-str">"2026-01-15T10:30:00+01:00"</span>
}
],
<span class="json-key">"total"</span>: <span class="json-num">24</span>, <span class="json-key">"filters"</span>: { <span class="json-key">"level"</span>: <span class="json-str">"high"</span> }
}</pre>
</div>
</div>
<!-- Incidents Feed -->
<div id="svc-incidents" class="endpoint">
<div class="endpoint-header" onclick="toggle(this)">
<span class="method-badge method-get">GET</span>
<span class="endpoint-path">/services/incidents-feed</span>
<span class="auth-tag auth-apikey">read:incidents</span>
</div>
<div class="endpoint-body">
<p class="endpoint-desc">Feed incidenti con status Art.23 (deadlines 24h/72h/30d) e flag di scadenza. Utile per integrare in SIEM e SOC dashboard.</p>
<h4 class="params-title">Query Parameters</h4>
<table class="params-table">
<thead><tr><th>Parametro</th><th>Tipo</th><th>Descrizione</th></tr></thead>
<tbody>
<tr><td><code class="param-name">status</code></td><td><code class="param-type">string</code></td><td><code>open</code>, <code>investigating</code>, <code>resolved</code>, <code>closed</code></td></tr>
<tr><td><code class="param-name">severity</code></td><td><code class="param-type">string</code></td><td><code>critical</code>, <code>high</code>, <code>medium</code>, <code>low</code></td></tr>
<tr><td><code class="param-name">significant_only</code></td><td><code class="param-type">boolean</code></td><td>Solo incidenti Art.23 significativi</td></tr>
<tr><td><code class="param-name">from</code></td><td><code class="param-type">datetime</code></td><td>Filtra da data ISO</td></tr>
</tbody>
</table>
<h4 class="params-title">Risposta 200 — Elemento</h4>
<pre><span class="json-key">{
"id"</span>: <span class="json-num">7</span>,
<span class="json-key">"incident_code"</span>: <span class="json-str">"INC-2026-007"</span>,
<span class="json-key">"title"</span>: <span class="json-str">"Accesso non autorizzato sistema ERP"</span>,
<span class="json-key">"severity"</span>: <span class="json-str">"high"</span>,
<span class="json-key">"is_significant"</span>: <span class="json-bool">true</span>,
<span class="json-key">"art23_status"</span>: {
<span class="json-key">"early_warning"</span>: { <span class="json-key">"due"</span>: <span class="json-str">"2026-02-21T14:00:00+01:00"</span>, <span class="json-key">"sent"</span>: <span class="json-bool">true</span>, <span class="json-key">"overdue"</span>: <span class="json-bool">false</span> },
<span class="json-key">"notification"</span>: { <span class="json-key">"due"</span>: <span class="json-str">"2026-02-23T14:00:00+01:00"</span>, <span class="json-key">"sent"</span>: <span class="json-bool">false</span>, <span class="json-key">"overdue"</span>: <span class="json-bool">false</span> },
<span class="json-key">"final_report"</span>: { <span class="json-key">"due"</span>: <span class="json-str">"2026-03-22T14:00:00+01:00"</span>, <span class="json-key">"sent"</span>: <span class="json-bool">false</span>, <span class="json-key">"overdue"</span>: <span class="json-bool">false</span> }
}
}</pre>
</div>
</div>
<!-- Controls Status -->
<div id="svc-controls" class="endpoint">
<div class="endpoint-header" onclick="toggle(this)">
<span class="method-badge method-get">GET</span>
<span class="endpoint-path">/services/controls-status</span>
<span class="auth-tag auth-apikey">read:compliance</span>
</div>
<div class="endpoint-body">
<p class="endpoint-desc">Stato dei controlli di sicurezza Art.21 raggruppati per categoria (governance, network_security, access_control, ecc.).</p>
<h4 class="params-title">Risposta 200 — Elemento categoria</h4>
<pre><span class="json-key">{
"category"</span>: <span class="json-str">"network_security"</span>,
<span class="json-key">"total"</span>: <span class="json-num">8</span>,
<span class="json-key">"implemented"</span>: <span class="json-num">5</span>,
<span class="json-key">"partial"</span>: <span class="json-num">2</span>,
<span class="json-key">"not_implemented"</span>: <span class="json-num">1</span>,
<span class="json-key">"score"</span>: <span class="json-num">75</span>
}</pre>
</div>
</div>
<!-- Assets Critical -->
<div id="svc-assets" class="endpoint">
<div class="endpoint-header" onclick="toggle(this)">
<span class="method-badge method-get">GET</span>
<span class="endpoint-path">/services/assets-critical</span>
<span class="auth-tag auth-apikey">read:assets</span>
</div>
<div class="endpoint-body">
<p class="endpoint-desc">Inventario asset critici con tipo, livello di criticità e dipendenze. Filtrabili per tipo e livello.</p>
<h4 class="params-title">Query Parameters</h4>
<table class="params-table">
<thead><tr><th>Parametro</th><th>Tipo</th><th>Descrizione</th></tr></thead>
<tbody>
<tr><td><code class="param-name">type</code></td><td><code class="param-type">string</code></td><td><code>server</code>, <code>network</code>, <code>software</code>, <code>data</code>, <code>service</code></td></tr>
<tr><td><code class="param-name">criticality</code></td><td><code class="param-type">string</code></td><td><code>critical</code>, <code>high</code>, <code>medium</code>, <code>low</code></td></tr>
</tbody>
</table>
</div>
</div>
<!-- Suppliers Risk -->
<div id="svc-suppliers" class="endpoint">
<div class="endpoint-header" onclick="toggle(this)">
<span class="method-badge method-get">GET</span>
<span class="endpoint-path">/services/suppliers-risk</span>
<span class="auth-tag auth-apikey">read:supply_chain</span>
</div>
<div class="endpoint-body">
<p class="endpoint-desc">Panoramica rischio fornitori con risk_score, data ultima valutazione e flag critici. Include stats aggregate.</p>
<h4 class="params-title">Risposta 200 — Stats</h4>
<pre><span class="json-key">{
"suppliers"</span>: [...],
<span class="json-key">"stats"</span>: {
<span class="json-key">"total"</span>: <span class="json-num">18</span>,
<span class="json-key">"critical"</span>: <span class="json-num">2</span>,
<span class="json-key">"high"</span>: <span class="json-num">4</span>,
<span class="json-key">"avg_risk_score"</span>: <span class="json-num">42</span>
}
}</pre>
</div>
</div>
<!-- Policies Approved -->
<div id="svc-policies" class="endpoint">
<div class="endpoint-header" onclick="toggle(this)">
<span class="method-badge method-get">GET</span>
<span class="endpoint-path">/services/policies-approved</span>
<span class="auth-tag auth-apikey">read:policies</span>
</div>
<div class="endpoint-body">
<p class="endpoint-desc">Policy approvate con categoria, articolo NIS2 di riferimento e versione. Opzionalmente include il testo completo.</p>
<h4 class="params-title">Query Parameters</h4>
<table class="params-table">
<thead><tr><th>Parametro</th><th>Tipo</th><th>Descrizione</th></tr></thead>
<tbody>
<tr><td><code class="param-name">category</code></td><td><code class="param-type">string</code></td><td>Filtra per categoria (es. <code>incident_response</code>)</td></tr>
<tr><td><code class="param-name">include_content</code></td><td><code class="param-type">boolean</code></td><td>Include testo policy (default: false)</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- API KEYS MANAGEMENT -->
<div class="section">
<h2 class="section-title">Gestione API Keys</h2>
<p class="section-desc">CRUD API Keys. Richiede autenticazione JWT con ruolo <code>org_admin</code>.</p>
<div id="keys-list" class="endpoint">
<div class="endpoint-header" onclick="toggle(this)">
<span class="method-badge method-get">GET</span>
<span class="endpoint-path">/webhooks/api-keys</span>
<span class="auth-tag auth-jwt">JWT</span>
<span class="endpoint-summary">Lista API Keys dell'organizzazione</span>
</div>
<div class="endpoint-body">
<p class="endpoint-desc">Restituisce tutte le API Keys (attive e revocate) dell'organizzazione corrente. Non espone mai il testo completo della chiave.</p>
</div>
</div>
<div id="keys-create" class="endpoint">
<div class="endpoint-header" onclick="toggle(this)">
<span class="method-badge method-post">POST</span>
<span class="endpoint-path">/webhooks/api-keys</span>
<span class="auth-tag auth-jwt">JWT org_admin</span>
</div>
<div class="endpoint-body">
<div class="info-box warning">La chiave completa viene restituita <strong>SOLO UNA VOLTA</strong> nella risposta di creazione. Salvarla immediatamente.</div>
<h4 class="params-title">Body (JSON)</h4>
<table class="params-table">
<thead><tr><th>Campo</th><th>Tipo</th><th>Obbligatorio</th><th>Descrizione</th></tr></thead>
<tbody>
<tr><td><code class="param-name">name</code></td><td><code class="param-type">string</code></td><td><span class="param-required">*</span></td><td>Nome descrittivo (es. "SIEM Integration")</td></tr>
<tr><td><code class="param-name">scopes</code></td><td><code class="param-type">array</code></td><td><span class="param-required">*</span></td><td>Array di scope (es. <code>["read:risks","read:incidents"]</code>)</td></tr>
<tr><td><code class="param-name">expires_at</code></td><td><code class="param-type">datetime</code></td><td></td><td>Scadenza opzionale (ISO 8601)</td></tr>
</tbody>
</table>
<h4 class="params-title">Risposta 201</h4>
<pre><span class="json-key">{
"id"</span>: <span class="json-num">5</span>,
<span class="json-key">"name"</span>: <span class="json-str">"SIEM Integration"</span>,
<span class="json-key">"key"</span>: <span class="json-str">"nis2_a3f8c2d1e4b7..."</span>,
<span class="json-key">"key_prefix"</span>: <span class="json-str">"nis2_a3f8c2"</span>,
<span class="json-key">"scopes"</span>: [<span class="json-str">"read:risks"</span>, <span class="json-str">"read:incidents"</span>],
<span class="json-key">"warning"</span>: <span class="json-str">"Salva questa chiave in modo sicuro. Non sara' piu' visibile."</span>
}</pre>
</div>
</div>
<div id="keys-delete" class="endpoint">
<div class="endpoint-header" onclick="toggle(this)">
<span class="method-badge method-delete">DELETE</span>
<span class="endpoint-path">/webhooks/api-keys/<span>{id}</span></span>
<span class="auth-tag auth-jwt">JWT org_admin</span>
<span class="endpoint-summary">Revoca (soft delete) API Key</span>
</div>
<div class="endpoint-body">
<p class="endpoint-desc">Revoca una API Key impostando <code>is_active = 0</code>. La chiave non viene eliminata dal DB per mantenere l'audit trail.</p>
</div>
</div>
</div>
<!-- WEBHOOK SUBSCRIPTIONS -->
<div class="section">
<h2 class="section-title">Webhook Subscriptions</h2>
<p class="section-desc">Gestione sottoscrizioni webhook outbound. NIS2 Agile invierà POST HTTP all'URL configurato al verificarsi degli eventi sottoscritti.</p>
<div id="wh-list" class="endpoint">
<div class="endpoint-header" onclick="toggle(this)">
<span class="method-badge method-get">GET</span>
<span class="endpoint-path">/webhooks/subscriptions</span>
<span class="auth-tag auth-jwt">JWT</span>
<span class="endpoint-summary">Lista subscriptions + statistiche delivery</span>
</div>
<div class="endpoint-body">
<p class="endpoint-desc">Include statistiche di delivery (total, success, failed) per ogni subscription. Non espone il secret HMAC.</p>
</div>
</div>
<div id="wh-create" class="endpoint">
<div class="endpoint-header" onclick="toggle(this)">
<span class="method-badge method-post">POST</span>
<span class="endpoint-path">/webhooks/subscriptions</span>
<span class="auth-tag auth-jwt">JWT org_admin</span>
</div>
<div class="endpoint-body">
<div class="info-box warning">Il secret HMAC viene restituito <strong>SOLO UNA VOLTA</strong>. Usarlo per verificare la firma <code>X-NIS2-Signature</code>.</div>
<h4 class="params-title">Body (JSON)</h4>
<table class="params-table">
<thead><tr><th>Campo</th><th>Tipo</th><th>Obbligatorio</th><th>Descrizione</th></tr></thead>
<tbody>
<tr><td><code class="param-name">name</code></td><td><code class="param-type">string</code></td><td><span class="param-required">*</span></td><td>Nome descrittivo</td></tr>
<tr><td><code class="param-name">url</code></td><td><code class="param-type">string</code></td><td><span class="param-required">*</span></td><td>URL https:// destinazione POST</td></tr>
<tr><td><code class="param-name">events</code></td><td><code class="param-type">array</code></td><td><span class="param-required">*</span></td><td>Array eventi (o <code>["*"]</code> per wildcard)</td></tr>
</tbody>
</table>
<h4 class="params-title">Risposta 201</h4>
<pre><span class="json-key">{
"id"</span>: <span class="json-num">3</span>,
<span class="json-key">"secret"</span>: <span class="json-str">"a3f8c2d1e4b7f9a2c8d3e1f4..."</span>,
<span class="json-key">"warning"</span>: <span class="json-str">"Salva il secret. Sara' usato per verificare la firma X-NIS2-Signature."</span>
}</pre>
</div>
</div>
<div id="wh-update" class="endpoint">
<div class="endpoint-header" onclick="toggle(this)">
<span class="method-badge method-put">PUT</span>
<span class="endpoint-path">/webhooks/subscriptions/<span>{id}</span></span>
<span class="auth-tag auth-jwt">JWT</span>
<span class="endpoint-summary">Aggiorna name, events, is_active</span>
</div>
<div class="endpoint-body">
<p class="endpoint-desc">Aggiornamento parziale (PATCH semantics). Solo i campi inviati vengono aggiornati: <code>name</code>, <code>events</code>, <code>is_active</code>.</p>
</div>
</div>
<div id="wh-delete" class="endpoint">
<div class="endpoint-header" onclick="toggle(this)">
<span class="method-badge method-delete">DELETE</span>
<span class="endpoint-path">/webhooks/subscriptions/<span>{id}</span></span>
<span class="auth-tag auth-jwt">JWT org_admin</span>
<span class="endpoint-summary">Elimina subscription e storico delivery</span>
</div>
</div>
<div id="wh-test" class="endpoint">
<div class="endpoint-header" onclick="toggle(this)">
<span class="method-badge method-post">POST</span>
<span class="endpoint-path">/webhooks/subscriptions/<span>{id}</span>/test</span>
<span class="auth-tag auth-jwt">JWT</span>
<span class="endpoint-summary">Invia evento webhook.test</span>
</div>
<div class="endpoint-body">
<p class="endpoint-desc">Invia un ping di test per verificare che l'endpoint remoto sia raggiungibile e risponda correttamente. Controlla il delivery log per il risultato.</p>
</div>
</div>
<div id="wh-deliveries" class="endpoint">
<div class="endpoint-header" onclick="toggle(this)">
<span class="method-badge method-get">GET</span>
<span class="endpoint-path">/webhooks/deliveries</span>
<span class="auth-tag auth-jwt">JWT</span>
<span class="endpoint-summary">Log ultimi 100 delivery</span>
</div>
<div class="endpoint-body">
<h4 class="params-title">Query Parameters</h4>
<table class="params-table">
<thead><tr><th>Parametro</th><th>Tipo</th><th>Descrizione</th></tr></thead>
<tbody>
<tr><td><code class="param-name">subscription_id</code></td><td><code class="param-type">integer</code></td><td>Filtra per subscription specifica</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- WEBHOOK EVENTS CATALOG -->
<div id="wh-events" class="section">
<h2 class="section-title">Catalogo Eventi Webhook</h2>
<p class="section-desc">NIS2 Agile emette eventi su tutte le operazioni rilevanti per la compliance. Ogni evento ha un payload tipizzato.</p>
<table class="params-table">
<thead><tr><th>Evento</th><th>Trigger</th><th>Payload</th></tr></thead>
<tbody>
<tr><td><code>incident.created</code></td><td>Nuovo incidente registrato</td><td><code>incidentPayload(incident, 'created')</code></td></tr>
<tr><td><code>incident.updated</code></td><td>Incidente modificato</td><td><code>incidentPayload(incident, 'updated')</code></td></tr>
<tr><td><code>incident.significant</code></td><td>Incidente flaggato Art.23</td><td><code>incidentPayload</code> con deadlines</td></tr>
<tr><td><code>incident.deadline_warning</code></td><td>Scadenza 24h/72h imminente</td><td>deadline + ore rimanenti</td></tr>
<tr><td><code>risk.high_created</code></td><td>Rischio HIGH o CRITICAL creato</td><td><code>riskPayload(risk, 'created')</code></td></tr>
<tr><td><code>risk.updated</code></td><td>Rischio aggiornato</td><td><code>riskPayload(risk, 'updated')</code></td></tr>
<tr><td><code>compliance.score_changed</code></td><td>Variazione score >5%</td><td>previous_score, new_score, delta, label</td></tr>
<tr><td><code>policy.approved</code></td><td>Policy approvata</td><td><code>policyPayload(policy)</code></td></tr>
<tr><td><code>policy.created</code></td><td>Nuova policy creata</td><td>id, title, category, nis2_article</td></tr>
<tr><td><code>supplier.risk_flagged</code></td><td>Fornitore con rischio HIGH/CRITICAL</td><td>supplier + risk_level</td></tr>
<tr><td><code>assessment.completed</code></td><td>Gap assessment completato</td><td>score, gap_count, top_gaps[]</td></tr>
<tr><td><code>whistleblowing.received</code></td><td>Nuova segnalazione anonima</td><td>id, category, priority (no PII)</td></tr>
<tr><td><code>normative.update</code></td><td>Aggiornamento normativo NIS2/ACN</td><td>title, source, effective_date</td></tr>
<tr><td><code>webhook.test</code></td><td>Ping manuale di test</td><td>message, timestamp</td></tr>
<tr><td><code>*</code></td><td>Wildcard — tutti gli eventi</td><td>—</td></tr>
</tbody>
</table>
<h3 class="params-title" style="margin-top:24px;">Struttura envelope payload</h3>
<pre><span class="json-key">{
"id"</span>: <span class="json-str">"550e8400-e29b-41d4-a716-446655440000"</span>,
<span class="json-key">"event"</span>: <span class="json-str">"incident.created"</span>,
<span class="json-key">"api_version"</span>: <span class="json-str">"1.0.0"</span>,
<span class="json-key">"created"</span>: <span class="json-num">1709900400</span>,
<span class="json-key">"created_at"</span>: <span class="json-str">"2026-03-07T10:00:00+01:00"</span>,
<span class="json-key">"source"</span>: <span class="json-str">"nis2-agile"</span>,
<span class="json-key">"org_id"</span>: <span class="json-num">42</span>,
<span class="json-key">"data"</span>: { <span class="json-key">"action"</span>: <span class="json-str">"created"</span>, <span class="json-key">"incident"</span>: { ... } }
}</pre>
</div>
<!-- WEBHOOK SIGNATURE -->
<div id="wh-signature" class="section">
<h2 class="section-title">Verifica Firma Webhook</h2>
<p class="section-desc">Ogni delivery include l'header <code>X-NIS2-Signature</code> con firma HMAC-SHA256 del body. Verifica sempre la firma prima di processare il payload.</p>
<div class="info-box info">
Pattern identico a Stripe webhook verification: <code>sha256=HMAC_SHA256(body, secret)</code>
</div>
<h3 class="params-title">Headers inviati</h3>
<pre><code>X-NIS2-Signature: sha256=a3f8c2d1e4b7...
X-NIS2-Event: incident.created
X-NIS2-Delivery-Id: 147
X-NIS2-Attempt: 1
Content-Type: application/json</code></pre>
<h3 class="params-title" style="margin-top:20px;">Verifica in PHP</h3>
<pre><span class="json-str">$body</span> = file_get_contents(<span class="json-str">'php://input'</span>);
<span class="json-str">$secret</span> = <span class="json-str">'il-tuo-secret'</span>;
<span class="json-str">$signature</span> = $_SERVER[<span class="json-str">'HTTP_X_NIS2_SIGNATURE'</span>];
<span class="json-str">$expected</span> = <span class="json-str">'sha256='</span> . hash_hmac(<span class="json-str">'sha256'</span>, <span class="json-str">$body</span>, <span class="json-str">$secret</span>);
if (!hash_equals(<span class="json-str">$expected</span>, <span class="json-str">$signature</span>)) {
http_response_code(401);
exit(<span class="json-str">'Invalid signature'</span>);
}</pre>
<h3 class="params-title" style="margin-top:20px;">Verifica in Python</h3>
<pre>import hmac, hashlib
secret = b'il-tuo-secret'
body = request.data # bytes
expected = 'sha256=' + hmac.new(secret, body, hashlib.sha256).hexdigest()
received = request.headers.get('X-NIS2-Signature', '')
if not hmac.compare_digest(expected, received):
abort(401)</pre>
<h3 class="params-title" style="margin-top:20px;">Retry Policy</h3>
<table class="params-table">
<thead><tr><th>Tentativo</th><th>Ritardo</th><th>Note</th></tr></thead>
<tbody>
<tr><td>1°</td><td>Immediato</td><td>Fire-and-forget sincrono</td></tr>
<tr><td>2°</td><td>+5 minuti</td><td>Processato da cron</td></tr>
<tr><td>3°</td><td>+30 minuti</td><td>Ultimo tentativo</td></tr>
</tbody>
</table>
<p style="font-size:0.8rem; color:var(--text-muted); margin-top:12px;">Dopo 10 fallimenti consecutivi la subscription viene messa in pausa automatica (<code>failure_count >= 10</code>).</p>
</div>
<div style="padding-top:40px; border-top:1px solid var(--border); color:var(--text-muted); font-size:0.8rem;">
NIS2 Agile API Reference v1.0.0 — &copy; 2026 CertiSource Srl —
<a href="https://nis2.certisource.it" style="color:var(--primary);">nis2.certisource.it</a>
</div>
</main>
</div>
<script>
function toggle(header) {
const body = header.nextElementSibling;
body.classList.toggle('open');
}
function scrollTo(id) {
const el = document.getElementById(id);
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
document.querySelectorAll('.sidebar-link').forEach(l => l.classList.remove('active'));
event.currentTarget.classList.add('active');
}
}
// Auto-open first endpoint in each section
document.querySelectorAll('.endpoint:first-of-type .endpoint-body').forEach(b => b.classList.add('open'));
</script>
</body>
</html>
+55
View File
@@ -98,6 +98,10 @@ $controllerMap = [
'admin' => 'AdminController',
'onboarding' => 'OnboardingController',
'ncr' => 'NonConformityController',
'services' => 'ServicesController',
'webhooks' => 'WebhookController',
'whistleblowing'=> 'WhistleblowingController',
'normative' => 'NormativeController',
];
if (!isset($controllerMap[$controllerName])) {
@@ -289,6 +293,55 @@ $actionMap = [
'POST:{id}/sync' => 'syncExternal',
'POST:webhook' => 'webhook',
],
// ── ServicesController (API pubblica) ──────────
'services' => [
'GET:status' => 'status',
'GET:complianceSummary' => 'complianceSummary',
'GET:risksFeed' => 'risksFeed',
'GET:incidentsFeed' => 'incidentsFeed',
'GET:controlsStatus' => 'controlsStatus',
'GET:assetsCritical' => 'assetsCritical',
'GET:suppliersRisk' => 'suppliersRisk',
'GET:policiesApproved' => 'policiesApproved',
'GET:openapi' => 'openapi',
],
// ── WebhookController (CRUD keys + subscriptions) ──
'webhooks' => [
'GET:apiKeys' => 'listApiKeys',
'POST:apiKeys' => 'createApiKey',
'DELETE:apiKeys/{subId}' => 'deleteApiKey',
'GET:subscriptions' => 'listSubscriptions',
'POST:subscriptions' => 'createSubscription',
'PUT:subscriptions/{subId}' => 'updateSubscription',
'DELETE:subscriptions/{subId}' => 'deleteSubscription',
'POST:subscriptions/{subId}/test' => 'testSubscription',
'GET:deliveries' => 'listDeliveries',
'POST:retry' => 'processRetry',
],
// ── WhistleblowingController (Art.32 NIS2) ─────
'whistleblowing' => [
'POST:submit' => 'submit',
'GET:list' => 'list',
'GET:{id}' => 'get',
'PUT:{id}' => 'update',
'POST:{id}/assign' => 'assign',
'POST:{id}/close' => 'close',
'GET:stats' => 'stats',
'GET:trackAnonymous' => 'trackAnonymous',
],
// ── NormativeController (Feed NIS2/ACN) ─────────
'normative' => [
'GET:list' => 'list',
'GET:{id}' => 'get',
'POST:{id}/ack' => 'acknowledge',
'GET:pending' => 'pending',
'GET:stats' => 'stats',
'POST:create' => 'create',
],
];
// ═══════════════════════════════════════════════════════════════════════════
@@ -340,6 +393,8 @@ if (is_numeric($actionName)) {
$sid = (int) $subResourceId;
$candidates[] = ['p' => "{$method}:{id}/{$camelSub}/{subId}", 'a' => [$rid, $sid]];
}
// e.g. POST:subscriptions/{subId}/test
$candidates[] = ['p' => "{$method}:{$actionName}/{subId}/{$camelSub}", 'a' => [$rid]];
$candidates[] = ['p' => "{$method}:{id}/{$camelSub}", 'a' => [$rid]];
$candidates[] = ['p' => "{$method}:{$actionName}/{subId}", 'a' => [$rid]];
}
+156
View File
@@ -0,0 +1,156 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AllRisk × NIS2 Agile — Integrazione</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--primary: #06b6d4; --amber: #f59e0b;
--gray-200: #e2e8f0; --gray-500: #64748b; --gray-700: #334155; --gray-900: #0f172a;
--radius: 8px; --font: -apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;
--mono: 'Cascadia Code','Consolas',monospace;
}
body { background: #f8fafc; font-family: var(--font); color: var(--gray-900); }
.header { background: linear-gradient(135deg, #78350f, #92400e); padding: 40px 48px; color: #fff; }
.header-badges { display: flex; gap: 10px; margin-bottom: 16px; }
.badge { padding: 4px 12px; border-radius: 20px; font-size: 0.75rem; font-weight: 700; }
.badge-nis2 { background: rgba(6,182,212,0.2); color: #67e8f9; border: 1px solid rgba(6,182,212,0.3); }
.badge-ar { background: rgba(245,158,11,0.2); color: #fde68a; border: 1px solid rgba(245,158,11,0.3); }
h1 { font-size: 1.875rem; font-weight: 800; margin-bottom: 8px; }
.header p { color: #fde68a; font-size: 1rem; }
.container { max-width: 960px; margin: 0 auto; padding: 40px 24px; }
h2 { font-size: 1.25rem; font-weight: 700; margin-bottom: 12px; padding-bottom: 10px; border-bottom: 2px solid var(--gray-200); }
.section { margin-bottom: 48px; }
p { color: var(--gray-500); font-size: 0.9rem; line-height: 1.7; margin-bottom: 12px; }
pre { background: #1e293b; color: #e2e8f0; padding: 20px; border-radius: var(--radius); font-family: var(--mono); font-size: 0.8125rem; overflow-x: auto; line-height: 1.7; margin: 12px 0; }
code { background: #f1f5f9; padding: 2px 6px; border-radius: 4px; font-family: var(--mono); font-size: 0.85em; color: var(--gray-700); }
.step { display: flex; gap: 16px; margin-bottom: 20px; padding: 18px; background: #fff; border: 1px solid var(--gray-200); border-radius: var(--radius); }
.step-num { width: 30px; height: 30px; border-radius: 50%; background: var(--amber); color: #fff; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 0.875rem; flex-shrink: 0; }
.step-content h3 { font-size: 0.9375rem; font-weight: 700; margin-bottom: 6px; }
.info-box { padding: 14px 16px; border-radius: var(--radius); margin: 16px 0; font-size: 0.875rem; }
.info-amber { background: #fffbeb; border-left: 3px solid var(--amber); color: #92400e; }
.field-map { width: 100%; border-collapse: collapse; }
.field-map th { padding: 9px 12px; background: #f1f5f9; font-size: 0.8rem; font-weight: 700; text-align: left; }
.field-map td { padding: 9px 12px; border-bottom: 1px solid var(--gray-200); font-size: 0.8rem; }
</style>
</head>
<body>
<div class="header">
<div class="header-badges">
<span class="badge badge-ar">AllRisk</span>
<span>×</span>
<span class="badge badge-nis2">NIS2 Agile</span>
</div>
<h1>Integrazione AllRisk ← NIS2 Agile</h1>
<p>Esporta il risk register cyber NIS2 verso AllRisk per consolidamento nel registro rischi enterprise. Importazione automatica dei rischi HIGH/CRITICAL tramite API Key.</p>
</div>
<div class="container">
<div class="section">
<h2>Scenario di integrazione</h2>
<p>AllRisk è il registro rischi enterprise centralizzato. NIS2 Agile gestisce i rischi cyber in ottica normativa (ISO 27005 / NIS2). L'integrazione permette di:</p>
<div class="step"><div class="step-num">1</div><div class="step-content"><h3>Consolidamento automatico</h3><p>I rischi HIGH/CRITICAL di NIS2 vengono automaticamente importati in AllRisk come rischi di categoria "Cyber/IT" con score e deadline normalizzati.</p></div></div>
<div class="step"><div class="step-num">2</div><div class="step-content"><h3>Aggiornamento bidirezionale</h3><p>AllRisk notifica NIS2 tramite webhook se un rischio enterprise si trasforma in minaccia cyber (es. incidente supply chain → rischio NIS2).</p></div></div>
<div class="step"><div class="step-num">3</div><div class="step-content"><h3>Reporting unificato</h3><p>Report rischi enterprise unico che include dimensione NIS2 con compliance score per ogni categoria di rischio cyber (Art.21 domains).</p></div></div>
</div>
<div class="section">
<h2>Mappatura campi NIS2 → AllRisk</h2>
<table class="field-map">
<thead><tr><th>Campo NIS2 Agile</th><th>Campo AllRisk</th><th>Trasformazione</th></tr></thead>
<tbody>
<tr><td><code>risk_code</code></td><td><code>external_ref</code></td><td>Prefisso <code>NIS2-</code> + codice</td></tr>
<tr><td><code>title</code></td><td><code>risk_name</code></td><td>Diretta</td></tr>
<tr><td><code>risk_level</code></td><td><code>severity</code></td><td>critical→5, high→4, medium→3, low→2</td></tr>
<tr><td><code>likelihood</code> × <code>impact</code></td><td><code>risk_score</code></td><td>Scala 0–25 → 0–100</td></tr>
<tr><td><code>category</code></td><td><code>risk_category</code></td><td>Mappato su tassonomia AllRisk "Cyber"</td></tr>
<tr><td><code>nis2_article</code></td><td><code>regulatory_ref</code></td><td>Prefisso "NIS2 Art."</td></tr>
<tr><td><code>treatment</code></td><td><code>response_strategy</code></td><td>mitigate→Reduce, accept→Accept, ecc.</td></tr>
<tr><td><code>review_date</code></td><td><code>next_review</code></td><td>Diretta (ISO 8601)</td></tr>
</tbody>
</table>
</div>
<div class="section">
<h2>Import automatico — Script PHP</h2>
<pre><span style="color:#7dd3fc;">// AllRisk — import_nis2_risks.php (cron settimanale)</span>
<span style="color:#f1fa8c;">$apiKey</span> = getenv(<span style="color:#86efac;">'NIS2_API_KEY'</span>);
<span style="color:#f1fa8c;">$orgId</span> = getenv(<span style="color:#86efac;">'NIS2_ORG_ID'</span>);
<span style="color:#f1fa8c;">$baseUrl</span> = <span style="color:#86efac;">'https://nis2.certisource.it/api'</span>;
<span style="color:#f1fa8c;">$allriskOrgId</span> = getenv(<span style="color:#86efac;">'ALLRISK_ORG_ID'</span>);
<span style="color:#7dd3fc;">// Fetch rischi HIGH/CRITICAL aperti da NIS2</span>
<span style="color:#f1fa8c;">$response</span> = nis2Get(<span style="color:#f1fa8c;">$baseUrl</span> . <span style="color:#86efac;">'/services/risks-feed'</span>, [
<span style="color:#86efac;">'level'</span> => <span style="color:#86efac;">'high'</span>,
<span style="color:#86efac;">'status'</span> => <span style="color:#86efac;">'open'</span>,
<span style="color:#86efac;">'limit'</span> => 200,
], <span style="color:#f1fa8c;">$apiKey</span>, <span style="color:#f1fa8c;">$orgId</span>);
<span style="color:#f1fa8c;">$risks</span> = <span style="color:#f1fa8c;">$response</span>[<span style="color:#86efac;">'data'</span>][<span style="color:#86efac;">'risks'</span>] ?? [];
<span style="color:#f1fa8c;">$imported</span> = 0;
foreach (<span style="color:#f1fa8c;">$risks</span> as <span style="color:#f1fa8c;">$risk</span>) {
<span style="color:#f1fa8c;">$allriskPayload</span> = [
<span style="color:#86efac;">'external_ref'</span> => <span style="color:#86efac;">'NIS2-'</span> . <span style="color:#f1fa8c;">$risk</span>[<span style="color:#86efac;">'risk_code'</span>],
<span style="color:#86efac;">'risk_name'</span> => <span style="color:#f1fa8c;">$risk</span>[<span style="color:#86efac;">'title'</span>],
<span style="color:#86efac;">'risk_category'</span> => <span style="color:#86efac;">'Cyber/IT'</span>,
<span style="color:#86efac;">'severity'</span> => mapSeverity(<span style="color:#f1fa8c;">$risk</span>[<span style="color:#86efac;">'risk_level'</span>]),
<span style="color:#86efac;">'risk_score'</span> => round(<span style="color:#f1fa8c;">$risk</span>[<span style="color:#86efac;">'risk_score'</span>] / 25 * 100),
<span style="color:#86efac;">'regulatory_ref'</span> => <span style="color:#86efac;">'NIS2 '</span> . (<span style="color:#f1fa8c;">$risk</span>[<span style="color:#86efac;">'nis2_article'</span>] ?? <span style="color:#86efac;">'Art.21'</span>),
<span style="color:#86efac;">'response_strategy'</span> => mapTreatment(<span style="color:#f1fa8c;">$risk</span>[<span style="color:#86efac;">'treatment'</span>]),
<span style="color:#86efac;">'source_system'</span> => <span style="color:#86efac;">'NIS2 Agile'</span>,
<span style="color:#86efac;">'organization_id'</span> => <span style="color:#f1fa8c;">$allriskOrgId</span>,
];
<span style="color:#7dd3fc;">// Upsert: aggiorna se external_ref esiste, crea altrimenti</span>
AllRiskApiClient::upsertRisk(<span style="color:#f1fa8c;">$allriskPayload</span>);
<span style="color:#f1fa8c;">$imported</span>++;
}
error_log("[NIS2→AllRisk] Importati {<span style="color:#f1fa8c;">$imported</span>} rischi");
<span style="color:#f1fa8c;">function</span> mapSeverity(<span style="color:#f1fa8c;">string $level</span>): int {
return match(<span style="color:#f1fa8c;">$level</span>) { <span style="color:#86efac;">'critical'</span> => 5, <span style="color:#86efac;">'high'</span> => 4, <span style="color:#86efac;">'medium'</span> => 3, default => 2 };
}
<span style="color:#f1fa8c;">function</span> mapTreatment(<span style="color:#f1fa8c;">string $t</span>): string {
return match(<span style="color:#f1fa8c;">$t</span>) {
<span style="color:#86efac;">'mitigate'</span> => <span style="color:#86efac;">'Reduce'</span>, <span style="color:#86efac;">'accept'</span> => <span style="color:#86efac;">'Accept'</span>,
<span style="color:#86efac;">'transfer'</span> => <span style="color:#86efac;">'Transfer'</span>, default => <span style="color:#86efac;">'Avoid'</span>
};
}</pre>
</div>
<div class="section">
<h2>Webhook NIS2 → AllRisk (real-time)</h2>
<p>Configura webhook per importazione immediata dei nuovi rischi HIGH/CRITICAL:</p>
<pre><span style="color:#7dd3fc;"># Settings → Webhook in NIS2 Agile</span>
URL: https://allrisk.certisource.it/api/v1/webhooks/nis2
Events: risk.high_created, compliance.score_changed, incident.significant</pre>
<pre><span style="color:#7dd3fc;">// AllRisk — webhook receiver per NIS2</span>
<span style="color:#f1fa8c;">$payload</span> = verifyAndDecode($_SERVER[<span style="color:#86efac;">'HTTP_X_NIS2_SIGNATURE'</span>], file_get_contents(<span style="color:#86efac;">'php://input'</span>));
if (<span style="color:#f1fa8c;">$payload</span>[<span style="color:#86efac;">'event'</span>] === <span style="color:#86efac;">'risk.high_created'</span>) {
<span style="color:#f1fa8c;">$risk</span> = <span style="color:#f1fa8c;">$payload</span>[<span style="color:#86efac;">'data'</span>][<span style="color:#86efac;">'risk'</span>];
AllRiskApiClient::upsertRisk([
<span style="color:#86efac;">'external_ref'</span> => <span style="color:#86efac;">'NIS2-'</span> . <span style="color:#f1fa8c;">$risk</span>[<span style="color:#86efac;">'id'</span>],
<span style="color:#86efac;">'risk_name'</span> => <span style="color:#f1fa8c;">$risk</span>[<span style="color:#86efac;">'title'</span>],
<span style="color:#86efac;">'severity'</span> => <span style="color:#f1fa8c;">$risk</span>[<span style="color:#86efac;">'risk_level'</span>] === <span style="color:#86efac;">'critical'</span> ? 5 : 4,
<span style="color:#86efac;">'source_system'</span>=> <span style="color:#86efac;">'NIS2 Agile'</span>,
<span style="color:#86efac;">'alert'</span> => true, <span style="color:#7dd3fc;">// Notifica CRO AllRisk</span>
]);
}
http_response_code(200);</pre>
</div>
<div class="info-box info-amber">
<strong>Nota architetturale:</strong> Il flusso è unidirezionale NIS2 → AllRisk. Per il flusso inverso (AllRisk → NIS2), AllRisk chiama l'API NIS2 per creare rischi cyber direttamente tramite le API JWT (POST /api/risks/create) usando le credenziali dell'utente integration.
</div>
</div>
</body>
</html>
+66
View File
@@ -0,0 +1,66 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NIS2 Agile — Integrazioni</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #f8fafc; --white: #ffffff; --primary: #06b6d4;
--gray-100: #f1f5f9; --gray-200: #e2e8f0; --gray-500: #64748b;
--gray-700: #334155; --gray-900: #0f172a;
--radius: 12px; --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
}
body { background: var(--bg); font-family: var(--font); color: var(--gray-900); padding: 40px 24px; }
.container { max-width: 1000px; margin: 0 auto; }
h1 { font-size: 1.75rem; font-weight: 800; margin-bottom: 8px; }
p.subtitle { color: var(--gray-500); margin-bottom: 40px; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 24px; }
.card { background: var(--white); border-radius: var(--radius); border: 1px solid var(--gray-200); padding: 28px; transition: all 0.2s; }
.card:hover { transform: translateY(-2px); box-shadow: 0 8px 24px rgba(0,0,0,0.08); }
.card-icon { width: 48px; height: 48px; border-radius: 12px; display: flex; align-items: center; justify-content: center; margin-bottom: 16px; font-size: 1.5rem; }
.card-title { font-size: 1.125rem; font-weight: 700; margin-bottom: 8px; }
.card-desc { font-size: 0.875rem; color: var(--gray-500); line-height: 1.6; margin-bottom: 20px; }
.card-link { display: inline-flex; align-items: center; gap: 6px; color: var(--primary); font-size: 0.875rem; font-weight: 600; text-decoration: none; }
.card-link:hover { text-decoration: underline; }
.badge { display: inline-flex; padding: 3px 8px; border-radius: 20px; font-size: 0.7rem; font-weight: 700; margin-bottom: 12px; }
</style>
</head>
<body>
<div class="container">
<h1>NIS2 Agile — Integrazioni</h1>
<p class="subtitle">Pagine di integrazione per connettere NIS2 Agile con altri prodotti della suite Agile.</p>
<div class="grid">
<div class="card">
<div class="card-icon" style="background:#f0fdf4;">📋</div>
<span class="badge" style="background:#dcfce7; color:#166534;">Webhook + Widget</span>
<div class="card-title">231 Agile ← NIS2</div>
<div class="card-desc">Integra i dati di compliance NIS2 in 231 Agile. Visualizza compliance score, rischi cyber e incidenti significativi nel contesto del Modello 231.</div>
<a href="lg231.html" class="card-link">Guida integrazione →</a>
</div>
<div class="card">
<div class="card-icon" style="background:#f0f9ff;">🌿</div>
<span class="badge" style="background:#e0f2fe; color:#075985;">API Widget</span>
<div class="card-title">SustainAI ← NIS2</div>
<div class="card-desc">Alimenta i report ESG/sostenibilità di SustainAI con dati di governance cybersecurity NIS2. Compliance score e policy approvate per l'area G (Governance).</div>
<a href="sustainai.html" class="card-link">Guida integrazione →</a>
</div>
<div class="card">
<div class="card-icon" style="background:#fef9c3;">⚡</div>
<span class="badge" style="background:#fef9c3; color:#854d0e;">REST API</span>
<div class="card-title">AllRisk ← NIS2</div>
<div class="card-desc">Esporta il risk register cyber NIS2 verso AllRisk per consolidamento nel registro rischi enterprise. Sincronizzazione automatica via API Key.</div>
<a href="allrisk.html" class="card-link">Guida integrazione →</a>
</div>
<div class="card">
<div class="card-icon" style="background:#faf5ff;">🔒</div>
<span class="badge" style="background:#f3e8ff; color:#6b21a8;">SIEM</span>
<div class="card-title">SIEM / SOC</div>
<div class="card-desc">Integra NIS2 Agile con SIEM (Splunk, Elastic, IBM QRadar) tramite webhook outbound. Ricevi eventi incident.created, risk.high_created in tempo reale.</div>
<a href="siem.html" class="card-link">Guida integrazione →</a>
</div>
</div>
</div>
</body>
</html>
+225
View File
@@ -0,0 +1,225 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>231 Agile × NIS2 Agile — Integrazione</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #f8fafc; --white: #fff; --primary: #06b6d4; --green: #10b981;
--gray-100: #f1f5f9; --gray-200: #e2e8f0; --gray-500: #64748b; --gray-700: #334155; --gray-900: #0f172a;
--danger: #ef4444; --warning: #f59e0b; --mono: 'Cascadia Code','Consolas',monospace;
--radius: 8px; --font: -apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;
}
body { background: var(--bg); font-family: var(--font); color: var(--gray-900); }
.header { background: linear-gradient(135deg, #1e293b, #0f172a); padding: 40px 48px; color: #fff; }
.header-badges { display: flex; gap: 10px; margin-bottom: 16px; }
.badge { padding: 4px 12px; border-radius: 20px; font-size: 0.75rem; font-weight: 700; }
.badge-nis2 { background: rgba(6,182,212,0.2); color: #67e8f9; border: 1px solid rgba(6,182,212,0.3); }
.badge-231 { background: rgba(16,185,129,0.2); color: #6ee7b7; border: 1px solid rgba(16,185,129,0.3); }
h1 { font-size: 1.875rem; font-weight: 800; margin-bottom: 8px; }
.header p { color: #94a3b8; font-size: 1rem; }
.container { max-width: 960px; margin: 0 auto; padding: 40px 24px; }
h2 { font-size: 1.25rem; font-weight: 700; margin-bottom: 12px; padding-bottom: 10px; border-bottom: 2px solid var(--gray-200); }
h3 { font-size: 1rem; font-weight: 700; margin-bottom: 8px; color: var(--gray-700); }
.section { margin-bottom: 48px; }
p { color: var(--gray-500); font-size: 0.9rem; line-height: 1.7; margin-bottom: 12px; }
pre, code { font-family: var(--mono); }
pre { background: #1e293b; color: #e2e8f0; padding: 20px; border-radius: var(--radius); font-size: 0.8125rem; overflow-x: auto; line-height: 1.7; margin: 12px 0; }
code { background: var(--gray-100); padding: 2px 6px; border-radius: 4px; font-size: 0.85em; color: var(--gray-700); }
.step { display: flex; gap: 16px; margin-bottom: 24px; padding: 20px; background: var(--white); border: 1px solid var(--gray-200); border-radius: var(--radius); }
.step-num { width: 32px; height: 32px; border-radius: 50%; background: var(--primary); color: #fff; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 0.875rem; flex-shrink: 0; }
.step-content h3 { margin-bottom: 6px; }
.info-box { padding: 14px 16px; border-radius: var(--radius); margin-bottom: 16px; font-size: 0.875rem; }
.info-warn { background: #fff7ed; border-left: 3px solid var(--warning); color: #92400e; }
.info-info { background: #eff6ff; border-left: 3px solid #3b82f6; color: #1e40af; }
.info-success { background: #f0fdf4; border-left: 3px solid var(--green); color: #065f46; }
.widget-preview { background: var(--white); border: 1px solid var(--gray-200); border-radius: var(--radius); padding: 24px; margin-top: 20px; }
.widget-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
.widget-title { font-size: 0.875rem; font-weight: 700; display: flex; align-items: center; gap: 8px; }
.score-ring { width: 80px; height: 80px; display: flex; align-items: center; justify-content: center; background: conic-gradient(#06b6d4 73%, #e2e8f0 0); border-radius: 50%; font-size: 1.25rem; font-weight: 800; }
.metric-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-top: 16px; }
.metric { text-align: center; padding: 12px; background: var(--gray-100); border-radius: 6px; }
.metric-val { font-size: 1.25rem; font-weight: 700; color: var(--gray-900); }
.metric-label { font-size: 0.7rem; color: var(--gray-500); margin-top: 2px; }
</style>
</head>
<body>
<div class="header">
<div class="header-badges">
<span class="badge badge-nis2">NIS2 Agile</span>
<span>×</span>
<span class="badge badge-231">231 Agile</span>
</div>
<h1>Integrazione 231 Agile ← NIS2 Agile</h1>
<p>Porta i dati di compliance cybersecurity NIS2 nel contesto del Modello Organizzativo 231. Rischi cyber come presidi 231, incidenti significativi come non conformità.</p>
</div>
<div class="container">
<div class="section">
<h2>Architettura dell'integrazione</h2>
<p>NIS2 Agile espone un'API REST con API Key e un sistema di webhook outbound. 231 Agile può consumare i dati in due modalità:</p>
<div class="step">
<div class="step-num">A</div>
<div class="step-content">
<h3>Pull (Services API)</h3>
<p>231 Agile chiama periodicamente le API NIS2 per aggiornare il cruscotto 231. Indicato per dati storici e report periodici (mensile/trimestrale).</p>
</div>
</div>
<div class="step">
<div class="step-num">B</div>
<div class="step-content">
<h3>Push (Webhook outbound)</h3>
<p>NIS2 Agile notifica 231 Agile in tempo reale su eventi critici. Indicato per incidenti significativi, rischi HIGH/CRITICAL, variazioni compliance.</p>
</div>
</div>
</div>
<div class="section">
<h2>Step 1 — Crea API Key in NIS2 Agile</h2>
<div class="info-warn">Richiede ruolo <code>org_admin</code> in NIS2 Agile.</div>
<div class="step"><div class="step-num">1</div><div class="step-content"><h3>Accedi a Settings → API Keys</h3><p>In NIS2 Agile, vai in <code>Settings</code> → tab <strong>API Keys</strong> → <strong>Nuova API Key</strong>.</p></div></div>
<div class="step"><div class="step-num">2</div><div class="step-content"><h3>Seleziona gli scope</h3><p>Per 231 Agile ti servono: <code>read:compliance</code>, <code>read:risks</code>, <code>read:incidents</code>. Opzionale: <code>read:policies</code>.</p></div></div>
<div class="step"><div class="step-num">3</div><div class="step-content"><h3>Salva la chiave</h3><p>La chiave <code>nis2_xxxxx</code> viene mostrata <strong>una sola volta</strong>. Copiala in <strong>231 Agile → Integrazioni → NIS2 Agile → API Key</strong>.</p></div></div>
</div>
<div class="section">
<h2>Step 2 — Integrazione Pull (PHP / 231 Agile)</h2>
<p>Aggiungi questo codice nel cron job notturno di 231 Agile per sincronizzare i dati NIS2:</p>
<pre><span style="color:#7dd3fc;">// 231 Agile — CronJob: sync_nis2_compliance.php</span>
<span style="color:#f1fa8c;">$apiKey</span> = getenv(<span style="color:#86efac;">'NIS2_API_KEY'</span>); <span style="color:#7dd3fc;">// nis2_abc123...</span>
<span style="color:#f1fa8c;">$orgId</span> = getenv(<span style="color:#86efac;">'NIS2_ORG_ID'</span>);
<span style="color:#f1fa8c;">$baseUrl</span> = <span style="color:#86efac;">'https://nis2.certisource.it/api'</span>;
<span style="color:#f1fa8c;">function</span> nis2Get(<span style="color:#f1fa8c;">string $endpoint</span>, array <span style="color:#f1fa8c;">$query</span> = []): array {
global <span style="color:#f1fa8c;">$apiKey</span>, <span style="color:#f1fa8c;">$orgId</span>, <span style="color:#f1fa8c;">$baseUrl</span>;
<span style="color:#f1fa8c;">$url</span> = <span style="color:#f1fa8c;">$baseUrl</span> . <span style="color:#f1fa8c;">$endpoint</span>;
if (!empty(<span style="color:#f1fa8c;">$query</span>)) <span style="color:#f1fa8c;">$url</span> .= <span style="color:#86efac;">'?'</span> . http_build_query(<span style="color:#f1fa8c;">$query</span>);
<span style="color:#f1fa8c;">$ch</span> = curl_init(<span style="color:#f1fa8c;">$url</span>);
curl_setopt_array(<span style="color:#f1fa8c;">$ch</span>, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
<span style="color:#86efac;">'X-API-Key: '</span> . <span style="color:#f1fa8c;">$apiKey</span>,
<span style="color:#86efac;">'X-Organization-Id: '</span> . <span style="color:#f1fa8c;">$orgId</span>,
],
]);
return json_decode(curl_exec(<span style="color:#f1fa8c;">$ch</span>), true) ?? [];
}
<span style="color:#7dd3fc;">// 1. Recupera compliance summary</span>
<span style="color:#f1fa8c;">$compliance</span> = nis2Get(<span style="color:#86efac;">'/services/compliance-summary'</span>);
<span style="color:#f1fa8c;">$score</span> = <span style="color:#f1fa8c;">$compliance</span>[<span style="color:#86efac;">'data'</span>][<span style="color:#86efac;">'overall_score'</span>] ?? 0;
<span style="color:#7dd3fc;">// 2. Recupera rischi HIGH/CRITICAL per mappa rischi 231</span>
<span style="color:#f1fa8c;">$risks</span> = nis2Get(<span style="color:#86efac;">'/services/risks-feed'</span>, [<span style="color:#86efac;">'level'</span> => <span style="color:#86efac;">'high'</span>, <span style="color:#86efac;">'status'</span> => <span style="color:#86efac;">'open'</span>]);
<span style="color:#7dd3fc;">// 3. Recupera incidenti Art.23 aperti</span>
<span style="color:#f1fa8c;">$incidents</span> = nis2Get(<span style="color:#86efac;">'/services/incidents-feed'</span>, [
<span style="color:#86efac;">'significant_only'</span> => 1, <span style="color:#86efac;">'status'</span> => <span style="color:#86efac;">'open'</span>
]);
<span style="color:#7dd3fc;">// 4. Salva in DB 231 Agile — tabella nis2_integration</span>
<span style="color:#7dd3fc;">// DB231::upsert('nis2_integration', ['org_id'=>$orgId231, 'score'=>$score, ...]);</span></pre>
</div>
<div class="section">
<h2>Step 3 — Webhook Push (real-time)</h2>
<p>Configura in NIS2 Agile → Settings → Webhook un endpoint del tuo server 231 Agile per ricevere eventi in tempo reale:</p>
<pre><span style="color:#7dd3fc;"># Endpoint da configurare in NIS2 Agile → Settings → Webhook</span>
URL: https://app-231.certisource.it/webhooks/nis2
Events: incident.significant, incident.deadline_warning, risk.high_created, compliance.score_changed</pre>
<h3 style="margin-top:20px;">Receiver PHP su 231 Agile</h3>
<pre><span style="color:#7dd3fc;">// 231 Agile — routes/webhook_nis2.php</span>
<span style="color:#f1fa8c;">$secret</span> = getenv(<span style="color:#86efac;">'NIS2_WEBHOOK_SECRET'</span>);
<span style="color:#f1fa8c;">$body</span> = file_get_contents(<span style="color:#86efac;">'php://input'</span>);
<span style="color:#f1fa8c;">$sig</span> = $_SERVER[<span style="color:#86efac;">'HTTP_X_NIS2_SIGNATURE'</span>] ?? <span style="color:#86efac;">''</span>;
if (!hash_equals(<span style="color:#86efac;">'sha256='</span> . hash_hmac(<span style="color:#86efac;">'sha256'</span>, <span style="color:#f1fa8c;">$body</span>, <span style="color:#f1fa8c;">$secret</span>), <span style="color:#f1fa8c;">$sig</span>)) {
http_response_code(401); exit;
}
<span style="color:#f1fa8c;">$payload</span> = json_decode(<span style="color:#f1fa8c;">$body</span>, true);
<span style="color:#f1fa8c;">$event</span> = <span style="color:#f1fa8c;">$payload</span>[<span style="color:#86efac;">'event'</span>];
switch (<span style="color:#f1fa8c;">$event</span>) {
case <span style="color:#86efac;">'incident.significant'</span>:
<span style="color:#7dd3fc;">// Crea non-conformità in 231 Agile</span>
NonConformityService::createFromNis2Incident(<span style="color:#f1fa8c;">$payload</span>[<span style="color:#86efac;">'data'</span>][<span style="color:#86efac;">'incident'</span>]);
break;
case <span style="color:#86efac;">'risk.high_created'</span>:
<span style="color:#7dd3fc;">// Aggiorna mappa rischi presidi 231</span>
RiskService::importFromNis2(<span style="color:#f1fa8c;">$payload</span>[<span style="color:#86efac;">'data'</span>][<span style="color:#86efac;">'risk'</span>]);
break;
case <span style="color:#86efac;">'compliance.score_changed'</span>:
<span style="color:#7dd3fc;">// Aggiorna dashboard 231 con nuovo score NIS2</span>
DashboardService::updateNis2Score(<span style="color:#f1fa8c;">$payload</span>[<span style="color:#86efac;">'data'</span>][<span style="color:#86efac;">'new_score'</span>]);
break;
}
http_response_code(200);</pre>
</div>
<div class="section">
<h2>Widget NIS2 per Dashboard 231</h2>
<p>Embed HTML con chiamata API diretta. Copialo nella dashboard di 231 Agile:</p>
<div class="widget-preview">
<div class="widget-header">
<div class="widget-title">
<svg viewBox="0 0 24 24" fill="#06b6d4" width="18" height="18"><path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4z"/></svg>
NIS2 Compliance (Preview widget)
</div>
<span style="font-size:0.7rem; color:#64748b;">nis2.certisource.it</span>
</div>
<div style="display:flex; align-items:center; gap:24px;">
<div class="score-ring">73%</div>
<div>
<p style="font-size:0.9rem; font-weight:700; margin-bottom:4px;">Sostanzialmente Conforme</p>
<p style="font-size:0.75rem; color:#64748b;">D.Lgs. 138/2024 — Art.21 NIS2</p>
</div>
</div>
<div class="metric-row">
<div class="metric"><div class="metric-val" style="color:#ef4444;">2</div><div class="metric-label">Incidenti aperti</div></div>
<div class="metric"><div class="metric-val" style="color:#f59e0b;">5</div><div class="metric-label">Rischi HIGH</div></div>
<div class="metric"><div class="metric-val" style="color:#10b981;">12</div><div class="metric-label">Policy approvate</div></div>
</div>
</div>
<pre style="margin-top:16px;"><span style="color:#7dd3fc;">&lt;!-- Widget NIS2 per 231 Agile dashboard --&gt;</span>
&lt;div id="nis2-widget"&gt;&lt;/div&gt;
&lt;script&gt;
(async () => {
const r = await fetch('https://nis2.certisource.it/api/services/compliance-summary', {
headers: {
'X-API-Key': '<span style="color:#86efac;">nis2_YOUR_KEY</span>',
'X-Organization-Id': '<span style="color:#86efac;">YOUR_ORG_ID</span>'
}
});
const { data } = await r.json();
document.getElementById('nis2-widget').innerHTML = `
&lt;div style="padding:20px; border:1px solid #e2e8f0; border-radius:8px;"&gt;
&lt;h4 style="font-size:.875rem; font-weight:700; margin-bottom:12px;"&gt;
🔒 NIS2 Compliance Score
&lt;/h4&gt;
&lt;div style="font-size:2rem; font-weight:800; color:#06b6d4;"&gt;
${data.overall_score}%
&lt;/div&gt;
&lt;div style="font-size:.75rem; color:#64748b;"&gt;${data.label}&lt;/div&gt;
&lt;div style="margin-top:12px; display:grid; grid-template-columns:1fr 1fr 1fr; gap:8px;"&gt;
&lt;div&gt;&lt;strong&gt;${data.incidents.open}&lt;/strong&gt;&lt;br&gt;&lt;small&gt;Incidenti&lt;/small&gt;&lt;/div&gt;
&lt;div&gt;&lt;strong&gt;${data.risks.high}&lt;/strong&gt;&lt;br&gt;&lt;small&gt;Rischi HIGH&lt;/small&gt;&lt;/div&gt;
&lt;div&gt;&lt;strong&gt;${data.policies.approved}&lt;/strong&gt;&lt;br&gt;&lt;small&gt;Policy&lt;/small&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;`;
})();
&lt;/script&gt;</pre>
</div>
<div class="info-success">
<strong>Dati mappati 231 ↔ NIS2:</strong> Rischi cyber NIS2 → Presidi 231 (ex. D.Lgs. 231/01 Art.25-septies) | Incidenti significativi → Non Conformità 231 | Compliance score → KPI Operatività 231
</div>
</div>
</body>
</html>
+179
View File
@@ -0,0 +1,179 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SIEM × NIS2 Agile — Integrazione</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--primary: #06b6d4; --purple: #8b5cf6;
--gray-200: #e2e8f0; --gray-500: #64748b; --gray-700: #334155;
--radius: 8px; --font: -apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;
--mono: 'Cascadia Code','Consolas',monospace;
}
body { background: #f8fafc; font-family: var(--font); color: #0f172a; }
.header { background: linear-gradient(135deg, #2e1065, #3b0764); padding: 40px 48px; color: #fff; }
.header-badges { display: flex; gap: 10px; margin-bottom: 16px; }
.badge { padding: 4px 12px; border-radius: 20px; font-size: 0.75rem; font-weight: 700; }
.badge-nis2 { background: rgba(6,182,212,0.2); color: #67e8f9; border: 1px solid rgba(6,182,212,0.3); }
.badge-siem { background: rgba(139,92,246,0.2); color: #ddd6fe; border: 1px solid rgba(139,92,246,0.3); }
h1 { font-size: 1.875rem; font-weight: 800; margin-bottom: 8px; }
.header p { color: #ddd6fe; font-size: 1rem; }
.container { max-width: 960px; margin: 0 auto; padding: 40px 24px; }
h2 { font-size: 1.25rem; font-weight: 700; margin-bottom: 12px; padding-bottom: 10px; border-bottom: 2px solid var(--gray-200); }
.section { margin-bottom: 48px; }
p { color: var(--gray-500); font-size: 0.9rem; line-height: 1.7; margin-bottom: 12px; }
pre { background: #1e293b; color: #e2e8f0; padding: 20px; border-radius: var(--radius); font-family: var(--mono); font-size: 0.8125rem; overflow-x: auto; line-height: 1.7; margin: 12px 0; }
code { background: #f1f5f9; padding: 2px 6px; border-radius: 4px; font-family: var(--mono); font-size: 0.85em; }
.siem-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 16px; margin-top: 16px; }
.siem-card { background: #fff; border: 1px solid var(--gray-200); border-radius: var(--radius); padding: 16px; }
.siem-card h4 { font-size: 0.875rem; font-weight: 700; margin-bottom: 6px; }
.siem-card p { font-size: 0.8rem; color: var(--gray-500); margin: 0; }
.info-box { padding: 14px 16px; border-radius: var(--radius); margin: 16px 0; font-size: 0.875rem; }
.info-purple { background: #f5f3ff; border-left: 3px solid var(--purple); color: #4c1d95; }
</style>
</head>
<body>
<div class="header">
<div class="header-badges">
<span class="badge badge-siem">SIEM / SOC</span>
<span>×</span>
<span class="badge badge-nis2">NIS2 Agile</span>
</div>
<h1>Integrazione SIEM / SOC ← NIS2 Agile</h1>
<p>Ricevi eventi NIS2 in tempo reale nel tuo SIEM. Incidenti Art.23, rischi HIGH/CRITICAL, variazioni compliance come alert automatici.</p>
</div>
<div class="container">
<div class="section">
<h2>SIEM supportati</h2>
<div class="siem-grid">
<div class="siem-card"><h4>Splunk Enterprise</h4><p>HTTP Event Collector (HEC) + custom TA per NIS2 Agile. Dashboard Art.23 precompilata.</p></div>
<div class="siem-card"><h4>Elastic SIEM</h4><p>Logstash HTTP input + index template NIS2. Alert rule per incident.significant.</p></div>
<div class="siem-card"><h4>IBM QRadar</h4><p>Universal DSM + webhook-to-syslog bridge. Evento NIS2 → offense QRadar.</p></div>
<div class="siem-card"><h4>Microsoft Sentinel</h4><p>Logic App webhook receiver → Azure Monitor. Playbook SOAR per Art.23.</p></div>
</div>
</div>
<div class="section">
<h2>Configurazione Webhook NIS2 → SIEM</h2>
<p>Configura in NIS2 Agile → Settings → Webhook l'endpoint del tuo SIEM:</p>
<pre><span style="color:#7dd3fc;"># SIEM HEC / HTTP Input endpoint</span>
URL: https://your-siem.example.com:8088/services/collector <span style="color:#7dd3fc;"># Splunk HEC</span>
URL: https://your-elk.example.com:9200/_ingest/pipeline/nis2 <span style="color:#7dd3fc;"># Elastic</span>
Events: incident.created, incident.significant, incident.deadline_warning,
risk.high_created, compliance.score_changed</pre>
</div>
<div class="section">
<h2>Splunk HEC — Bridge PHP</h2>
<p>Se il SIEM non supporta nativamente la firma HMAC-SHA256, usa questo bridge come proxy tra NIS2 e Splunk HEC:</p>
<pre><span style="color:#7dd3fc;">// nis2_to_splunk_bridge.php — Deploy su server intermedio</span>
<span style="color:#f1fa8c;">$nis2Secret</span> = getenv(<span style="color:#86efac;">'NIS2_WEBHOOK_SECRET'</span>);
<span style="color:#f1fa8c;">$splunkHec</span> = getenv(<span style="color:#86efac;">'SPLUNK_HEC_URL'</span>); <span style="color:#7dd3fc;">// https://splunk:8088/services/collector</span>
<span style="color:#f1fa8c;">$splunkToken</span> = getenv(<span style="color:#86efac;">'SPLUNK_HEC_TOKEN'</span>);
<span style="color:#7dd3fc;">// 1. Verifica firma NIS2</span>
<span style="color:#f1fa8c;">$body</span> = file_get_contents(<span style="color:#86efac;">'php://input'</span>);
<span style="color:#f1fa8c;">$sig</span> = $_SERVER[<span style="color:#86efac;">'HTTP_X_NIS2_SIGNATURE'</span>] ?? <span style="color:#86efac;">''</span>;
if (!hash_equals(<span style="color:#86efac;">'sha256='</span> . hash_hmac(<span style="color:#86efac;">'sha256'</span>, <span style="color:#f1fa8c;">$body</span>, <span style="color:#f1fa8c;">$nis2Secret</span>), <span style="color:#f1fa8c;">$sig</span>)) {
http_response_code(401); exit;
}
<span style="color:#f1fa8c;">$payload</span> = json_decode(<span style="color:#f1fa8c;">$body</span>, true);
<span style="color:#7dd3fc;">// 2. Trasforma in formato Splunk HEC</span>
<span style="color:#f1fa8c;">$splunkEvent</span> = [
<span style="color:#86efac;">'time'</span> => <span style="color:#f1fa8c;">$payload</span>[<span style="color:#86efac;">'created'</span>],
<span style="color:#86efac;">'host'</span> => <span style="color:#86efac;">'nis2-agile'</span>,
<span style="color:#86efac;">'source'</span> => <span style="color:#86efac;">'nis2-agile-webhook'</span>,
<span style="color:#86efac;">'sourcetype'</span> => <span style="color:#86efac;">'nis2:event'</span>,
<span style="color:#86efac;">'index'</span> => <span style="color:#86efac;">'nis2_compliance'</span>,
<span style="color:#86efac;">'event'</span> => [
<span style="color:#86efac;">'event_type'</span> => <span style="color:#f1fa8c;">$payload</span>[<span style="color:#86efac;">'event'</span>],
<span style="color:#86efac;">'event_id'</span> => <span style="color:#f1fa8c;">$payload</span>[<span style="color:#86efac;">'id'</span>],
<span style="color:#86efac;">'org_id'</span> => <span style="color:#f1fa8c;">$payload</span>[<span style="color:#86efac;">'org_id'</span>],
<span style="color:#86efac;">'data'</span> => <span style="color:#f1fa8c;">$payload</span>[<span style="color:#86efac;">'data'</span>],
<span style="color:#86efac;">'api_version'</span> => <span style="color:#f1fa8c;">$payload</span>[<span style="color:#86efac;">'api_version'</span>],
],
];
<span style="color:#7dd3fc;">// 3. Forward a Splunk HEC</span>
<span style="color:#f1fa8c;">$ch</span> = curl_init(<span style="color:#f1fa8c;">$splunkHec</span>);
curl_setopt_array(<span style="color:#f1fa8c;">$ch</span>, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(<span style="color:#f1fa8c;">$splunkEvent</span>),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
<span style="color:#86efac;">'Authorization: Splunk '</span> . <span style="color:#f1fa8c;">$splunkToken</span>,
<span style="color:#86efac;">'Content-Type: application/json'</span>,
],
]);
<span style="color:#f1fa8c;">$result</span> = curl_exec(<span style="color:#f1fa8c;">$ch</span>);
curl_close(<span style="color:#f1fa8c;">$ch</span>);
http_response_code(200);</pre>
</div>
<div class="section">
<h2>Elastic — Logstash Pipeline</h2>
<pre><span style="color:#7dd3fc;"># logstash/pipelines/nis2.conf</span>
input {
http {
port => 8181
codec => json
<span style="color:#7dd3fc;"># Aggiungi filtro HMAC-SHA256 con plugin custom</span>
}
}
filter {
mutate {
add_field => { "[@metadata][index]" => "nis2-compliance-%{+YYYY.MM}" }
}
date {
match => [ "[created]", "UNIX" ]
target => "@timestamp"
}
}
output {
elasticsearch {
hosts => ["https://elasticsearch:9200"]
index => "%{[@metadata][index]}"
}
}</pre>
</div>
<div class="section">
<h2>Microsoft Sentinel — Logic App</h2>
<pre><span style="color:#7dd3fc;">// Azure Logic App — trigger HTTP + azione Send to Log Analytics</span>
{
"triggers": {
"When_a_HTTP_request_is_received": {
"type": "Request",
"kind": "Http",
"inputs": { "schema": {} }
}
},
"actions": {
"Send_Data_to_Log_Analytics": {
"type": "ApiConnection",
"inputs": {
"body": "@{triggerBody()}",
"headers": { "Log-Type": "NIS2AgileEvent" },
"host": { "connection": { "name": "@parameters('$connections')['azureloganalyticsdatacollector']" } },
"method": "post",
"path": "/api/logs"
}
}
}
}</pre>
</div>
<div class="info-box info-purple">
<strong>Art.23 NIS2 + SIEM:</strong> Configurare alert SIEM su <code>incident.significant</code> e <code>incident.deadline_warning</code> permette di automatizzare il tracking delle scadenze 24h/72h/30d direttamente nel SOC. Il team può così gestire incidenti NIS2 senza uscire dal workflow SIEM.
</div>
</div>
</body>
</html>
+156
View File
@@ -0,0 +1,156 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SustainAI × NIS2 Agile — Integrazione</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--primary: #06b6d4; --green: #10b981; --gray-200: #e2e8f0;
--gray-500: #64748b; --gray-700: #334155; --gray-900: #0f172a;
--radius: 8px; --font: -apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;
--mono: 'Cascadia Code','Consolas',monospace;
}
body { background: #f8fafc; font-family: var(--font); color: var(--gray-900); }
.header { background: linear-gradient(135deg, #064e3b, #065f46); padding: 40px 48px; color: #fff; }
.header-badges { display: flex; gap: 10px; margin-bottom: 16px; }
.badge { padding: 4px 12px; border-radius: 20px; font-size: 0.75rem; font-weight: 700; }
.badge-nis2 { background: rgba(6,182,212,0.2); color: #67e8f9; border: 1px solid rgba(6,182,212,0.3); }
.badge-sus { background: rgba(16,185,129,0.2); color: #6ee7b7; border: 1px solid rgba(16,185,129,0.3); }
h1 { font-size: 1.875rem; font-weight: 800; margin-bottom: 8px; }
.header p { color: #a7f3d0; font-size: 1rem; }
.container { max-width: 960px; margin: 0 auto; padding: 40px 24px; }
h2 { font-size: 1.25rem; font-weight: 700; margin-bottom: 12px; padding-bottom: 10px; border-bottom: 2px solid var(--gray-200); }
.section { margin-bottom: 48px; }
p { color: var(--gray-500); font-size: 0.9rem; line-height: 1.7; margin-bottom: 12px; }
pre { background: #1e293b; color: #e2e8f0; padding: 20px; border-radius: var(--radius); font-family: var(--mono); font-size: 0.8125rem; overflow-x: auto; line-height: 1.7; margin: 12px 0; }
code { background: #f1f5f9; padding: 2px 6px; border-radius: 4px; font-family: var(--mono); font-size: 0.85em; color: var(--gray-700); }
.step { display: flex; gap: 16px; margin-bottom: 20px; padding: 18px; background: #fff; border: 1px solid var(--gray-200); border-radius: var(--radius); }
.step-num { width: 30px; height: 30px; border-radius: 50%; background: var(--green); color: #fff; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 0.875rem; flex-shrink: 0; }
.info-box { padding: 14px 16px; border-radius: var(--radius); margin: 16px 0; font-size: 0.875rem; }
.info-green { background: #f0fdf4; border-left: 3px solid var(--green); color: #065f46; }
.mapping-table { width: 100%; border-collapse: collapse; }
.mapping-table th { padding: 10px 14px; background: #f1f5f9; font-size: 0.8rem; text-align: left; font-weight: 700; }
.mapping-table td { padding: 10px 14px; border-bottom: 1px solid var(--gray-200); font-size: 0.8375rem; }
.esg-badge { padding: 2px 8px; border-radius: 4px; font-size: 0.7rem; font-weight: 700; }
.esg-g { background: #e0f2fe; color: #075985; }
.esg-e { background: #dcfce7; color: #166534; }
.esg-s { background: #faf5ff; color: #6b21a8; }
</style>
</head>
<body>
<div class="header">
<div class="header-badges">
<span class="badge badge-sus">SustainAI</span>
<span>×</span>
<span class="badge badge-nis2">NIS2 Agile</span>
</div>
<h1>Integrazione SustainAI ← NIS2 Agile</h1>
<p>Alimenta l'area Governance (G) e Sociale (S) dei report ESG/sostenibilità con i dati di compliance cybersecurity NIS2. La governance della sicurezza informatica è un KPI ESG rilevante (GRI 418, SASB, CSRD).</p>
</div>
<div class="container">
<div class="section">
<h2>Mappatura NIS2 → ESG</h2>
<p>I dati NIS2 Agile si mappano naturalmente ai framework ESG più diffusi:</p>
<table class="mapping-table">
<thead><tr><th>Dato NIS2 Agile</th><th>Endpoint</th><th>Pilastro ESG</th><th>Framework</th></tr></thead>
<tbody>
<tr><td>Compliance score Art.21</td><td><code>/services/compliance-summary</code></td><td><span class="esg-badge esg-g">G</span> Governance</td><td>GRI 205, CSRD</td></tr>
<tr><td>Policy sicurezza approvate</td><td><code>/services/policies-approved</code></td><td><span class="esg-badge esg-g">G</span> Governance</td><td>ISO 27001, GRI 418</td></tr>
<tr><td>Incidenti data breach / Art.23</td><td><code>/services/incidents-feed</code></td><td><span class="esg-badge esg-s">S</span> Sociale</td><td>GRI 418 (Privacy)</td></tr>
<tr><td>Controlli di sicurezza implementati</td><td><code>/services/controls-status</code></td><td><span class="esg-badge esg-g">G</span> Governance</td><td>SASB</td></tr>
<tr><td>Rischio supply chain fornitori</td><td><code>/services/suppliers-risk</code></td><td><span class="esg-badge esg-e">E</span> Ambientale + <span class="esg-badge esg-g">G</span></td><td>GRI 308</td></tr>
<tr><td>Segnalazioni whistleblowing</td><td><code>/api/whistleblowing/stats</code></td><td><span class="esg-badge esg-s">S</span> Sociale</td><td>GRI 205 (Anti-corruzione)</td></tr>
</tbody>
</table>
</div>
<div class="section">
<h2>Step 1 — API Key con scope minimi</h2>
<p>Crea in NIS2 Agile una chiave con scope limitati per SustainAI:</p>
<pre>Scope richiesti:
read:compliance ← score e controlli Art.21
read:incidents ← incidenti per KPI privacy/GDPR
read:policies ← policy approvate (governance evidence)
read:supply_chain ← rischio fornitori ESG</pre>
</div>
<div class="section">
<h2>Step 2 — Sync mensile per report ESG</h2>
<pre><span style="color:#7dd3fc;">// SustainAI — sync_nis2_esg.php (cron mensile)</span>
<span style="color:#f1fa8c;">$apiKey</span> = getenv(<span style="color:#86efac;">'NIS2_API_KEY'</span>);
<span style="color:#f1fa8c;">$orgId</span> = getenv(<span style="color:#86efac;">'NIS2_ORG_ID'</span>);
<span style="color:#f1fa8c;">$base</span> = <span style="color:#86efac;">'https://nis2.certisource.it/api'</span>;
<span style="color:#f1fa8c;">$headers</span> = [
<span style="color:#86efac;">'X-API-Key: '</span> . <span style="color:#f1fa8c;">$apiKey</span>,
<span style="color:#86efac;">'X-Organization-Id: '</span> . <span style="color:#f1fa8c;">$orgId</span>,
];
<span style="color:#7dd3fc;">// G — Compliance score (KPI governance cybersecurity)</span>
<span style="color:#f1fa8c;">$compliance</span> = nis2Get(<span style="color:#f1fa8c;">$base</span> . <span style="color:#86efac;">'/services/compliance-summary'</span>, <span style="color:#f1fa8c;">$headers</span>);
<span style="color:#f1fa8c;">$cyberScore</span> = <span style="color:#f1fa8c;">$compliance</span>[<span style="color:#86efac;">'data'</span>][<span style="color:#86efac;">'overall_score'</span>] ?? 0;
<span style="color:#f1fa8c;">$policyCount</span> = <span style="color:#f1fa8c;">$compliance</span>[<span style="color:#86efac;">'data'</span>][<span style="color:#86efac;">'policies'</span>][<span style="color:#86efac;">'approved'</span>] ?? 0;
<span style="color:#7dd3fc;">// S — Privacy breaches (GRI 418)</span>
<span style="color:#f1fa8c;">$incidents</span> = nis2Get(<span style="color:#f1fa8c;">$base</span> . <span style="color:#86efac;">'/services/incidents-feed?significant_only=1'</span>, <span style="color:#f1fa8c;">$headers</span>);
<span style="color:#f1fa8c;">$breaches</span> = array_filter(
<span style="color:#f1fa8c;">$incidents</span>[<span style="color:#86efac;">'data'</span>][<span style="color:#86efac;">'incidents'</span>] ?? [],
fn(<span style="color:#f1fa8c;">$i</span>) => <span style="color:#f1fa8c;">$i</span>[<span style="color:#86efac;">'classification'</span>] === <span style="color:#86efac;">'data_breach'</span>
);
<span style="color:#7dd3fc;">// G — Supply chain risk (ESG fornitori)</span>
<span style="color:#f1fa8c;">$suppliers</span> = nis2Get(<span style="color:#f1fa8c;">$base</span> . <span style="color:#86efac;">'/services/suppliers-risk'</span>, <span style="color:#f1fa8c;">$headers</span>);
<span style="color:#f1fa8c;">$highRiskSuppliers</span> = <span style="color:#f1fa8c;">$suppliers</span>[<span style="color:#86efac;">'data'</span>][<span style="color:#86efac;">'stats'</span>][<span style="color:#86efac;">'high'</span>] +
<span style="color:#f1fa8c;">$suppliers</span>[<span style="color:#86efac;">'data'</span>][<span style="color:#86efac;">'stats'</span>][<span style="color:#86efac;">'critical'</span>];
<span style="color:#7dd3fc;">// Aggiorna KPI ESG in SustainAI</span>
EsgKpiService::updateCyberGovernance([
<span style="color:#86efac;">'nis2_score'</span> => <span style="color:#f1fa8c;">$cyberScore</span>,
<span style="color:#86efac;">'policies_approved'</span> => <span style="color:#f1fa8c;">$policyCount</span>,
<span style="color:#86efac;">'data_breaches'</span> => count(<span style="color:#f1fa8c;">$breaches</span>),
<span style="color:#86efac;">'high_risk_suppliers'</span> => <span style="color:#f1fa8c;">$highRiskSuppliers</span>,
<span style="color:#86efac;">'period'</span> => date(<span style="color:#86efac;">'Y-m'</span>),
]);</pre>
</div>
<div class="section">
<h2>Widget NIS2 per Report ESG SustainAI</h2>
<pre>&lt;!-- SustainAI: sezione Governance → Cybersecurity KPIs --&gt;
&lt;div id="nis2-esg-widget" style="padding:20px; border:1px solid #e2e8f0; border-radius:8px; background:#f0fdf4;"&gt;&lt;/div&gt;
&lt;script&gt;
fetch('https://nis2.certisource.it/api/services/compliance-summary', {
headers: { 'X-API-Key': '<span style="color:#86efac;">nis2_YOUR_KEY</span>', 'X-Organization-Id': '<span style="color:#86efac;">ORG_ID</span>' }
}).then(r => r.json()).then(({ data }) => {
document.getElementById('nis2-esg-widget').innerHTML = `
&lt;h4 style="font-size:.875rem; font-weight:700; color:#065f46; margin-bottom:16px;"&gt;
🔒 Governance Cybersecurity — NIS2 Compliance
&lt;/h4&gt;
&lt;div style="display:grid; grid-template-columns:repeat(4,1fr); gap:12px; text-align:center;"&gt;
&lt;div&gt;&lt;div style="font-size:1.5rem; font-weight:800; color:#06b6d4;"&gt;${data.overall_score}%&lt;/div&gt;
&lt;div style="font-size:.7rem; color:#64748b;"&gt;NIS2 Score&lt;/div&gt;&lt;/div&gt;
&lt;div&gt;&lt;div style="font-size:1.5rem; font-weight:800; color:#10b981;"&gt;${data.policies.approved}&lt;/div&gt;
&lt;div style="font-size:.7rem; color:#64748b;"&gt;Policy Approvate&lt;/div&gt;&lt;/div&gt;
&lt;div&gt;&lt;div style="font-size:1.5rem; font-weight:800; color:#f59e0b;"&gt;${data.risks.high}&lt;/div&gt;
&lt;div style="font-size:.7rem; color:#64748b;"&gt;Rischi HIGH&lt;/div&gt;&lt;/div&gt;
&lt;div&gt;&lt;div style="font-size:1.5rem; font-weight:800; color:#ef4444;"&gt;${data.incidents.significant}&lt;/div&gt;
&lt;div style="font-size:.7rem; color:#64748b;"&gt;Incidenti Art.23&lt;/div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p style="font-size:.7rem; color:#94a3b8; margin-top:12px; text-align:right;"&gt;
Fonte: NIS2 Agile — nis2.certisource.it — Aggiornato: ${new Date().toLocaleDateString('it')}
&lt;/p&gt;`;
});
&lt;/script&gt;</pre>
</div>
<div class="info-box info-green">
<strong>CSRD / ESRS E5:</strong> La cybersecurity è esplicitamente inclusa nelle ESRS come rischio materiale (ESRS 2 IRO-1). NIS2 Agile fornisce le evidenze documentali per il reporting CSRD sul governo dei rischi digitali.
</div>
</div>
</body>
</html>
+7 -5
View File
@@ -192,10 +192,12 @@ function loadSidebar() {
{
label: 'Gestione', i18nKey: 'nav.management',
items: [
{ 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' },
{ name: 'Supply Chain', href: 'supply-chain.html', icon: iconLink(), i18nKey: 'nav.supply_chain' },
{ 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' },
{ name: 'Supply Chain', href: 'supply-chain.html', icon: iconLink(), i18nKey: 'nav.supply_chain' },
{ name: 'Segnalazioni', href: 'whistleblowing.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"/></svg>` },
{ name: 'Normative', href: 'normative.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clip-rule="evenodd"/></svg>` },
]
},
{
@@ -203,7 +205,7 @@ function loadSidebar() {
items: [
{ name: 'Formazione', href: 'training.html', icon: iconAcademicCap(), i18nKey: 'nav.training' },
{ name: 'Asset', href: 'assets.html', icon: iconServer(), i18nKey: 'nav.assets' },
{ name: 'Audit & Report',href: 'reports.html', icon: iconChartBar(), i18nKey: 'nav.audit' },
{ name: 'Audit & Report',href: 'reports.html', icon: iconChartBar(), i18nKey: 'nav.audit' },
]
},
{
+256
View File
@@ -0,0 +1,256 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Aggiornamenti Normativi - NIS2 Agile</title>
<link rel="stylesheet" href="/css/style.css">
<style>
.impact-badge { display: inline-flex; align-items: center; padding: 2px 10px; border-radius: 12px; font-size: 0.7rem; font-weight: 700; }
.impact-critical { background: var(--danger-bg); color: var(--danger); }
.impact-high { background: #fff7ed; color: #c2410c; }
.impact-medium { background: var(--warning-bg); color: #a16207; }
.impact-low { background: var(--gray-100); color: var(--gray-600); }
.impact-informational { background: rgba(6,182,212,0.1); color: #0891b2; }
.ack-badge { display: inline-flex; align-items: center; gap: 5px; padding: 2px 10px; border-radius: 12px; font-size: 0.7rem; font-weight: 600; }
.ack-done { background: var(--success-bg); color: var(--success); }
.ack-pending { background: var(--warning-bg); color: #a16207; }
.source-chip { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 4px; font-size: 0.7rem; font-weight: 700; background: var(--gray-100); color: var(--gray-600); font-family: monospace; }
.update-card { border: 1px solid var(--gray-200); border-radius: var(--border-radius-lg); padding: 20px; margin-bottom: 16px; transition: all var(--transition-fast); }
.update-card:hover { border-color: var(--primary); box-shadow: 0 2px 8px rgba(6,182,212,0.1); }
.update-card.action-required { border-left: 3px solid var(--warning); }
.update-card.acked { opacity: 0.7; }
.ack-progress { height: 6px; background: var(--gray-200); border-radius: 3px; overflow: hidden; }
.ack-progress-bar { height: 100%; background: var(--primary); border-radius: 3px; transition: width 0.5s ease; }
</style>
</head>
<body>
<div class="app-layout">
<div id="sidebar-container"></div>
<main class="main-content">
<div class="page-header">
<div class="page-header-content">
<h1 class="page-title">Aggiornamenti Normativi</h1>
<p class="page-subtitle">Feed NIS2 / ACN / DORA — Presa visione obbligatoria per audit compliance</p>
</div>
<div class="page-header-actions">
<button class="btn btn-secondary" onclick="loadPending()">
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"/></svg>
Solo Pendenti
</button>
</div>
</div>
<!-- Stats -->
<div class="stats-grid mb-24" id="stats-grid">
<div class="stat-card"><div class="spinner"></div></div>
</div>
<!-- Ack Progress bar -->
<div class="card mb-24">
<div class="card-body" style="padding:16px 20px;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">
<span style="font-size:0.875rem; font-weight:600; color:var(--gray-700);">Avanzamento presa visione</span>
<span id="ack-pct" style="font-size:0.875rem; font-weight:700; color:var(--primary);">—</span>
</div>
<div class="ack-progress">
<div class="ack-progress-bar" id="ack-bar" style="width:0%"></div>
</div>
<p style="font-size:0.75rem; color:var(--gray-500); margin-top:6px;">Documentare la presa visione degli aggiornamenti normativi è richiesto per la compliance NIS2 Art.21.</p>
</div>
</div>
<!-- Filters -->
<div class="card mb-16">
<div class="card-body" style="padding:14px 20px;">
<div style="display:flex; gap:12px; flex-wrap:wrap; align-items:center;">
<select id="filter-source" class="form-control" style="width:auto;" onchange="loadUpdates()">
<option value="">Tutte le fonti</option>
<option value="dlgs_138_2024">D.Lgs. 138/2024</option>
<option value="nis2_directive">Direttiva NIS2</option>
<option value="acn_guideline">ACN Guideline</option>
<option value="dora">DORA</option>
<option value="enisa">ENISA</option>
<option value="iso27001">ISO 27001</option>
</select>
<select id="filter-impact" class="form-control" style="width:auto;" onchange="loadUpdates()">
<option value="">Tutti i livelli</option>
<option value="critical">Critico</option>
<option value="high">Alto</option>
<option value="medium">Medio</option>
<option value="low">Basso</option>
<option value="informational">Informativo</option>
</select>
<label style="display:flex; align-items:center; gap:8px; font-size:0.875rem; cursor:pointer;">
<input type="checkbox" id="filter-action" onchange="loadUpdates()" style="accent-color:var(--primary);">
Solo azione richiesta
</label>
</div>
</div>
</div>
<div id="updates-container"><div class="spinner" style="margin:60px auto;"></div></div>
</main>
</div>
<script src="/js/api.js"></script>
<script src="/js/common.js"></script>
<script src="/js/i18n.js"></script>
<script src="/js/help.js"></script>
<script>
if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar();
I18n.init();
const sourceLabels = {
nis2_directive: 'NIS2 Directive', dlgs_138_2024: 'D.Lgs. 138/2024',
acn_guideline: 'ACN', dora: 'DORA', enisa: 'ENISA', iso27001: 'ISO 27001', other: 'Altro'
};
const impactLabels = {
critical: 'Critico', high: 'Alto', medium: 'Medio', low: 'Basso', informational: 'Informativo'
};
async function loadStats() {
try {
const result = await api.request('GET', '/normative/stats');
if (result.success) renderStats(result.data);
} catch (e) {}
}
function renderStats(d) {
document.getElementById('stats-grid').innerHTML = `
<div class="stat-card">
<div class="stat-label">Aggiornamenti Totali</div>
<div class="stat-value">${d.total_updates || 0}</div>
</div>
<div class="stat-card">
<div class="stat-label">Prese Visione</div>
<div class="stat-value" style="color:var(--success);">${d.acknowledged || 0}</div>
</div>
<div class="stat-card">
<div class="stat-label">Pendenti</div>
<div class="stat-value" style="color:var(--warning);">${d.pending || 0}</div>
</div>
<div class="stat-card">
<div class="stat-label">Compliance ACK</div>
<div class="stat-value" style="color:var(--primary);">${d.ack_rate || 0}%</div>
</div>`;
document.getElementById('ack-pct').textContent = (d.ack_rate || 0) + '%';
document.getElementById('ack-bar').style.width = (d.ack_rate || 0) + '%';
}
async function loadUpdates() {
const container = document.getElementById('updates-container');
container.innerHTML = '<div class="spinner" style="margin:60px auto;"></div>';
try {
const params = new URLSearchParams();
const source = document.getElementById('filter-source').value;
const impact = document.getElementById('filter-impact').value;
const action = document.getElementById('filter-action').checked;
if (source) params.append('source', source);
if (impact) params.append('impact', impact);
if (action) params.append('action_required', '1');
const result = await api.request('GET', '/normative/list?' + params);
if (result.success) renderUpdates(result.data.updates || []);
} catch (e) {
container.innerHTML = '<div class="empty-state"><h4>Errore caricamento aggiornamenti</h4></div>';
}
}
async function loadPending() {
const container = document.getElementById('updates-container');
container.innerHTML = '<div class="spinner" style="margin:60px auto;"></div>';
try {
const result = await api.request('GET', '/normative/pending');
if (result.success) renderUpdates(result.data.pending || [], true);
} catch (e) { container.innerHTML = '<div class="empty-state"><h4>Errore.</h4></div>'; }
}
function renderUpdates(updates, pendingOnly) {
const container = document.getElementById('updates-container');
if (!updates.length) {
container.innerHTML = `<div class="empty-state">
<svg viewBox="0 0 20 20" fill="currentColor" width="40" height="40" style="color:var(--gray-300)"><path fill-rule="evenodd" d="M6 2a2 2 0 00-2 2v12a2 2 0 002 2h8a2 2 0 002-2V7.414A2 2 0 0015.414 6L12 2.586A2 2 0 0010.586 2H6zm2 10a1 1 0 10-2 0v3a1 1 0 102 0v-3zm2-3a1 1 0 011 1v5a1 1 0 11-2 0v-5a1 1 0 011-1zm4-1a1 1 0 10-2 0v6a1 1 0 102 0V8z" clip-rule="evenodd"/></svg>
<h4>${pendingOnly ? 'Nessun aggiornamento pendente — tutto preso in visione!' : 'Nessun aggiornamento trovato'}</h4>
</div>`;
return;
}
let html = '';
updates.forEach(u => {
const acked = u.is_acknowledged || !!(u.acknowledged_at);
const actionClass = u.action_required ? 'action-required' : '';
const ackedClass = acked ? 'acked' : '';
const domains = (u.affected_domains || []).map(d =>
`<span class="source-chip">${escapeHtml(d)}</span>`
).join(' ');
html += `<div class="update-card ${actionClass} ${ackedClass}" id="update-${u.id}">
<div style="display:flex; justify-content:space-between; align-items:flex-start; gap:12px; flex-wrap:wrap;">
<div style="flex:1;">
<div style="display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-bottom:8px;">
<span class="source-chip">${escapeHtml(sourceLabels[u.source] || u.source)}</span>
<span class="impact-badge impact-${u.impact_level}">${escapeHtml(impactLabels[u.impact_level] || u.impact_level)}</span>
${u.action_required ? '<span style="font-size:0.7rem; font-weight:700; color:var(--warning); background:var(--warning-bg); padding:2px 8px; border-radius:4px;">Azione richiesta</span>' : ''}
${acked
? `<span class="ack-badge ack-done"><svg viewBox="0 0 20 20" fill="currentColor" width="11" height="11"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>Preso in visione ${u.ack_by_name ? 'da ' + escapeHtml(u.ack_by_name) : ''}</span>`
: '<span class="ack-badge ack-pending">Da prendere in visione</span>'
}
</div>
<h3 style="font-size:1rem; font-weight:700; color:var(--gray-900); margin-bottom:6px;">${escapeHtml(u.title)}</h3>
${u.reference ? `<p style="font-size:0.75rem; color:var(--gray-500); margin-bottom:8px;"><code>${escapeHtml(u.reference)}</code></p>` : ''}
<p style="font-size:0.8125rem; color:var(--gray-600); line-height:1.6; margin-bottom:10px;">${escapeHtml(u.summary)}</p>
${domains ? `<div style="display:flex; gap:6px; flex-wrap:wrap;">${domains}</div>` : ''}
</div>
<div style="display:flex; flex-direction:column; gap:8px; align-items:flex-end; flex-shrink:0;">
${u.effective_date ? `<p style="font-size:0.75rem; color:var(--gray-500);">In vigore: <strong>${formatDate(u.effective_date)}</strong></p>` : ''}
${u.url ? `<a href="${escapeHtml(u.url)}" target="_blank" rel="noopener" class="btn btn-sm btn-secondary" style="font-size:0.75rem;">
<svg viewBox="0 0 20 20" fill="currentColor" width="12" height="12"><path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z"/><path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z"/></svg>
Fonte
</a>` : ''}
${!acked ? `<button class="btn btn-sm btn-primary" onclick="acknowledgeUpdate(${u.id})">
<svg viewBox="0 0 20 20" fill="currentColor" width="14" height="14"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>
Prendi in visione
</button>` : ''}
</div>
</div>
</div>`;
});
container.innerHTML = html;
}
async function acknowledgeUpdate(id) {
showModal('Presa Visione', `
<p style="font-size:0.875rem; color:var(--gray-600); margin-bottom:16px;">
Documentare la presa visione dell'aggiornamento normativo. Opzionalmente aggiungere note sull'impatto per la tua organizzazione.
</p>
<div class="form-group">
<label class="form-label">Note (opzionale)</label>
<textarea id="ack-notes" class="form-control" rows="3" placeholder="Es. Analizzato con il team di compliance. Nessun impatto diretto rilevato..."></textarea>
</div>
`, `<button class="btn btn-primary" onclick="confirmAck(${id})">Conferma presa visione</button>
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>`);
}
async function confirmAck(id) {
const notes = document.getElementById('ack-notes').value;
try {
const result = await api.request('POST', `/normative/${id}/ack`, { notes });
if (result.success) {
closeModal();
showNotification('Presa visione registrata e documentata.', 'success');
loadUpdates();
loadStats();
} else showNotification(result.message || 'Errore.', 'error');
} catch (e) { showNotification('Errore di connessione.', 'error'); }
}
// Init
loadStats();
loadUpdates();
</script>
</body>
</html>
+325 -5
View File
@@ -212,6 +212,8 @@
<button class="settings-tab" onclick="switchTab('profile')">Profilo</button>
<button class="settings-tab" onclick="switchTab('members')">Membri</button>
<button class="settings-tab" onclick="switchTab('security')">Sicurezza</button>
<button class="settings-tab" onclick="switchTab('apikeys')">API Keys</button>
<button class="settings-tab" onclick="switchTab('webhooks')">Webhook</button>
</div>
<!-- ══════════════ TAB: Organizzazione ══════════════ -->
@@ -479,6 +481,64 @@
</div>
</div>
</div>
<!-- ══════════════ TAB: API Keys ══════════════ -->
<div class="tab-panel" id="tab-apikeys">
<div class="card mb-24">
<div class="card-header" style="display:flex; justify-content:space-between; align-items:center;">
<div>
<h3>API Keys</h3>
<p style="font-size:0.8125rem; color:var(--gray-500); margin-top:4px;">
Chiavi per accesso esterno alle API NIS2 Agile (SIEM, GRC, dashboard esterne).
</p>
</div>
<button class="btn btn-primary" onclick="showCreateApiKeyModal()">
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"/></svg>
Nuova API Key
</button>
</div>
<div class="card-body" style="padding:0;">
<div id="apikeys-container"><div class="spinner" style="margin:40px auto;"></div></div>
</div>
</div>
<div class="card">
<div class="card-header"><h3>Scope Disponibili</h3></div>
<div class="card-body">
<div id="available-scopes-container"></div>
</div>
</div>
</div>
<!-- ══════════════ TAB: Webhook ══════════════ -->
<div class="tab-panel" id="tab-webhooks">
<div class="card mb-24">
<div class="card-header" style="display:flex; justify-content:space-between; align-items:center;">
<div>
<h3>Webhook Subscriptions</h3>
<p style="font-size:0.8125rem; color:var(--gray-500); margin-top:4px;">
Notifiche push verso sistemi esterni (SIEM, 231 Agile, SustainAI) su eventi NIS2.
</p>
</div>
<button class="btn btn-primary" onclick="showCreateWebhookModal()">
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"/></svg>
Nuovo Webhook
</button>
</div>
<div class="card-body" style="padding:0;">
<div id="webhooks-container"><div class="spinner" style="margin:40px auto;"></div></div>
</div>
</div>
<div class="card">
<div class="card-header" style="display:flex; justify-content:space-between; align-items:center;">
<h3>Delivery Log</h3>
<button class="btn btn-sm btn-secondary" onclick="loadDeliveries()">Aggiorna</button>
</div>
<div class="card-body" style="padding:0;">
<div id="deliveries-container"><div class="empty-state" style="padding:32px;"><p>Seleziona un webhook per vedere i delivery.</p></div></div>
</div>
</div>
</div>
</div>
</main>
</div>
@@ -506,20 +566,19 @@
// ── Tab Navigation ───────────────────────────────────────
function switchTab(tab) {
// Rimuovi active da tutti i tab e pannelli
document.querySelectorAll('.settings-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
// Attiva il tab selezionato
const tabMap = { org: 0, profile: 1, members: 2, security: 3 };
const panelMap = { org: 'tab-org', profile: 'tab-profile', members: 'tab-members', security: 'tab-security' };
const tabMap = { org: 0, profile: 1, members: 2, security: 3, apikeys: 4, webhooks: 5 };
const panelMap = { org: 'tab-org', profile: 'tab-profile', members: 'tab-members', security: 'tab-security', apikeys: 'tab-apikeys', webhooks: 'tab-webhooks' };
document.querySelectorAll('.settings-tab')[tabMap[tab]].classList.add('active');
document.getElementById(panelMap[tab]).classList.add('active');
// Carica dati del tab se necessario
if (tab === 'members') loadMembers();
if (tab === 'security') loadAuditLog();
if (tab === 'apikeys') loadApiKeys();
if (tab === 'webhooks') { loadWebhooks(); loadDeliveries(); }
}
// ── Settori NIS2 ─────────────────────────────────────────
@@ -979,6 +1038,267 @@
}
}
// ── API Keys ─────────────────────────────────────────────
async function loadApiKeys() {
const container = document.getElementById('apikeys-container');
container.innerHTML = '<div class="spinner" style="margin:40px auto;"></div>';
try {
const result = await api.request('GET', '/webhooks/api-keys');
if (result.success) {
renderApiKeys(result.data.api_keys || []);
renderAvailableScopes(result.data.available_scopes || {});
}
} catch (e) { container.innerHTML = '<div class="empty-state"><h4>Errore caricamento API Keys</h4></div>'; }
}
function renderApiKeys(keys) {
const container = document.getElementById('apikeys-container');
if (!keys.length) {
container.innerHTML = `<div class="empty-state" style="padding:40px;"><svg viewBox="0 0 20 20" fill="currentColor" width="40" height="40" style="color:var(--gray-300)"><path fill-rule="evenodd" d="M18 8a6 6 0 01-7.743 5.743L10 14l-1 1-1 1H6v2H2v-4l4.257-4.257A6 6 0 1118 8zm-6-4a1 1 0 100 2 2 2 0 012 2 1 1 0 102 0 4 4 0 00-4-4z" clip-rule="evenodd"/></svg><h4>Nessuna API Key</h4><p>Crea una chiave per integrare sistemi esterni.</p></div>`;
return;
}
let html = `<div class="table-container"><table><thead><tr><th>Nome</th><th>Prefisso</th><th>Scopes</th><th>Ultimo Uso</th><th>Scadenza</th><th>Stato</th><th>Azioni</th></tr></thead><tbody>`;
keys.forEach(k => {
const active = k.is_active ? '<span class="badge badge-success">Attiva</span>' : '<span class="badge badge-danger">Revocata</span>';
const scopes = (k.scopes || []).map(s => `<span class="badge badge-neutral" style="font-size:0.65rem;">${escapeHtml(s)}</span>`).join(' ');
html += `<tr>
<td><strong>${escapeHtml(k.name)}</strong><br><small style="color:var(--gray-400);">Creata da ${escapeHtml(k.created_by_name || '-')}</small></td>
<td><code style="font-size:0.8rem;">${escapeHtml(k.key_prefix)}...</code></td>
<td>${scopes}</td>
<td>${k.last_used_at ? formatDateTime(k.last_used_at) : '<span style="color:var(--gray-400);">Mai</span>'}</td>
<td>${k.expires_at ? formatDate(k.expires_at) : '<span style="color:var(--gray-400);">Nessuna</span>'}</td>
<td>${active}</td>
<td>${k.is_active ? `<button class="btn-icon-action btn-danger-hover" onclick="revokeApiKey(${k.id}, '${escapeHtml(k.name)}')" title="Revoca"><svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9z" clip-rule="evenodd"/></svg></button>` : ''}</td>
</tr>`;
});
html += '</tbody></table></div>';
container.innerHTML = html;
}
function renderAvailableScopes(scopes) {
const container = document.getElementById('available-scopes-container');
let html = '<div style="display:grid; grid-template-columns:repeat(auto-fill,minmax(280px,1fr)); gap:12px;">';
Object.entries(scopes).forEach(([key, desc]) => {
html += `<div style="padding:12px; background:var(--gray-50); border-radius:var(--border-radius); border:1px solid var(--gray-200);">
<code style="font-size:0.8rem; color:var(--primary);">${escapeHtml(key)}</code>
<p style="font-size:0.8rem; color:var(--gray-600); margin-top:4px;">${escapeHtml(desc)}</p>
</div>`;
});
html += '</div>';
container.innerHTML = html;
}
function showCreateApiKeyModal() {
showModal('Crea API Key', `
<div class="form-group">
<label class="form-label">Nome *</label>
<input type="text" id="apikey-name" class="form-control" placeholder="Es. SIEM Integration, Dashboard ESG..." required>
</div>
<div class="form-group">
<label class="form-label">Scopes *</label>
<div id="apikey-scopes-checkboxes" style="display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-top:8px;">
${['read:all','read:compliance','read:risks','read:incidents','read:assets','read:supply_chain','read:policies'].map(s =>
`<label style="display:flex; align-items:center; gap:8px; font-size:0.875rem; cursor:pointer;">
<input type="checkbox" name="scope" value="${s}" style="accent-color:var(--primary);">
<code style="font-size:0.75rem;">${s}</code>
</label>`
).join('')}
</div>
</div>
<div class="form-group">
<label class="form-label">Scadenza (opzionale)</label>
<input type="date" id="apikey-expires" class="form-control">
</div>
`, '<button class="btn btn-primary" onclick="createApiKey()">Crea API Key</button>');
}
async function createApiKey() {
const name = document.getElementById('apikey-name').value.trim();
const scopes = [...document.querySelectorAll('input[name="scope"]:checked')].map(cb => cb.value);
const expires = document.getElementById('apikey-expires').value;
if (!name) { showNotification('Inserisci un nome per la chiave.', 'error'); return; }
if (!scopes.length) { showNotification('Seleziona almeno uno scope.', 'error'); return; }
try {
const result = await api.request('POST', '/webhooks/api-keys', { name, scopes, expires_at: expires || null });
if (result.success) {
closeModal();
showModal('API Key Creata', `
<div style="padding:16px; background:var(--warning-bg); border:1px solid var(--warning); border-radius:var(--border-radius); margin-bottom:16px;">
<strong style="color:#a16207;">⚠ Salva questa chiave ora. Non sarà più visibile.</strong>
</div>
<div style="padding:12px; background:var(--gray-900); border-radius:var(--border-radius); font-family:monospace; font-size:0.875rem; color:#34d399; word-break:break-all; user-select:all;">
${escapeHtml(result.data.key)}
</div>
<p style="font-size:0.8rem; color:var(--gray-500); margin-top:8px;">Usa come header: <code>X-API-Key: ${escapeHtml(result.data.key)}</code></p>
`, '<button class="btn btn-primary" onclick="closeModal(); loadApiKeys();">Ho salvato la chiave</button>');
} else { showNotification(result.message || 'Errore.', 'error'); }
} catch (e) { showNotification('Errore di connessione.', 'error'); }
}
async function revokeApiKey(id, name) {
if (!confirm(`Revocare la chiave "${name}"? L'operazione è irreversibile.`)) return;
try {
const result = await api.request('DELETE', `/webhooks/api-keys/${id}`);
if (result.success) { showNotification('API Key revocata.', 'success'); loadApiKeys(); }
else showNotification(result.message || 'Errore.', 'error');
} catch (e) { showNotification('Errore di connessione.', 'error'); }
}
// ── Webhooks ─────────────────────────────────────────────
const availableEvents = {
'incident.created': 'Nuovo incidente', 'incident.updated': 'Incidente aggiornato',
'incident.significant': 'Incidente significativo (Art.23)', 'incident.deadline_warning': 'Scadenza Art.23 imminente',
'risk.high_created': 'Rischio HIGH/CRITICAL', 'risk.updated': 'Rischio aggiornato',
'compliance.score_changed': 'Variazione compliance >5%', 'policy.approved': 'Policy approvata',
'policy.created': 'Nuova policy', 'supplier.risk_flagged': 'Fornitore a rischio',
'assessment.completed': 'Assessment completato', 'whistleblowing.received': 'Nuova segnalazione',
'normative.update': 'Aggiornamento normativo', 'webhook.test': 'Test',
'*': 'Tutti gli eventi (wildcard)'
};
async function loadWebhooks() {
const container = document.getElementById('webhooks-container');
container.innerHTML = '<div class="spinner" style="margin:40px auto;"></div>';
try {
const result = await api.request('GET', '/webhooks/subscriptions');
if (result.success) renderWebhooks(result.data.subscriptions || []);
} catch (e) { container.innerHTML = '<div class="empty-state"><h4>Errore caricamento webhook</h4></div>'; }
}
function renderWebhooks(subs) {
const container = document.getElementById('webhooks-container');
if (!subs.length) {
container.innerHTML = `<div class="empty-state" style="padding:40px;"><svg viewBox="0 0 20 20" fill="currentColor" width="40" height="40" style="color:var(--gray-300)"><path d="M2 5a2 2 0 012-2h7a2 2 0 012 2v4a2 2 0 01-2 2H9l-3 3v-3H4a2 2 0 01-2-2V5z"/><path d="M15 7v2a4 4 0 01-4 4H9.828l-1.766 1.767c.28.149.599.233.938.233h2l3 3v-3h2a2 2 0 002-2V9a2 2 0 00-2-2h-1z"/></svg><h4>Nessun Webhook</h4><p>Configura webhook per notifiche push verso SIEM e sistemi esterni.</p></div>`;
return;
}
let html = `<div class="table-container"><table><thead><tr><th>Nome</th><th>URL</th><th>Eventi</th><th>Delivery</th><th>Stato</th><th>Azioni</th></tr></thead><tbody>`;
subs.forEach(s => {
const evts = (s.events || []).map(e => `<span class="badge badge-neutral" style="font-size:0.65rem;">${escapeHtml(e)}</span>`).join(' ');
const active = s.is_active ? '<span class="badge badge-success">Attivo</span>' : '<span class="badge badge-warning">Pausa</span>';
const deliveryInfo = `<small style="color:var(--gray-500);">${s.success_deliveries||0}✓ ${s.failed_deliveries||0}✗</small>`;
html += `<tr>
<td><strong>${escapeHtml(s.name)}</strong>${s.failure_count >= 5 ? '<br><span style="color:var(--danger);font-size:0.75rem;">⚠ '+s.failure_count+' errori</span>' : ''}</td>
<td><code style="font-size:0.75rem; word-break:break-all;">${escapeHtml(s.url)}</code></td>
<td>${evts}</td>
<td>${deliveryInfo}</td>
<td>${active}</td>
<td style="white-space:nowrap;">
<button class="btn-icon-action" onclick="testWebhook(${s.id})" title="Test Ping">
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd"/></svg>
</button>
<button class="btn-icon-action" onclick="toggleWebhook(${s.id}, ${s.is_active})" title="${s.is_active ? 'Disabilita' : 'Abilita'}">
<svg viewBox="0 0 20 20" fill="currentColor"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/></svg>
</button>
<button class="btn-icon-action btn-danger-hover" onclick="deleteWebhook(${s.id}, '${escapeHtml(s.name)}')" title="Elimina">
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9z" clip-rule="evenodd"/></svg>
</button>
</td>
</tr>`;
});
html += '</tbody></table></div>';
container.innerHTML = html;
}
function showCreateWebhookModal() {
const evtCheckboxes = Object.entries(availableEvents).map(([key, label]) =>
`<label style="display:flex; align-items:center; gap:8px; font-size:0.8rem; cursor:pointer; padding:4px 0;">
<input type="checkbox" name="wh-event" value="${key}" style="accent-color:var(--primary);">
<span><code style="font-size:0.7rem;">${key}</code> — ${escapeHtml(label)}</span>
</label>`
).join('');
showModal('Crea Webhook', `
<div class="form-group">
<label class="form-label">Nome *</label>
<input type="text" id="wh-name" class="form-control" placeholder="Es. SIEM Integration, 231 Agile Notify...">
</div>
<div class="form-group">
<label class="form-label">URL Endpoint * (https://...)</label>
<input type="url" id="wh-url" class="form-control" placeholder="https://your-siem.example.com/webhooks/nis2">
</div>
<div class="form-group">
<label class="form-label">Eventi da ascoltare *</label>
<div style="max-height:200px; overflow-y:auto; padding:8px; border:1px solid var(--gray-200); border-radius:var(--border-radius); margin-top:8px;">${evtCheckboxes}</div>
</div>
`, '<button class="btn btn-primary" onclick="createWebhook()">Crea Webhook</button>');
}
async function createWebhook() {
const name = document.getElementById('wh-name').value.trim();
const url = document.getElementById('wh-url').value.trim();
const events = [...document.querySelectorAll('input[name="wh-event"]:checked')].map(cb => cb.value);
if (!name || !url) { showNotification('Nome e URL obbligatori.', 'error'); return; }
if (!events.length) { showNotification('Seleziona almeno un evento.', 'error'); return; }
try {
const result = await api.request('POST', '/webhooks/subscriptions', { name, url, events });
if (result.success) {
closeModal();
showModal('Webhook Creato', `
<div style="padding:16px; background:var(--warning-bg); border:1px solid var(--warning); border-radius:var(--border-radius); margin-bottom:16px;">
<strong style="color:#a16207;">⚠ Salva il secret per verificare la firma HMAC. Non sarà più visibile.</strong>
</div>
<p style="font-size:0.875rem; color:var(--gray-600); margin-bottom:8px;">Usa questo secret per verificare l'header <code>X-NIS2-Signature</code>:</p>
<div style="padding:12px; background:var(--gray-900); border-radius:var(--border-radius); font-family:monospace; font-size:0.875rem; color:#34d399; word-break:break-all; user-select:all;">
${escapeHtml(result.data.secret)}
</div>
<p style="font-size:0.75rem; color:var(--gray-500); margin-top:8px;">Firma: <code>sha256=HMAC_SHA256(body, secret)</code></p>
`, '<button class="btn btn-primary" onclick="closeModal(); loadWebhooks();">Ho salvato il secret</button>');
} else showNotification(result.message || 'Errore.', 'error');
} catch (e) { showNotification('Errore di connessione.', 'error'); }
}
async function testWebhook(id) {
try {
const result = await api.request('POST', `/webhooks/subscriptions/${id}/test`);
if (result.success) { showNotification('Ping di test inviato. Controlla i delivery log.', 'success'); loadDeliveries(); }
else showNotification(result.message || 'Errore.', 'error');
} catch (e) { showNotification('Errore di connessione.', 'error'); }
}
async function toggleWebhook(id, currentActive) {
try {
const result = await api.request('PUT', `/webhooks/subscriptions/${id}`, { is_active: currentActive ? 0 : 1 });
if (result.success) { loadWebhooks(); showNotification(currentActive ? 'Webhook disabilitato.' : 'Webhook abilitato.', 'success'); }
} catch (e) { showNotification('Errore di connessione.', 'error'); }
}
async function deleteWebhook(id, name) {
if (!confirm(`Eliminare il webhook "${name}"?`)) return;
try {
const result = await api.request('DELETE', `/webhooks/subscriptions/${id}`);
if (result.success) { showNotification('Webhook eliminato.', 'success'); loadWebhooks(); }
} catch (e) { showNotification('Errore di connessione.', 'error'); }
}
async function loadDeliveries(subscriptionId) {
const container = document.getElementById('deliveries-container');
container.innerHTML = '<div class="spinner" style="margin:40px auto;"></div>';
try {
const url = subscriptionId ? `/webhooks/deliveries?subscription_id=${subscriptionId}` : '/webhooks/deliveries';
const result = await api.request('GET', url);
if (result.success) {
const deliveries = result.data.deliveries || [];
if (!deliveries.length) {
container.innerHTML = '<div class="empty-state" style="padding:32px;"><h4>Nessun delivery registrato</h4></div>';
return;
}
let html = `<div class="table-container"><table><thead><tr><th>Evento</th><th>Webhook</th><th>Stato</th><th>HTTP</th><th>Tentativo</th><th>Data</th></tr></thead><tbody>`;
deliveries.forEach(d => {
const statusClass = d.status === 'delivered' ? 'success' : d.status === 'retrying' ? 'warning' : 'danger';
html += `<tr>
<td><code style="font-size:0.75rem;">${escapeHtml(d.event_type)}</code></td>
<td style="font-size:0.8rem;">${escapeHtml(d.subscription_name || '-')}</td>
<td><span class="badge badge-${statusClass}">${escapeHtml(d.status)}</span></td>
<td>${d.http_status ? `<code>${d.http_status}</code>` : '-'}</td>
<td>${d.attempt}/3</td>
<td style="font-size:0.8rem;">${formatDateTime(d.created_at)}</td>
</tr>`;
});
html += '</tbody></table></div>';
container.innerHTML = html;
}
} catch (e) { container.innerHTML = '<div class="empty-state"><h4>Errore caricamento delivery</h4></div>'; }
}
// ── Audit Log ────────────────────────────────────────────
async function loadAuditLog() {
const container = document.getElementById('audit-log-container');
+436
View File
@@ -0,0 +1,436 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Segnalazioni - NIS2 Agile</title>
<link rel="stylesheet" href="/css/style.css">
<style>
.priority-badge { display: inline-flex; align-items: center; padding: 2px 10px; border-radius: 12px; font-size: 0.7rem; font-weight: 700; }
.priority-critical { background: var(--danger-bg); color: var(--danger); }
.priority-high { background: #fff7ed; color: #c2410c; }
.priority-medium { background: var(--warning-bg); color: #a16207; }
.priority-low { background: var(--gray-100); color: var(--gray-600); }
.status-badge { display: inline-flex; align-items: center; padding: 2px 10px; border-radius: 12px; font-size: 0.7rem; font-weight: 700; }
.status-received { background: rgba(6,182,212,0.1); color: #0891b2; }
.status-under_review { background: var(--warning-bg); color: #a16207; }
.status-investigating { background: #f3e8ff; color: #7c3aed; }
.status-resolved, .status-closed { background: var(--success-bg); color: var(--success); }
.status-rejected { background: var(--danger-bg); color: var(--danger); }
.anonymous-shield { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; background: rgba(16,185,129,0.1); border-radius: 12px; color: var(--success); font-size: 0.75rem; font-weight: 600; }
.art32-banner { background: linear-gradient(135deg, rgba(6,182,212,0.1), rgba(16,185,129,0.1)); border: 1px solid rgba(6,182,212,0.2); border-radius: var(--border-radius-lg); padding: 20px 24px; margin-bottom: 28px; }
.art32-banner h3 { font-size: 0.9375rem; font-weight: 700; color: var(--gray-800); margin-bottom: 6px; }
.art32-banner p { font-size: 0.8125rem; color: var(--gray-600); }
.track-form { background: var(--gray-50); border: 1px solid var(--gray-200); border-radius: var(--border-radius); padding: 20px; margin-bottom: 24px; }
</style>
</head>
<body>
<div class="app-layout">
<div id="sidebar-container"></div>
<main class="main-content">
<div class="page-header">
<div class="page-header-content">
<h1 class="page-title">Segnalazioni Sicurezza</h1>
<p class="page-subtitle">Canale interno Art.32 NIS2 — Segnala violazioni e anomalie di sicurezza</p>
</div>
<div class="page-header-actions">
<button class="btn btn-secondary" onclick="switchView('track')">
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd"/></svg>
Traccia Segnalazione Anonima
</button>
<button class="btn btn-primary" onclick="switchView('submit')">
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"/></svg>
Nuova Segnalazione
</button>
</div>
</div>
<!-- Art.32 Banner -->
<div class="art32-banner">
<h3>Art. 32 D.Lgs. 138/2024 — Canale Segnalazioni Interno</h3>
<p>Le entità NIS2 devono predisporre canali interni per la segnalazione di violazioni alla sicurezza informatica. Le segnalazioni anonime sono protette e il segnalante non può essere identificato. Ogni segnalazione viene gestita con priorità e tracciabilità.</p>
</div>
<!-- Stats Cards -->
<div class="stats-grid mb-24" id="stats-grid">
<div class="stat-card"><div class="spinner"></div></div>
</div>
<!-- Views -->
<div id="view-list">
<!-- Filters -->
<div class="card mb-16">
<div class="card-body" style="padding:16px 20px;">
<div style="display:flex; gap:12px; flex-wrap:wrap; align-items:center;">
<select id="filter-status" class="form-control" style="width:auto;" onchange="loadReports()">
<option value="">Tutti gli stati</option>
<option value="received">Ricevute</option>
<option value="under_review">In revisione</option>
<option value="investigating">In indagine</option>
<option value="resolved">Risolte</option>
<option value="closed">Chiuse</option>
</select>
<select id="filter-priority" class="form-control" style="width:auto;" onchange="loadReports()">
<option value="">Tutte le priorità</option>
<option value="critical">Critica</option>
<option value="high">Alta</option>
<option value="medium">Media</option>
<option value="low">Bassa</option>
</select>
<select id="filter-category" class="form-control" style="width:auto;" onchange="loadReports()">
<option value="">Tutte le categorie</option>
<option value="security_incident">Incidente sicurezza</option>
<option value="data_breach">Data breach</option>
<option value="unauthorized_access">Accesso non autorizzato</option>
<option value="policy_violation">Violazione policy</option>
<option value="supply_chain_risk">Rischio supply chain</option>
<option value="nis2_non_compliance">Non conformità NIS2</option>
<option value="other">Altro</option>
</select>
</div>
</div>
</div>
<div id="reports-container"><div class="spinner" style="margin:60px auto;"></div></div>
</div>
<!-- Submit Form -->
<div id="view-submit" style="display:none;">
<div class="card" style="max-width:720px;">
<div class="card-header">
<h3>Nuova Segnalazione</h3>
<p style="font-size:0.8125rem; color:var(--gray-500); margin-top:4px;">Segnala anonimamente o con il tuo nome. L'anonimato è garantito dalla piattaforma.</p>
</div>
<div class="card-body">
<div style="margin-bottom:20px; padding:12px 16px; background:rgba(16,185,129,0.08); border:1px solid rgba(16,185,129,0.2); border-radius:var(--border-radius); display:flex; align-items:center; gap:10px;">
<svg viewBox="0 0 20 20" fill="currentColor" width="18" height="18" style="color:var(--success); flex-shrink:0;"><path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"/></svg>
<label style="display:flex; align-items:center; gap:8px; font-size:0.875rem; cursor:pointer;">
<input type="checkbox" id="is-anonymous" checked style="accent-color:var(--success);">
<span><strong>Segnalazione anonima</strong> — La tua identità non verrà registrata</span>
</label>
</div>
<form id="submit-form" onsubmit="submitReport(event)">
<div class="form-group">
<label class="form-label">Categoria *</label>
<select id="rep-category" class="form-control" required>
<option value="">Seleziona categoria...</option>
<option value="security_incident">Incidente di sicurezza</option>
<option value="data_breach">Data breach / Violazione dati</option>
<option value="unauthorized_access">Accesso non autorizzato</option>
<option value="policy_violation">Violazione policy interna</option>
<option value="supply_chain_risk">Rischio supply chain</option>
<option value="nis2_non_compliance">Non conformità NIS2</option>
<option value="corruption">Corruzione / Frode</option>
<option value="other">Altro</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Titolo *</label>
<input type="text" id="rep-title" class="form-control" placeholder="Breve descrizione dell'anomalia..." required>
</div>
<div class="form-group">
<label class="form-label">Descrizione dettagliata *</label>
<textarea id="rep-description" class="form-control" rows="6" placeholder="Descrivi cosa hai osservato, quando è successo, chi è coinvolto (se noto), e qualsiasi altra informazione utile..." required></textarea>
</div>
<div class="form-group">
<label class="form-label">Priorità stimata</label>
<select id="rep-priority" class="form-control">
<option value="medium">Media</option>
<option value="high">Alta</option>
<option value="critical">Critica — Azione immediata richiesta</option>
<option value="low">Bassa</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Articolo NIS2 violato (opzionale)</label>
<input type="text" id="rep-article" class="form-control" placeholder="Es. Art.21, Art.23...">
</div>
<div class="form-group" id="contact-email-group">
<label class="form-label">Email per follow-up (opzionale)</label>
<input type="email" id="rep-email" class="form-control" placeholder="Email solo per ricevere aggiornamenti">
<p style="font-size:0.75rem; color:var(--gray-500); margin-top:4px;">Lascia vuoto per mantenere l'anonimato completo. Oppure fornisci un'email temporanea.</p>
</div>
<div style="display:flex; gap:12px; margin-top:24px;">
<button type="submit" class="btn btn-primary">Invia Segnalazione</button>
<button type="button" class="btn btn-secondary" onclick="switchView('list')">Annulla</button>
</div>
</form>
</div>
</div>
</div>
<!-- Track Anonymous -->
<div id="view-track" style="display:none;">
<div class="card" style="max-width:600px;">
<div class="card-header">
<h3>Traccia Segnalazione Anonima</h3>
<p style="font-size:0.8125rem; color:var(--gray-500); margin-top:4px;">Inserisci il token ricevuto al momento della segnalazione per verificarne lo stato.</p>
</div>
<div class="card-body">
<div class="form-group">
<label class="form-label">Token Segnalazione</label>
<input type="text" id="track-token" class="form-control" placeholder="Inserisci il token...">
</div>
<div style="display:flex; gap:12px;">
<button class="btn btn-primary" onclick="trackReport()">Verifica Stato</button>
<button class="btn btn-secondary" onclick="switchView('list')">Annulla</button>
</div>
<div id="track-result" style="margin-top:24px;"></div>
</div>
</div>
</div>
</main>
</div>
<!-- Report Detail Modal handled by common.js showModal -->
<script src="/js/api.js"></script>
<script src="/js/common.js"></script>
<script src="/js/i18n.js"></script>
<script src="/js/help.js"></script>
<script>
if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar();
I18n.init();
let currentView = 'list';
const categoryLabels = {
security_incident: 'Incidente sicurezza', data_breach: 'Data breach',
unauthorized_access: 'Accesso non autorizzato', policy_violation: 'Violazione policy',
supply_chain_risk: 'Rischio supply chain', nis2_non_compliance: 'Non conformità NIS2',
corruption: 'Corruzione/Frode', fraud: 'Frode', other: 'Altro'
};
const priorityLabels = { critical: 'Critica', high: 'Alta', medium: 'Media', low: 'Bassa' };
const statusLabels = {
received: 'Ricevuta', under_review: 'In revisione', investigating: 'In indagine',
resolved: 'Risolta', closed: 'Chiusa', rejected: 'Respinta'
};
function switchView(view) {
currentView = view;
['list','submit','track'].forEach(v => {
document.getElementById('view-' + v).style.display = v === view ? 'block' : 'none';
});
if (view === 'list') { loadStats(); loadReports(); }
}
async function loadStats() {
try {
const result = await api.request('GET', '/whistleblowing/stats');
if (result.success) renderStats(result.data.stats);
} catch (e) {}
}
function renderStats(s) {
const grid = document.getElementById('stats-grid');
grid.innerHTML = `
<div class="stat-card">
<div class="stat-label">Totale Segnalazioni</div>
<div class="stat-value">${s.total || 0}</div>
</div>
<div class="stat-card">
<div class="stat-label">Da Gestire</div>
<div class="stat-value" style="color:var(--warning);">${(s.received || 0) + (s.under_review || 0)}</div>
</div>
<div class="stat-card">
<div class="stat-label">In Indagine</div>
<div class="stat-value" style="color:var(--primary);">${s.investigating || 0}</div>
</div>
<div class="stat-card">
<div class="stat-label">Critiche/Alte</div>
<div class="stat-value" style="color:var(--danger);">${(s.critical || 0) + (s.high || 0)}</div>
</div>`;
}
async function loadReports() {
const container = document.getElementById('reports-container');
container.innerHTML = '<div class="spinner" style="margin:60px auto;"></div>';
try {
const params = new URLSearchParams();
const status = document.getElementById('filter-status').value;
const priority = document.getElementById('filter-priority').value;
const category = document.getElementById('filter-category').value;
if (status) params.append('status', status);
if (priority) params.append('priority', priority);
if (category) params.append('category', category);
const result = await api.request('GET', '/whistleblowing/list?' + params);
if (result.success) renderReports(result.data.reports || []);
} catch (e) {
container.innerHTML = '<div class="empty-state"><h4>Errore caricamento segnalazioni</h4></div>';
}
}
function renderReports(reports) {
const container = document.getElementById('reports-container');
if (!reports.length) {
container.innerHTML = `
<div class="empty-state">
<svg viewBox="0 0 20 20" fill="currentColor" width="40" height="40" style="color:var(--gray-300)"><path fill-rule="evenodd" d="M10 1.944A11.954 11.954 0 012.166 5C2.056 5.649 2 6.319 2 7c0 5.225 3.34 9.67 8 11.317C14.66 16.67 18 12.225 18 7c0-.682-.057-1.35-.166-2.001A11.954 11.954 0 0110 1.944zM11 14a1 1 0 11-2 0 1 1 0 012 0zm0-7a1 1 0 10-2 0v3a1 1 0 102 0V7z" clip-rule="evenodd"/></svg>
<h4>Nessuna segnalazione trovata</h4>
<p>Il canale di segnalazione è attivo. Le segnalazioni appariranno qui.</p>
</div>`;
return;
}
let html = `<div class="table-container"><table>
<thead><tr><th>Codice</th><th>Categoria</th><th>Titolo</th><th>Priorità</th><th>Stato</th><th>Anonima</th><th>Assegnata</th><th>Data</th><th></th></tr></thead><tbody>`;
reports.forEach(r => {
html += `<tr>
<td><code style="font-size:0.8rem;">${escapeHtml(r.report_code)}</code></td>
<td><span class="badge badge-neutral">${escapeHtml(categoryLabels[r.category] || r.category)}</span></td>
<td><strong style="font-size:0.875rem;">${escapeHtml(r.title)}</strong></td>
<td><span class="priority-badge priority-${r.priority}">${escapeHtml(priorityLabels[r.priority] || r.priority)}</span></td>
<td><span class="status-badge status-${r.status}">${escapeHtml(statusLabels[r.status] || r.status)}</span></td>
<td>${r.is_anonymous ? '<span class="anonymous-shield"><svg viewBox="0 0 20 20" fill="currentColor" width="12" height="12"><path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"/></svg>Anonima</span>' : '<span style="color:var(--gray-400); font-size:0.75rem;">Firmata</span>'}</td>
<td>${r.assigned_to_name ? escapeHtml(r.assigned_to_name) : '<span style="color:var(--gray-400);">—</span>'}</td>
<td style="font-size:0.8rem;">${formatDate(r.created_at)}</td>
<td><button class="btn btn-sm btn-secondary" onclick="viewReport(${r.id})">Gestisci</button></td>
</tr>`;
});
html += '</tbody></table></div>';
container.innerHTML = html;
}
async function viewReport(id) {
try {
const result = await api.request('GET', `/whistleblowing/${id}`);
if (!result.success) { showNotification('Errore caricamento.', 'error'); return; }
const r = result.data;
const timelineHtml = (r.timeline || []).map(t => `
<div style="padding:10px 0; border-bottom:1px solid var(--gray-100); display:flex; gap:12px;">
<div style="width:8px; height:8px; border-radius:50%; background:var(--primary); margin-top:6px; flex-shrink:0;"></div>
<div>
<p style="font-size:0.8125rem; color:var(--gray-700);">${escapeHtml(t.description)}</p>
<p style="font-size:0.75rem; color:var(--gray-400);">${formatDateTime(t.created_at)} ${t.created_by_name ? '— ' + escapeHtml(t.created_by_name) : ''}</p>
</div>
</div>`).join('');
showModal(`${escapeHtml(r.report_code)} — ${escapeHtml(r.title)}`, `
<div style="display:flex; gap:10px; flex-wrap:wrap; margin-bottom:16px;">
<span class="priority-badge priority-${r.priority}">${escapeHtml(priorityLabels[r.priority] || r.priority)}</span>
<span class="status-badge status-${r.status}">${escapeHtml(statusLabels[r.status] || r.status)}</span>
${r.is_anonymous ? '<span class="anonymous-shield"><svg viewBox="0 0 20 20" fill="currentColor" width="12" height="12"><path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"/></svg>Anonima</span>' : ''}
</div>
<div style="padding:12px 16px; background:var(--gray-50); border-radius:var(--border-radius); margin-bottom:16px; font-size:0.875rem; line-height:1.6;">
${escapeHtml(r.description)}
</div>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:16px; font-size:0.8125rem;">
<div><strong>Categoria:</strong> ${escapeHtml(categoryLabels[r.category] || r.category)}</div>
<div><strong>Art. NIS2:</strong> ${r.nis2_article ? escapeHtml(r.nis2_article) : '—'}</div>
<div><strong>Assegnata a:</strong> ${r.assigned_to_name ? escapeHtml(r.assigned_to_name) : '—'}</div>
<div><strong>Data:</strong> ${formatDateTime(r.created_at)}</div>
</div>
<h4 style="font-size:0.8125rem; font-weight:700; margin-bottom:8px;">Timeline</h4>
<div>${timelineHtml || '<p style="color:var(--gray-400); font-size:0.8rem;">Nessun evento in timeline.</p>'}</div>
<div style="margin-top:20px; display:flex; flex-direction:column; gap:10px;">
<select id="modal-status" class="form-control">
<option value="">Cambia stato...</option>
${['under_review','investigating','resolved','closed','rejected'].map(s =>
`<option value="${s}" ${r.status === s ? 'selected' : ''}>${escapeHtml(statusLabels[s])}</option>`
).join('')}
</select>
<textarea id="modal-notes" class="form-control" rows="3" placeholder="Note di risoluzione...">${escapeHtml(r.resolution_notes || '')}</textarea>
</div>
`, `<button class="btn btn-primary" onclick="updateReport(${id})">Salva</button>
<button class="btn btn-secondary" onclick="closeModal()">Chiudi</button>`);
} catch (e) { showNotification('Errore di connessione.', 'error'); }
}
async function updateReport(id) {
const status = document.getElementById('modal-status').value;
const notes = document.getElementById('modal-notes').value;
const body = {};
if (status) body.status = status;
if (notes) body.resolution_notes = notes;
if (!Object.keys(body).length) { closeModal(); return; }
try {
const result = await api.request('PUT', `/whistleblowing/${id}`, body);
if (result.success) {
closeModal();
showNotification('Segnalazione aggiornata.', 'success');
loadReports(); loadStats();
} else showNotification(result.message || 'Errore.', 'error');
} catch (e) { showNotification('Errore di connessione.', 'error'); }
}
async function submitReport(e) {
e.preventDefault();
const btn = e.target.querySelector('[type=submit]');
btn.disabled = true; btn.textContent = 'Invio...';
try {
const body = {
category: document.getElementById('rep-category').value,
title: document.getElementById('rep-title').value,
description: document.getElementById('rep-description').value,
priority: document.getElementById('rep-priority').value,
nis2_article: document.getElementById('rep-article').value,
is_anonymous: document.getElementById('is-anonymous').checked ? 1 : 0,
contact_email: document.getElementById('rep-email').value,
organization_id: localStorage.getItem('nis2_org_id'),
};
const result = await api.request('POST', '/whistleblowing/submit', body);
if (result.success) {
const token = result.data.anonymous_token;
showModal('Segnalazione Inviata', `
<div style="padding:16px; background:rgba(16,185,129,0.1); border-radius:var(--border-radius); margin-bottom:16px;">
<p style="font-weight:700; color:var(--success);">Segnalazione ricevuta con codice: <code>${escapeHtml(result.data.report_code)}</code></p>
</div>
${token ? `<p style="font-size:0.875rem; color:var(--gray-600); margin-bottom:8px;">Token per tracking anonimo (conservalo):</p>
<div style="padding:12px; background:var(--gray-100); border-radius:var(--border-radius); font-family:monospace; font-size:0.8rem; word-break:break-all; user-select:all;">${escapeHtml(token)}</div>` : ''}
`, `<button class="btn btn-primary" onclick="closeModal(); switchView('list');">OK</button>`);
e.target.reset();
} else {
showNotification(result.message || 'Errore.', 'error');
}
} catch (err) { showNotification('Errore di connessione.', 'error'); }
finally { btn.disabled = false; btn.textContent = 'Invia Segnalazione'; }
}
async function trackReport() {
const token = document.getElementById('track-token').value.trim();
if (!token) { showNotification('Inserisci il token.', 'error'); return; }
const container = document.getElementById('track-result');
container.innerHTML = '<div class="spinner"></div>';
try {
const result = await api.request('GET', '/whistleblowing/track-anonymous?token=' + encodeURIComponent(token));
if (result.success) {
const r = result.data;
const tl = (r.timeline || []).map(t => `
<div style="display:flex; gap:10px; padding:8px 0; border-bottom:1px solid var(--gray-100);">
<div style="width:6px; height:6px; border-radius:50%; background:var(--primary); margin-top:7px; flex-shrink:0;"></div>
<div><p style="font-size:0.8125rem;">${escapeHtml(t.description)}</p>
<p style="font-size:0.75rem; color:var(--gray-400);">${formatDateTime(t.created_at)}</p></div>
</div>`).join('');
container.innerHTML = `
<div style="padding:16px; border:1px solid var(--gray-200); border-radius:var(--border-radius);">
<div style="display:flex; gap:10px; margin-bottom:12px;">
<code style="font-size:0.875rem;">${escapeHtml(r.report_code)}</code>
<span class="status-badge status-${r.status}">${escapeHtml(statusLabels[r.status] || r.status)}</span>
</div>
<p style="font-size:0.8125rem; color:var(--gray-600); margin-bottom:12px;">Categoria: ${escapeHtml(categoryLabels[r.category] || r.category)}</p>
<div>${tl}</div>
</div>`;
} else {
container.innerHTML = '<div class="empty-state"><h4>Token non valido o segnalazione non trovata</h4></div>';
}
} catch (e) {
container.innerHTML = '<div class="empty-state"><h4>Errore di connessione</h4></div>';
}
}
// Init
loadStats();
loadReports();
</script>
</body>
</html>