nis2-agile/public/docs/api.html
DevEnv nis2-agile 86e9bdded2 [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>
2026-03-07 13:20:24 +01:00

711 lines
48 KiB
HTML

<!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></td><td>Immediato</td><td>Fire-and-forget sincrono</td></tr>
<tr><td></td><td>+5 minuti</td><td>Processato da cron</td></tr>
<tr><td></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>