nis2-agile/public/integrazioniext.html
DevEnv nis2-agile f7347ccd8c [CONTEXT+MKTG] Contesto sessione + HTML migliorati per comunicazione terze parti
- CONTEXT_LAST_SESSION.md: documento completo di tutto lo sprint B2B
- mktg-api-doc.html: Quick Start box con chiave attiva, flusso visivo 4 step, curl pronti
- integrazioniext.html tab Inviti: tabella "Chi fa cosa" per ruolo, sezione mktg-agile
  con chiave API + 3 curl pronti, Quick Start aggiornato con tabella risorse per destinatario

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 16:37:14 +01:00

856 lines
45 KiB
HTML

<!doctype html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>NIS2 Agile — Integrazioni Esterne</title>
<link rel="stylesheet" href="css/style.css">
<style>
/* ── Page-specific overrides ── */
.integ-hero {
background: linear-gradient(135deg, rgba(6,182,212,.08) 0%, rgba(99,102,241,.06) 100%);
border: 1px solid rgba(6,182,212,.2);
border-radius: 12px;
padding: 2rem;
margin-bottom: 2rem;
}
.integ-hero h1 { font-size: 1.6rem; color: var(--primary); margin-bottom: .5rem; }
.integ-hero p { color: var(--text-secondary); max-width: 680px; line-height: 1.6; }
.provider-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.25rem;
margin-bottom: 2rem;
}
.provider-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 10px;
padding: 1.25rem;
transition: border-color .2s, transform .2s;
}
.provider-card:hover { border-color: var(--primary); transform: translateY(-2px); }
.provider-card .badge {
display: inline-block;
font-size: .65rem; font-weight: 700;
padding: .2rem .5rem; border-radius: 4px;
margin-bottom: .75rem;
}
.badge-available { background: rgba(34,197,94,.15); color: #22c55e; }
.badge-planned { background: rgba(234,179,8,.15); color: #eab308; }
.provider-card h3 { font-size: .95rem; margin-bottom: .4rem; }
.provider-card p { font-size: .82rem; color: var(--text-secondary); line-height: 1.5; }
.section-title {
font-size: 1.1rem; font-weight: 700;
color: var(--text-primary);
margin: 2rem 0 1rem;
padding-bottom: .5rem;
border-bottom: 1px solid var(--border);
}
.api-table { width: 100%; border-collapse: collapse; font-size: .83rem; margin-bottom: 1.5rem; }
.api-table th {
background: rgba(255,255,255,.04);
color: var(--text-secondary);
font-weight: 600; text-align: left;
padding: .6rem .75rem;
border-bottom: 1px solid var(--border);
}
.api-table td {
padding: .55rem .75rem;
border-bottom: 1px solid rgba(255,255,255,.04);
vertical-align: top;
}
.api-table tr:hover td { background: rgba(255,255,255,.02); }
.api-table code {
background: rgba(6,182,212,.1);
color: var(--primary);
padding: .15rem .4rem;
border-radius: 3px;
font-size: .78rem;
}
.method-get {
display: inline-block;
background: rgba(34,197,94,.15); color: #22c55e;
font-size: .65rem; font-weight: 700;
padding: .15rem .4rem; border-radius: 3px;
font-family: monospace;
}
.method-post {
display: inline-block;
background: rgba(249,115,22,.15); color: #f97316;
font-size: .65rem; font-weight: 700;
padding: .15rem .4rem; border-radius: 3px;
font-family: monospace;
}
.method-delete {
display: inline-block;
background: rgba(239,68,68,.15); color: #ef4444;
font-size: .65rem; font-weight: 700;
padding: .15rem .4rem; border-radius: 3px;
font-family: monospace;
}
.code-block {
background: #0f172a;
border: 1px solid var(--border);
border-radius: 8px;
padding: 1rem 1.25rem;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: .78rem;
line-height: 1.6;
color: #cbd5e1;
overflow-x: auto;
margin: .75rem 0 1.25rem;
white-space: pre;
}
.code-block .kw { color: #818cf8; }
.code-block .str { color: #86efac; }
.code-block .key { color: #67e8f9; }
.code-block .cmt { color: #475569; }
.steps-list {
list-style: none;
counter-reset: steps;
margin-bottom: 1.5rem;
}
.steps-list li {
counter-increment: steps;
display: flex; align-items: flex-start; gap: .75rem;
padding: .65rem 0;
border-bottom: 1px solid rgba(255,255,255,.04);
font-size: .85rem;
color: var(--text-secondary);
line-height: 1.5;
}
.steps-list li::before {
content: counter(steps);
flex-shrink: 0;
width: 24px; height: 24px;
background: rgba(6,182,212,.15);
color: var(--primary);
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: .75rem; font-weight: 700;
}
.checklist { list-style: none; margin-bottom: 1.5rem; }
.checklist li {
display: flex; align-items: flex-start; gap: .6rem;
padding: .4rem 0;
font-size: .84rem;
color: var(--text-secondary);
}
.checklist li::before {
content: '□';
color: var(--primary);
font-size: 1rem;
line-height: 1;
flex-shrink: 0;
}
.callout {
border-left: 3px solid var(--primary);
background: rgba(6,182,212,.05);
padding: .75rem 1rem;
border-radius: 0 6px 6px 0;
font-size: .83rem;
color: var(--text-secondary);
margin-bottom: 1.25rem;
line-height: 1.5;
}
.callout strong { color: var(--primary); }
.event-list { display: flex; flex-direction: column; gap: .5rem; margin-bottom: 1.5rem; }
.event-row {
display: flex; align-items: center; gap: .75rem;
background: rgba(255,255,255,.03);
border: 1px solid var(--border);
border-radius: 6px;
padding: .6rem .9rem;
font-size: .82rem;
}
.event-name {
font-family: monospace;
color: #f97316;
font-size: .78rem;
min-width: 230px;
}
.event-desc { color: var(--text-secondary); }
.event-lg231 {
margin-left: auto;
font-size: .7rem;
background: rgba(99,102,241,.12);
color: #818cf8;
padding: .15rem .5rem;
border-radius: 4px;
white-space: nowrap;
}
.tab-nav { display: flex; gap: 0; border-bottom: 1px solid var(--border); margin-bottom: 1.5rem; }
.tab-btn {
padding: .55rem 1.1rem;
font-size: .82rem; font-weight: 600;
color: var(--text-secondary);
background: none; border: none;
border-bottom: 2px solid transparent;
cursor: pointer; transition: .15s;
}
.tab-btn.active { color: var(--primary); border-color: var(--primary); }
.tab-pane { display: none; }
.tab-pane.active { display: block; }
</style>
</head>
<body>
<div id="sidebar-placeholder"></div>
<main class="main-content">
<div class="page-header">
<h1>Integrazioni Esterne</h1>
<p>API, webhook e connettori per sistemi GRC di terze parti</p>
</div>
<!-- Hero -->
<div class="integ-hero">
<h1>NIS2 Agile come Provider di Compliance</h1>
<p>NIS2 Agile espone una <strong>Services API REST</strong> che permette a sistemi esterni di leggere
score di conformità, rischi, incidenti e controlli in tempo reale. Auth via API Key dedicata,
rate limiting 100 req/ora, firma HMAC-SHA256 per webhook outbound.</p>
</div>
<!-- Provider cards -->
<div class="section-title">Provider Disponibili</div>
<div class="provider-grid">
<div class="provider-card">
<span class="badge badge-available">Disponibile</span>
<h3>231 Agile (lg231)</h3>
<p>Integrazione compliance D.Lgs. 231/2001 + NIS2. Incidenti cyber → OdV,
rischi IT → registro 231, score NIS2 nel dashboard aziendale.</p>
<a href="#lg231" style="color:var(--primary);font-size:.8rem;text-decoration:none">Guida integrazione →</a>
</div>
<div class="provider-card">
<span class="badge badge-planned">Pianificato</span>
<h3>AllRisk Agile</h3>
<p>Feed rischi cyber per piattaforma ERM unificata. Risk score NIS2
mappato su categorie ISO 31000.</p>
</div>
<div class="provider-card">
<span class="badge badge-planned">Pianificato</span>
<h3>SustainAI Agile</h3>
<p>Cybersecurity come pillar ESG. Score NIS2 nel report di sostenibilità
(CSRD, GRI 418).</p>
</div>
<div class="provider-card">
<span class="badge badge-available">Disponibile</span>
<h3>SIEM / SOC Esterni</h3>
<p>Webhook su <code>incident.created</code> e <code>risk.created_high</code>
verso qualsiasi endpoint HTTPS (Splunk, Elastic, PagerDuty…).</p>
</div>
</div>
<!-- Tabs -->
<div class="tab-nav">
<button class="tab-btn active" onclick="showTab('api')">Services API</button>
<button class="tab-btn" onclick="showTab('invites')">Inviti &amp; Licenze</button>
<button class="tab-btn" onclick="showTab('lg231')">Guida lg231</button>
<button class="tab-btn" onclick="showTab('webhook')">Webhook</button>
<button class="tab-btn" onclick="showTab('quickstart')">Quick Start</button>
</div>
<!-- TAB: Services API -->
<div id="tab-api" class="tab-pane active">
<div class="section-title">Endpoint disponibili</div>
<div class="callout">
<strong>Base URL:</strong> <code>https://nis2.agile.software/api/services</code><br>
<strong>Auth:</strong> Header <code>X-API-Key: nis2_xxxx</code> — oppure <code>Authorization: Bearer nis2_xxxx</code>
</div>
<table class="api-table">
<thead>
<tr><th>Metodo</th><th>Endpoint</th><th>Scope</th><th>Descrizione</th></tr>
</thead>
<tbody>
<tr><td><span class="method-post">POST</span></td><td><code>/token</code></td><td>read:all</td><td>Token exchange: API Key → JWT 15 min. lg231 usa questo JWT per le chiamate successive.</td></tr>
<tr><td><span class="method-post">POST</span></td><td><code>/sso</code></td><td>sso:login (o read:all)</td><td>SSO federato: passa email + ruolo + responsabilità → JWT NIS2 2h. Utente creato se non esiste. Auditato.</td></tr>
<tr><td><span class="method-get">GET</span></td><td><code>/status</code></td><td></td><td>Health check piattaforma, versione, DB. Nessuna auth.</td></tr>
<tr><td><span class="method-get">GET</span></td><td><code>/compliance-summary</code></td><td>read:compliance</td><td>Score NIS2 aggregato (0-100), domain scores Art.21, rischi aperti, incidenti</td></tr>
<tr><td><span class="method-get">GET</span></td><td><code>/risks/feed</code></td><td>read:risks</td><td>Registro rischi con livello ISO 27005, status, area. Filtri: <code>?level=high,critical&status=open</code></td></tr>
<tr><td><span class="method-get">GET</span></td><td><code>/incidents/feed</code></td><td>read:incidents</td><td>Incidenti Art.23: severity, status, notifiche CSIRT, deadline 72h, overdue</td></tr>
<tr><td><span class="method-get">GET</span></td><td><code>/controls/status</code></td><td>read:compliance</td><td>Stato controlli per dominio: implemented / partial / missing</td></tr>
<tr><td><span class="method-get">GET</span></td><td><code>/assets/critical</code></td><td>read:assets</td><td>Asset critici con tipo, criticità, dipendenze mappate</td></tr>
<tr><td><span class="method-get">GET</span></td><td><code>/suppliers/risk</code></td><td>read:suppliers</td><td>Fornitori: risk_level, ultimo assessment, is_flagged</td></tr>
<tr><td><span class="method-get">GET</span></td><td><code>/policies/approved</code></td><td>read:policies</td><td>Policy approvate: titolo, area, data, versione</td></tr>
<tr><td><span class="method-get">GET</span></td><td><code>/openapi.json</code></td><td></td><td>Specifica OpenAPI 3.0 in JSON</td></tr>
</tbody>
</table>
<div class="section-title">Esempio risposta /compliance-summary</div>
<div class="code-block"><span class="cmt">// GET /api/services/compliance-summary</span>
<span class="cmt">// X-API-Key: nis2_Abc123...</span>
{
<span class="key">"success"</span>: <span class="kw">true</span>,
<span class="key">"data"</span>: {
<span class="key">"organization"</span>: { <span class="key">"id"</span>: 5, <span class="key">"name"</span>: <span class="str">"Azienda XYZ"</span>, <span class="key">"sector"</span>: <span class="str">"energia"</span>, <span class="key">"nis2_entity_type"</span>: <span class="str">"essential"</span> },
<span class="key">"compliance"</span>: {
<span class="key">"overall_score"</span>: 73,
<span class="key">"label"</span>: <span class="str">"Parziale"</span>,
<span class="key">"domain_scores"</span>: [
{ <span class="key">"domain"</span>: <span class="str">"Gestione del rischio"</span>, <span class="key">"score"</span>: 80, <span class="key">"status"</span>: <span class="str">"compliant"</span> },
{ <span class="key">"domain"</span>: <span class="str">"Sicurezza supply chain"</span>, <span class="key">"score"</span>: 45, <span class="key">"status"</span>: <span class="str">"partial"</span> }
]
},
<span class="key">"risks"</span>: { <span class="key">"total"</span>: 12, <span class="key">"open"</span>: 8, <span class="key">"high_critical"</span>: 3 },
<span class="key">"incidents"</span>: { <span class="key">"total"</span>: 2, <span class="key">"open"</span>: 1, <span class="key">"significant"</span>: 1, <span class="key">"overdue"</span>: 0 }
}
}</div>
<div class="section-title">Headers risposta</div>
<table class="api-table">
<thead><tr><th>Header</th><th>Valore</th></tr></thead>
<tbody>
<tr><td><code>X-NIS2-API-Version</code></td><td>1.0</td></tr>
<tr><td><code>X-NIS2-Org-Id</code></td><td>ID organizzazione (intero)</td></tr>
<tr><td><code>X-RateLimit-Limit</code></td><td>100 (req/ora per chiave)</td></tr>
<tr><td><code>X-RateLimit-Remaining</code></td><td>Richieste rimaste nella finestra</td></tr>
</tbody>
</table>
</div>
<!-- TAB: Inviti & Licenze -->
<div id="tab-invites" class="tab-pane">
<div class="callout">
<strong>Sistema Inviti / Licenze B2B:</strong> NIS2 Agile genera token di invito che abilitano
il provisioning automatico di organizzazioni e utenti. L'e-commerce o il partner riceve l'invito,
lo consegna al cliente finale (es. lg231), che lo usa per attivarsi automaticamente.
L'accesso si interrompe alla scadenza della licenza.
</div>
<div class="section-title">Chi fa cosa — ruoli nell'ecosistema</div>
<table class="api-table" style="margin-bottom:1.5rem">
<thead><tr><th>Sistema</th><th>Ruolo</th><th>API usata</th><th>Auth</th></tr></thead>
<tbody>
<tr>
<td><strong>NIS2 Admin</strong><br><span style="font-size:.72rem;color:var(--text-secondary)">o mktg-agile</span></td>
<td>Genera i token licenza, fissa piano/durata/utenti/prezzo</td>
<td><code>POST /api/invites/create</code></td>
<td><span class="badge-status s-pending" style="font-size:.65rem">API Key admin:licenses</span></td>
</tr>
<tr>
<td><strong>E-commerce</strong></td>
<td>Riceve token all'acquisto, lo consegna al cliente con l'ordine</td>
<td>— (semplice consegna token)</td>
<td><span class="badge-status s-pending" style="font-size:.65rem">API Key admin:licenses</span></td>
</tr>
<tr>
<td><strong>lg231 / partner</strong></td>
<td>Valida il token, fa provisioning automatico per il cliente</td>
<td><code>GET /api/invites/validate</code><br><code>POST /api/services/provision</code></td>
<td><span class="badge-status s-used" style="font-size:.65rem">invite_token nel body</span></td>
</tr>
<tr>
<td><strong>Cliente finale</strong></td>
<td>Apre invite_url e si registra in autonomia via wizard</td>
<td><code>onboarding.html?invite=inv_...</code></td>
<td><span class="badge-status" style="font-size:.65rem;background:rgba(156,163,175,.1);color:#9ca3af">nessuna auth</span></td>
</tr>
<tr>
<td><strong>NIS2 Admin</strong></td>
<td>Monitora uso licenze, revoca, rigenera</td>
<td><code>GET /api/invites/list</code><br><code>DELETE /api/invites/{id}</code></td>
<td><span class="badge-status s-pending" style="font-size:.65rem">API Key o JWT super_admin</span></td>
</tr>
</tbody>
</table>
<div class="section-title">Flusso completo</div>
<div class="code-block" style="font-family:monospace;line-height:1.8;font-size:.82rem">
<span style="color:#22c55e">1. GENERAZIONE</span> NIS2 Admin (super_admin)
POST /api/invites → { token: "inv_abc123...", expires_at, plan, max_uses }
<span class="cmt">Il token è visibile UNA SOLA VOLTA. Solo hash SHA-256 in DB.</span>
<span style="color:#06b6d4">2. DISTRIBUZIONE</span> NIS2 → E-commerce → lg231
NIS2 Admin passa inv_abc123... all'e-commerce
E-commerce lo consegna al cliente con l'ordine
lg231 lo riceve (es. in metadata ordine)
<span style="color:#a78bfa">3. VALIDAZIONE</span> lg231 prima del provisioning
GET /api/invites/validate?token=inv_abc123...
→ { valid: true, plan: "professional", expires_at, remaining_uses, plan_features }
<span style="color:#f59e0b">4. PROVISIONING</span> lg231 attiva automaticamente
POST /api/services/provision (body include "invite_token": "inv_abc123...")
→ { org_id, api_key, access_token, license_expires_at, temp_password }
NIS2 marca l'invito come usato (used_count++)
<span style="color:#ef4444">5. SCADENZA</span> Accesso automaticamente revocato
Quando expires_at raggiunto:
- API key rimane ma licenza_expires_at è nel passato
- GET /api/invites/validate → { valid: false, code: "EXPIRED" }
- Rinnovo: nuovo invito dal NIS2 Admin → nuovo provisioning
</div>
<div class="section-title">API Inviti (solo super_admin)</div>
<table class="api-table">
<thead><tr><th>Metodo</th><th>Endpoint</th><th>Auth</th><th>Descrizione</th></tr></thead>
<tbody>
<tr><td><span class="method-post">POST</span></td><td><code>/api/invites/create</code></td><td>JWT super_admin</td><td>Genera 1..50 inviti in batch</td></tr>
<tr><td><span class="method-get">GET</span></td><td><code>/api/invites/list</code></td><td>JWT super_admin</td><td>Lista con filtri status/channel, auto-scade pending</td></tr>
<tr><td><span class="method-get">GET</span></td><td><code>/api/invites/{id}</code></td><td>JWT super_admin</td><td>Dettaglio singolo + org usante</td></tr>
<tr><td><span class="method-delete">DELETE</span></td><td><code>/api/invites/{id}</code></td><td>JWT super_admin</td><td>Revoca (non cancella — solo status=revoked)</td></tr>
<tr><td><span class="method-post">POST</span></td><td><code>/api/invites/{id}/regenerate</code></td><td>JWT super_admin</td><td>Nuovo token, stessa configurazione</td></tr>
<tr><td><span class="method-get">GET</span></td><td><code>/api/invites/validate?token=inv_...</code></td><td></td><td>Anteprima pubblica: piano, scadenza, usi rimasti</td></tr>
</tbody>
</table>
<div class="section-title">Generare un invito (NIS2 Admin)</div>
<div class="code-block"><span class="method-post">POST</span> https://nis2.agile.software/api/invites/create
Authorization: Bearer {jwt_super_admin}
Content-Type: application/json
{
<span class="key">"plan"</span>: <span class="str">"professional"</span>, <span class="cmt">// essentials | professional | enterprise</span>
<span class="key">"duration_months"</span>: 12, <span class="cmt">// durata licenza dopo attivazione</span>
<span class="key">"invite_expires_days"</span>: 30, <span class="cmt">// giorni di validità dell'invito stesso</span>
<span class="key">"max_uses"</span>: 1, <span class="cmt">// 1=one-shot, N=batch (es: 10 per reseller)</span>
<span class="key">"label"</span>: <span class="str">"lg231 Partner Q1 2026"</span>,
<span class="key">"channel"</span>: <span class="str">"lg231"</span>, <span class="cmt">// lg231 | ecommerce | direct | manual</span>
<span class="key">"issued_to"</span>: <span class="str">"partner@lg231.it"</span>,
<span class="key">"notes"</span>: <span class="str">"Ordine OPP-2026-042"</span>
}
<span class="cmt">// Risposta — token visibile UNA SOLA VOLTA:</span>
{
<span class="key">"invites"</span>: [{
<span class="key">"id"</span>: 7,
<span class="key">"token"</span>: <span class="str">"inv_a1b2c3d4e5f6..."</span>, <span class="cmt">← SALVARE SUBITO, non recuperabile</span>
<span class="key">"plan"</span>: <span class="str">"professional"</span>,
<span class="key">"expires_at"</span>: <span class="str">"2026-04-06 12:00:00"</span>,
<span class="key">"invite_url"</span>: <span class="str">"https://nis2.agile.software/onboarding.html?invite=inv_..."</span>
}],
<span class="key">"warning"</span>: <span class="str">"Salva i token subito — non saranno più visibili in chiaro."</span>
}</div>
<div class="section-title">Provisioning con invito (lg231 → NIS2)</div>
<div class="code-block"><span class="cmt">// lg231 usa l'invite_token ricevuto per attivare automaticamente il cliente:</span>
<span class="method-post">POST</span> https://nis2.agile.software/api/services/provision
Content-Type: application/json
{
<span class="key">"invite_token"</span>: <span class="str">"inv_a1b2c3d4..."</span>, <span class="cmt">← sostituisce X-Provision-Secret</span>
<span class="key">"company"</span>: {
<span class="key">"ragione_sociale"</span>: <span class="str">"Acme S.r.l."</span>,
<span class="key">"partita_iva"</span>: <span class="str">"02345678901"</span>,
<span class="key">"ateco_code"</span>: <span class="str">"62.01.00"</span>,
<span class="key">"sector"</span>: <span class="str">"ict"</span>
},
<span class="key">"admin"</span>: {
<span class="key">"email"</span>: <span class="str">"ciso@acme.it"</span>,
<span class="key">"first_name"</span>: <span class="str">"Marco"</span>,
<span class="key">"last_name"</span>: <span class="str">"Rossi"</span>
},
<span class="key">"caller"</span>: {
<span class="key">"system"</span>: <span class="str">"lg231"</span>,
<span class="key">"company_id"</span>: 142,
<span class="key">"callback_url"</span>: <span class="str">"https://lg231.agile.software/api/integrations/nis2-callback"</span>
}
}
<span class="cmt">// Risposta (il piano e la durata vengono dall'invito, non dal body):</span>
{
<span class="key">"provisioned"</span>: <span class="kw">true</span>,
<span class="key">"org_id"</span>: 8,
<span class="key">"api_key"</span>: <span class="str">"nis2_abcdef..."</span>, <span class="cmt">← salvare in lg231 metadata</span>
<span class="key">"access_token"</span>: <span class="str">"eyJ..."</span>, <span class="cmt">← JWT 2h per apertura immediata</span>
<span class="key">"temp_password"</span>: <span class="str">"NIS2_xxxxxx"</span>, <span class="cmt">← cambio obbligatorio al primo login</span>
<span class="key">"invite_plan"</span>: <span class="str">"professional"</span>,
<span class="key">"license_expires_at"</span>: <span class="str">"2027-03-07 12:00:00"</span>,
<span class="key">"dashboard_url"</span>: <span class="str">"https://nis2.agile.software/dashboard.html"</span>
}</div>
<div class="section-title">Validazione preventiva (opzionale)</div>
<div class="code-block"><span class="cmt"># lg231 può validare l'invito prima del provisioning:</span>
curl "https://nis2.agile.software/api/invites/validate?token=inv_a1b2c3..."
<span class="cmt">// Se valido:</span>
{ <span class="key">"valid"</span>: <span class="kw">true</span>, <span class="key">"plan"</span>: <span class="str">"professional"</span>, <span class="key">"duration_months"</span>: 12,
<span class="key">"expires_at"</span>: <span class="str">"2026-04-06T12:00:00"</span>, <span class="key">"remaining_uses"</span>: 1,
<span class="key">"plan_features"</span>: [<span class="str">"Policy Management + AI"</span>, <span class="str">"Supply Chain Assessment"</span>, ...] }
<span class="cmt">// Se scaduto:</span>
{ <span class="key">"valid"</span>: <span class="kw">false</span>, <span class="key">"error"</span>: <span class="str">"Invito scaduto il 2026-03-01 00:00:00"</span>, <span class="key">"code"</span>: <span class="str">"EXPIRED"</span> }
<span class="cmt">// Se già usato:</span>
{ <span class="key">"valid"</span>: <span class="kw">false</span>, <span class="key">"error"</span>: <span class="str">"Invito già utilizzato"</span>, <span class="key">"code"</span>: <span class="str">"ALREADY_USED"</span> }</div>
<div class="section-title">Gestione ciclo di vita</div>
<table class="api-table">
<thead><tr><th>Stato invito</th><th>Significato</th><th>Azione</th></tr></thead>
<tbody>
<tr><td><code>pending</code></td><td>Valido, non ancora usato</td><td>Usabile per provision</td></tr>
<tr><td><code>used</code></td><td>Raggiunto max_uses</td><td>Provisioning bloccato</td></tr>
<tr><td><code>expired</code></td><td>expires_at superato</td><td>Provisioning bloccato, rinnovare</td></tr>
<tr><td><code>revoked</code></td><td>Revocato da admin</td><td>Provisioning bloccato permanente</td></tr>
</tbody>
</table>
<div class="section-title">Per mktg-agile / E-commerce — Quick Start</div>
<div class="callout">
Chiave API già attiva per mktg-agile: <code style="color:#4ade80">nis2_mktg_8c8bd38e78fccb9faa749d8601051f42</code>
· Scope: <code>admin:licenses</code> · Scade: 2028-03-07
· Doc completa: <a href="/mktg-api-doc.html" style="color:var(--primary)">mktg-api-doc.html</a>
· Pannello web: <a href="/licenseExt.html" style="color:var(--primary)">licenseExt.html</a>
</div>
<div class="code-block"><span class="cmt"># 1. Crea licenza (mktg-agile → NIS2)</span>
curl -X POST https://nis2.agile.software/api/invites/create \
-H <span class="str">"X-API-Key: nis2_mktg_8c8bd38e78fccb9faa749d8601051f42"</span> \
-H <span class="str">"Content-Type: application/json"</span> \
-d <span class="str">'{"plan":"professional","duration_months":12,"max_users_per_org":10,
"label":"Ordine MKT-001","channel":"ecommerce","price_eur":990}'</span>
<span class="cmt"># → risposta contiene token (inv_xxx) e invite_url</span>
<span class="cmt"># → consegna invite_url al cliente nel messaggio di conferma ordine</span>
<span class="cmt"># 2. Verifica stato licenza in qualsiasi momento</span>
curl -H <span class="str">"X-API-Key: nis2_mktg_8c8bd38e78fccb9faa749d8601051f42"</span> \
"https://nis2.agile.software/api/invites/42"
<span class="cmt"># 3. Lista licenze attive per canale ecommerce</span>
curl -H <span class="str">"X-API-Key: nis2_mktg_8c8bd38e78fccb9faa749d8601051f42"</span> \
"https://nis2.agile.software/api/invites/list?channel=ecommerce&amp;status=pending"</div>
<div class="callout">
<strong>Sicurezza token:</strong> il token <code>inv_</code> viene restituito in chiaro UNA SOLA VOLTA
alla creazione. Nel DB è conservato solo il hash SHA-256. Il <code>token_prefix</code> (es: <code>inv_a1b2c3...</code>)
è visibile nell'admin panel per identificazione visiva.
</div>
</div>
<!-- TAB: Guida lg231 -->
<div id="tab-lg231" class="tab-pane">
<div class="callout">
<strong>Per l'agente Claude di lg231:</strong> questo tab descrive esattamente cosa implementare in lg231 Agile per consumare NIS2 come provider di compliance cybersecurity.
</div>
<div class="section-title">Perché integrare 231 con NIS2</div>
<table class="api-table" style="margin-bottom:1.5rem">
<thead><tr><th>Evento NIS2</th><th>Rilevanza D.Lgs. 231/2001</th></tr></thead>
<tbody>
<tr><td>Incidente cybersecurity (Art.23 NIS2)</td><td>Reato presupposto art.24-bis — criminalità informatica</td></tr>
<tr><td>Fornitore IT flaggato alto rischio</td><td>Rischio complicità in reati informatici via supply chain</td></tr>
<tr><td>Policy sicurezza non approvata</td><td>Gap nel MOG — Modello Organizzativo e di Gestione</td></tr>
<tr><td>Score NIS2 &lt; 40%</td><td>Indicatore OdV: carenza controlli, escalation consigliata</td></tr>
<tr><td>Whistleblowing sicurezza</td><td>Potenziale escalation verso OdV 231</td></tr>
</tbody>
</table>
<div class="section-title">1. Aggiungere NIS2 al provider-config (company-ms)</div>
<p style="font-size:.83rem;color:var(--text-secondary);margin-bottom:.75rem">
File: <code>services/company-ms/public/index.php</code> — route <code>PATCH /companies/{id}/provider-config</code>
</p>
<div class="code-block"><span class="cmt">// Aggiungere ai $allowed:</span>
$allowed = [
<span class="str">'certisource_pat'</span>,
<span class="str">'anthropic_api_key'</span>,
<span class="str">'openai_api_key'</span>,
<span class="cmt">// ── Nuovo provider NIS2 ──</span>
<span class="str">'nis2_api_key'</span>, <span class="cmt">// chiave da NIS2 → Settings → API Keys</span>
<span class="str">'nis2_org_id'</span>, <span class="cmt">// ID org NIS2 (dal URL dashboard o /api/services/status)</span>
<span class="str">'nis2_enabled'</span>, <span class="cmt">// bool: abilitare pull automatico</span>
];</div>
<div class="section-title">2. NIS2Client (shared lib o inline)</div>
<div class="code-block"><span class="kw">class</span> <span class="key">Nis2Client</span>
{
<span class="kw">const</span> BASE_URL = <span class="str">'https://nis2.agile.software/api/services'</span>;
<span class="kw">public static function</span> <span class="key">get</span>(<span class="key">string</span> $endpoint, <span class="key">string</span> $apiKey, <span class="key">array</span> $query = []): array
{
$url = self::BASE_URL . $endpoint;
<span class="kw">if</span> ($query) $url .= <span class="str">'?'</span> . http_build_query($query);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => <span class="kw">true</span>,
CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => [
<span class="str">'X-API-Key: '</span> . $apiKey,
<span class="str">'Accept: application/json'</span>,
<span class="str">'X-Caller: lg231-agile/1.0'</span>,
],
]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
<span class="kw">return</span> $code === 200 && $body
? [<span class="str">'success'</span> => <span class="kw">true</span>, <span class="str">'data'</span> => json_decode($body, <span class="kw">true</span>)]
: [<span class="str">'success'</span> => <span class="kw">false</span>, <span class="str">'http_code'</span> => $code];
}
}</div>
<div class="section-title">3. Widget score NIS2 nella company view</div>
<div class="code-block"><span class="cmt">// In company detail view PHP/JS:</span>
$nis2Key = $meta[<span class="str">'nis2_api_key'</span>] ?? <span class="kw">null</span>;
<span class="kw">if</span> ($nis2Key) {
$summary = Nis2Client::get(<span class="str">'/compliance-summary'</span>, $nis2Key);
<span class="cmt">// Mostrare gauge score + badge essential/important + rischi high</span>
<span class="cmt">// Link: https://nis2.agile.software/dashboard.html</span>
}</div>
<div class="section-title">4. Escalation incidenti → OdV (monitoring-ms)</div>
<div class="code-block"><span class="cmt">// Job periodico (cron o hook) nel monitoring-ms:</span>
$incidents = Nis2Client::get(<span class="str">'/incidents/feed'</span>, $apiKey,
[<span class="str">'significant'</span> => 1, <span class="str">'status'</span> => <span class="str">'open'</span>]);
<span class="kw">foreach</span> (($incidents[<span class="str">'data'</span>][<span class="str">'incidents'</span>] ?? []) <span class="kw">as</span> $inc) {
<span class="cmt">// Crea odv_activity:</span>
<span class="cmt">// type: 'cyber_incident_notifica'</span>
<span class="cmt">// description: 'Incidente NIS2 significativo: ' . $inc['title']</span>
<span class="cmt">// priority: $inc['overdue'] ? 'urgent' : 'high'</span>
<span class="cmt">// source: 'nis2_agile'</span>
<span class="cmt">// reference_id: $inc['id']</span>
}</div>
<div class="section-title">3b. SSO federato — apertura diretta NIS2 da lg231</div>
<div class="callout">
Quando un utente lg231 clicca "Apri NIS2" dal suo dashboard, lg231 chiama
<code>POST /api/services/sso</code> e redirige l'utente su NIS2 già autenticato,
portando con sé ruolo e responsabilità. Ogni accesso SSO è tracciato nell'audit trail.
</div>
<div class="code-block"><span class="cmt">// In lg231, quando l'utente clicca "Apri NIS2 Agile":</span>
$ssoResp = Nis2Client::post(<span class="str">'/sso'</span>, $apiKey, [
<span class="str">'user_email'</span> => $currentUser[<span class="str">'email'</span>],
<span class="str">'user_name'</span> => $currentUser[<span class="str">'first_name'</span>] . <span class="str">' '</span> . $currentUser[<span class="str">'last_name'</span>],
<span class="str">'user_role'</span> => <span class="str">'compliance_manager'</span>, <span class="cmt">// mappa da ruolo lg231</span>
<span class="str">'caller_system'</span> => <span class="str">'lg231'</span>,
<span class="str">'caller_user_id'</span> => $currentUser[<span class="str">'id'</span>],
<span class="str">'responsibilities'</span> => [
[<span class="str">'area'</span> => <span class="str">'MOG 231'</span>, <span class="str">'scope'</span> => <span class="str">'art.24-bis criminalità informatica'</span>],
[<span class="str">'area'</span> => <span class="str">'OdV'</span>, <span class="str">'scope'</span> => <span class="str">'monitoraggio cyber risk'</span>],
],
]);
<span class="kw">if</span> ($ssoResp[<span class="str">'success'</span>]) {
$jwt = $ssoResp[<span class="str">'data'</span>][<span class="str">'data'</span>][<span class="str">'token'</span>];
$redirectUrl = $ssoResp[<span class="str">'data'</span>][<span class="str">'data'</span>][<span class="str">'redirect_url'</span>];
<span class="cmt">// Redirect con token nel fragment (sicuro, non nel server log)</span>
header(<span class="str">'Location: '</span> . $redirectUrl . <span class="str">'#sso_token='</span> . urlencode($jwt));
}</div>
<div class="callout">
<strong>NIS2 lato frontend</strong>: in <code>dashboard.html</code> aggiungere:<br>
<code>const ssoToken = location.hash.match(/#sso_token=([^&]+)/)?.[1];</code><br>
<code>if (ssoToken) { localStorage.setItem('nis2_token', ssoToken); location.hash = ''; }</code>
</div>
<div class="section-title">5. Provisioning automatico dal portale lg231</div>
<div class="callout">
Quando un cliente lg231 acquista una licenza NIS2 Agile, lg231 riceve un <strong>invite_token</strong>
dall'e-commerce. Con questo token lg231 chiama <code>POST /api/services/provision</code> passando
i dati aziendali del cliente. NIS2 crea automaticamente org, admin e API key. Il piano e la
durata sono definiti dall'invito (non modificabili dal chiamante).
</div>
<div class="code-block"><span class="cmt">// Salva in lg231 companies.metadata dopo provisioning:</span>
$meta[<span class="str">'nis2_api_key'</span>] = $response[<span class="str">'api_key'</span>];
$meta[<span class="str">'nis2_org_id'</span>] = $response[<span class="str">'org_id'</span>];
$meta[<span class="str">'nis2_invite_id'</span>] = $response[<span class="str">'invite_id'</span>];
$meta[<span class="str">'nis2_license_plan'</span>] = $response[<span class="str">'invite_plan'</span>];
$meta[<span class="str">'nis2_expires_at'</span>] = $response[<span class="str">'license_expires_at'</span>];
$meta[<span class="str">'nis2_enabled'</span>] = <span class="kw">true</span>;
<span class="cmt">// Controlla scadenza prima di ogni chiamata:</span>
<span class="kw">if</span> (strtotime($meta[<span class="str">'nis2_expires_at'</span>] ?? <span class="str">'1970-01-01'</span>) &lt; time()) {
<span class="cmt">// licenza scaduta → mostrare banner rinnovo, non chiamare NIS2</span>
<span class="kw">return</span> [<span class="str">'success'</span> => <span class="kw">false</span>, <span class="str">'reason'</span> => <span class="str">'license_expired'</span>];
}</div>
<div class="section-title">Checklist implementazione lg231</div>
<ul class="checklist">
<li>company-ms: aggiungere <code>nis2_api_key</code>, <code>nis2_org_id</code>, <code>nis2_enabled</code>, <code>nis2_invite_id</code>, <code>nis2_license_plan</code>, <code>nis2_expires_at</code> a provider-config</li>
<li>shared: creare <code>Nis2Client</code> (curl wrapper leggero) con check scadenza licenza</li>
<li>company-ms: endpoint provisioning — ricevi invite_token, chiama <code>POST /api/services/provision</code>, salva risposta in metadata</li>
<li>UI: widget NIS2 score nella company detail (gauge + badge + rischi high)</li>
<li>risk-ms: import rischi cyber durante assessment (categoria art.24-bis)</li>
<li>monitoring-ms: escalation incidenti significativi → odv_activity</li>
<li>monitoring-ms: policy NIS2 non approvate → alert OdV</li>
<li>(opz.) endpoint <code>POST /api/integrations/nis2-webhook</code> + subscription</li>
</ul>
<div class="callout">
<strong>Nota CORS:</strong> tutte le chiamate a NIS2 devono avvenire <strong>server-to-server</strong>
(PHP cURL), non da browser. NIS2 non accetta origini lg231 in CORS per sicurezza.
</div>
</div>
<!-- TAB: Webhook -->
<div id="tab-webhook" class="tab-pane">
<div class="callout">
NIS2 invia notifiche push su eventi tramite webhook HTTPS firmati HMAC-SHA256.
Il sistema esterno riceve la notifica in tempo reale senza polling.
</div>
<div class="section-title">Eventi disponibili</div>
<div class="event-list">
<div class="event-row">
<span class="event-name">incident.created</span>
<span class="event-desc">Nuovo incidente di sicurezza registrato</span>
<span class="event-lg231">→ odv_activity</span>
</div>
<div class="event-row">
<span class="event-name">incident.deadline_warning</span>
<span class="event-desc">Scadenza Art.23 (24h/72h) entro 4 ore</span>
<span class="event-lg231">→ alert urgente OdV</span>
</div>
<div class="event-row">
<span class="event-name">risk.created_high</span>
<span class="event-desc">Nuovo rischio HIGH o CRITICAL creato</span>
<span class="event-lg231">→ risk register 231</span>
</div>
<div class="event-row">
<span class="event-name">compliance.score_changed</span>
<span class="event-desc">Variazione score NIS2 &gt; 5%</span>
<span class="event-lg231">→ aggiorna widget</span>
</div>
<div class="event-row">
<span class="event-name">policy.approved</span>
<span class="event-desc">Nuova policy di sicurezza approvata</span>
<span class="event-lg231">→ aggiorna MOG ref</span>
</div>
<div class="event-row">
<span class="event-name">supplier.risk_flagged</span>
<span class="event-desc">Fornitore IT flaggato alto rischio</span>
<span class="event-lg231">→ supply chain 231</span>
</div>
</div>
<div class="section-title">Registrare una subscription</div>
<div class="code-block"><span class="method-post">POST</span> https://nis2.agile.software/api/webhooks/subscriptions
Authorization: Bearer {jwt_token}
Content-Type: application/json
{
<span class="key">"url"</span>: <span class="str">"https://lg231.agile.software/api/integrations/nis2-webhook"</span>,
<span class="key">"events"</span>: [<span class="str">"incident.created"</span>, <span class="str">"incident.deadline_warning"</span>, <span class="str">"risk.created_high"</span>],
<span class="key">"description"</span>: <span class="str">"lg231 Agile integration"</span>
}
<span class="cmt">// Risposta:</span>
{ <span class="key">"id"</span>: 1, <span class="key">"secret"</span>: <span class="str">"whsec_abc123..."</span> }
<span class="cmt">// Salvare il secret in companies.metadata.nis2_webhook_secret</span></div>
<div class="section-title">Verifica firma HMAC (endpoint ricevente)</div>
<div class="code-block">$secret = $meta[<span class="str">'nis2_webhook_secret'</span>];
$body = file_get_contents(<span class="str">'php://input'</span>);
$expected = <span class="str">'sha256='</span> . hash_hmac(<span class="str">'sha256'</span>, $body, $secret);
$received = $_SERVER[<span class="str">'HTTP_X_NIS2_SIGNATURE'</span>] ?? <span class="str">''</span>;
<span class="kw">if</span> (!hash_equals($expected, $received)) {
http_response_code(401);
<span class="kw">exit</span>;
}
$event = $_SERVER[<span class="str">'HTTP_X_NIS2_EVENT'</span>] ?? <span class="str">''</span>;
$payload = json_decode($body, <span class="kw">true</span>);
<span class="kw">switch</span> ($event) {
<span class="kw">case</span> <span class="str">'incident.created'</span>:
<span class="cmt">// crea odv_activity...</span>
<span class="kw">break</span>;
<span class="kw">case</span> <span class="str">'risk.created_high'</span>:
<span class="cmt">// aggiungi a risk register...</span>
<span class="kw">break</span>;
}</div>
</div>
<!-- TAB: Quick Start -->
<div id="tab-quickstart" class="tab-pane">
<div class="section-title">Creare la prima API Key</div>
<ol class="steps-list">
<li>Login su <strong>NIS2 Agile</strong> con ruolo <code>org_admin</code> o superiore</li>
<li>Vai su <strong>Settings → API Keys</strong> (sezione Integrazioni)</li>
<li>Click <strong>"Nuova API Key"</strong> → nome descrittivo (es: "lg231 Integration") → scope: <code>read:all</code></li>
<li>Copia la chiave — formato <code>nis2_XXXX...</code> — visibile una sola volta</li>
<li>Nota l'<strong>Organization ID</strong> dal URL del dashboard (es: <code>/dashboard.html</code> dopo login)</li>
<li>In lg231: salva via <code>PATCH /companies/{id}/provider-config</code> con <code>nis2_api_key</code> e <code>nis2_org_id</code></li>
</ol>
<div class="section-title">Test rapido (curl)</div>
<div class="code-block"><span class="cmt"># 1. Verifica senza auth</span>
curl https://nis2.agile.software/api/services/status
<span class="cmt"># 2. Compliance summary</span>
curl -H <span class="str">"X-API-Key: nis2_TUA_CHIAVE"</span> \
https://nis2.agile.software/api/services/compliance-summary | python3 -m json.tool
<span class="cmt"># 3. Incidenti significativi aperti</span>
curl -H <span class="str">"X-API-Key: nis2_TUA_CHIAVE"</span> \
"https://nis2.agile.software/api/services/incidents/feed?significant=1&status=open"
<span class="cmt"># 4. Rischi high/critical</span>
curl -H <span class="str">"X-API-Key: nis2_TUA_CHIAVE"</span> \
"https://nis2.agile.software/api/services/risks/feed?level=high,critical"</div>
<div class="section-title">Risorse per ogni tipo di integratore</div>
<table class="api-table">
<thead><tr><th>Destinatario</th><th>Risorsa</th><th>URL</th></tr></thead>
<tbody>
<tr>
<td><strong>mktg-agile / E-commerce</strong></td>
<td>Documentazione API licenze + Quick Start</td>
<td><a href="/mktg-api-doc.html" style="color:var(--primary)">mktg-api-doc.html</a></td>
</tr>
<tr>
<td><strong>mktg-agile / E-commerce</strong></td>
<td>Pannello web gestione licenze</td>
<td><a href="/licenseExt.html" style="color:var(--primary)">licenseExt.html</a></td>
</tr>
<tr>
<td><strong>mktg-agile / E-commerce</strong></td>
<td>Postman Collection (import diretto)</td>
<td><a href="/nis2-license-api.postman.json" download style="color:var(--primary)">nis2-license-api.postman.json</a></td>
</tr>
<tr>
<td><strong>lg231 / Partner tecnici</strong></td>
<td>Questa pagina — tab Guida lg231</td>
<td><a href="/integrazioniext.html" style="color:var(--primary)">integrazioniext.html</a></td>
</tr>
<tr>
<td><strong>lg231 (agente Claude)</strong></td>
<td>Spec tecnica completa per implementazione</td>
<td><code>docs/integration/lg231-nis2-integration.md</code></td>
</tr>
<tr>
<td><strong>Tutti</strong></td>
<td>Health check (no auth)</td>
<td><code>GET https://nis2.agile.software/api/services/status</code></td>
</tr>
<tr>
<td><strong>Tutti</strong></td>
<td>App NIS2 Agile</td>
<td><a href="https://nis2.agile.software" style="color:var(--primary)">https://nis2.agile.software</a></td>
</tr>
</tbody>
</table>
</div>
</main>
<script src="js/common.js"></script>
<script src="js/i18n.js"></script>
<script>
function showTab(id) {
document.querySelectorAll('.tab-btn').forEach((b, i) => {
const ids = ['api', 'invites', 'lg231', 'webhook', 'quickstart'];
b.classList.toggle('active', ids[i] === id);
});
document.querySelectorAll('.tab-pane').forEach(p => {
p.classList.toggle('active', p.id === 'tab-' + id);
});
// scroll to tabs
document.querySelector('.tab-nav').scrollIntoView({ behavior: 'smooth', block: 'start' });
}
</script>
</body>
</html>