Compare commits

...

8 Commits

Author SHA1 Message Date
DevEnv nis2-agile
94d7867cea [FIX] Qdrant URL hostname drift-proof (RAG produzione) + recreate app
L'IP hardcoded Qdrant 172.21.0.5 era driftato a .3 (container senza IP statico) e
con php-fpm clear_env=no la env QDRANT_URL=172.21.0.5 (morta) veniva usata -> RAG web rotta.
Fix: QDRANT_URL e fallback VectorService usano l'hostname http://nis2-qdrant:6333,
risolto via Docker DNS sia in CLI sia in php-fpm. Verificato retrieval end-to-end (287 chunk).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 17:22:54 +02:00
DevEnv nis2-agile
0330bcf29d [DOCS] CONTEXT_LAST_SESSION: sessione 2026-05-29 pomeriggio (reboot, 5 commit, utenti/org Agile)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 17:19:05 +02:00
DevEnv nis2-agile
5c545ea3d0 [FEAT] Integrazione analisi docs/nis2 v1.7.0 — scoring asset, tassonomia incidenti, PIR, NIST CSF, fonti certe
Fase 1 - Asset Relevance Scoring NIS2 (GV.OC-04): metodologia 0-100 a 6 criteri,
  AssetScoringService + endpoint scoringGrid/score/relevantSystems + UI assets.html + registro stampabile.
Fase 2 - Tassonomia incidenti Determina ACN 164179/2025: IS-1..4 + regime essenziale/importante (Allegati 3/4).
Fase 3 - Post-Incident Review (5-Whys) + metriche TTD/TTC/TTR + timestamp di fase.
Fase 4 - Mapping NIST CSF 2.0 (43 controlli) reference-only.
Fonti certe: registry config/nis2_sources.php + grounding AI (vieta riferimenti inventati) +
  citazioni help.js + ingest PDF normativi nella KB RAG (scripts/ingest-nis2-sources.php).
Migrazioni 020/021/022 (additive idempotenti). Fix VectorService IP Qdrant (drift .5->.3).
Analisi concorrenza Evix (docs/EVIX_ANALISI_CONCORRENZA.html, gap-driven).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 17:15:13 +02:00
DevEnv nis2-agile
a7a21faa82 [FEAT] Knowledge Base RAG multi-livello (SYSTEM/FIRM/ORG) + Qdrant + Voyage
- KnowledgeBaseController: ingest, list, firmOrgs, search, delete
- VectorService (Qdrant + buildAuthzFilter), EmbedService (Voyage), RagService (pipeline)
- AIService::askWithRag con fallback graceful
- docker-compose: servizio qdrant + env Voyage (chiave da .env/vault, no hardcoded)
- SQL 012 consulting_firms, 013 firm_assignments + kb_uploaded_documents
- public/kb.html + kb.js (upload, lista, search preview)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 15:44:13 +02:00
DevEnv nis2-agile
9b53ca3ba1 [FEAT] MktgLead getJsonBody + script import-feedback-to-nexus + seed demo agile-tech
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 15:42:05 +02:00
DevEnv nis2-agile
1d934e4e63 [FEAT] UI: guida online, landing EN, mobile-conversion, ai-assistant, bug-reporter + help/i18n
- public/guida.html, index-en.html, service-continuity.html
- public/js/ai-assistant.js, bug-reporter.js (FAB supporto)
- public/mobile-conversion.css/js
- index.html, common.js, help.js, risks.html: aggiornamenti UI/help

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 15:42:00 +02:00
DevEnv nis2-agile
c0bf7b6c15 [DOCS] Standard cross-suite AgileHub + governance CLAUDE.md + registri agent
- CLAUDE.md: TZ, SSO, vault-steward, versioning, persona v2.0, multitenant, KB RAG
- docs/standards: persona-conversational-rules v2.0
- docs/STANDARD_*: installer-integration, email-relay, AI-prodotto, marketing-tenant, multitenant
- AGENT_CHANGES.md + OPEN_TICKETS.md (registri agent automatico)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 15:41:54 +02:00
DevEnv nis2-agile
1d13166d7a [CHORE] .gitignore: escludi backup (*.bak*, .backups/) e chiavi SSH effimere (.ssh-temp/)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 15:41:44 +02:00
72 changed files with 30965 additions and 46 deletions

8
.gitignore vendored
View File

@ -35,5 +35,9 @@ docker/data/
.claude/plans/
.claude/memory/
# Backup files
docker/*.bak*
# Backup files (qualsiasi posizione)
*.bak*
.backups/
# Chiavi SSH effimere — NON committare MAI chiavi private
.ssh-temp/

35
AGENT_CHANGES.md Normal file
View File

@ -0,0 +1,35 @@
# Agent Changes Log — nis2-agile
> Modifiche applicate automaticamente dall'agent AI AgileHub (ticket-agent-cron).
> Leggere ad ogni sessione per sapere cosa e cambiato.
---
## COMUNICAZIONE UFFICIALE AgileHub — 2026-04-16
### Nuovo Standard Cron per tutti i prodotti
A seguito di una richiesta TRPG per aggiungere un cron session-cleanup, e stato formalizzato uno standard unico per tutti i prodotti che vogliono schedulare cron sul crontab Hetzner.
**Standard completo (sempre aggiornato):**
- GET http://172.18.0.1:4214/standards/standard_cron
- GET http://172.18.0.1:4214/standards/cron_registry (22 cron attivi)
**Regole obbligatorie:**
1. Script in /var/www/<prodotto>/scripts/<nome>.sh
2. Log in /var/log/<prodotto>-<nome>.log
3. export TZ=Europe/Rome in testa allo script
4. Idempotente (rilanciabile senza danni)
5. Isolamento: tocca solo risorse del proprio prodotto
6. Aggiornare docs/CRON_REGISTRY.md (in agile-services)
7. Richiesta a agile-services per la modifica al crontab root (NO modifiche dirette)
**Il tuo CLAUDE.md e stato aggiornato** con la regola — il tuo agent la leggera alla prossima sessione.
**Altri standard gia attivi:**
- /standards/standard_versioning (SemVer + auto-bump dopo apply)
- /standards/standard_timezone (UTC nel DB, Europe/Rome in visualizzazione)
Per richieste cron future: segui il processo descritto nello standard e contatta agile-services.
---

550
CLAUDE.md
View File

@ -1,5 +1,110 @@
<!-- STANDARD:timezone-conventions:v1.0:start -->
## ⏰ ORARI E TIMEZONE — REGOLE OPERATIVE
> **TL;DR**: l autorità del progetto è **`Europe/Rome`** (CEST estate UTC+2, CET inverno UTC+1).
> Quando scrivi un timestamp, **indica SEMPRE il suffisso TZ** (`CEST`/`CET`/`UTC`) oppure usa ISO8601 con offset (`2026-05-09T16:19:00+02:00`). **Mai** timestamp ambigui.
| Sistema | TZ | Output esempio |
|---|---|---|
| Host Hetzner | `Europe/Rome` | `Sat May 09 16:19 CEST 2026` |
| Container DevEnv (alcuni in drift UTC, vedi standard full) | misto | verifica con `docker exec <c> date` |
| Container produzione | `Europe/Rome` | CEST |
| MySQL `time_zone` | `SYSTEM` | `NOW()` CEST, `UTC_TIMESTAMP()` UTC |
| Apache log `%t` | `Europe/Rome` (locale) | `[09/May/2026:13:04:52 +0200]` |
| Node.js MS | UTC interno | `Date().toISOString()``Z` |
| Crontab Hetzner | `Europe/Rome` | `0 3 * * *` = 03:00 italiane |
**Regole**:
1. **Audit/sequenze cross-MS** → UTC obbligatorio (`2026-05-09T14:19:00Z`)
2. **Doc operativi/UI** → CEST/CET con suffisso esplicito
3. **DB store** → UTC, display → locale
4. **Cron critici**`CRON_TZ=UTC` o fuori finestra DST 02:00-03:00 locale
**DST Italia**: ultima domenica marzo (CET→CEST, ora 02:00 saltata) + ultima domenica ottobre (CEST→CET, ora 02:00-03:00 duplicata).
**Spec completa**: `STANDARD_TIMEZONE_CONVENTIONS.md` (slug `timezone-conventions` v1.0, owner VIGILE).
<!-- STANDARD:timezone-conventions:v1.0:end -->
# NIS2 Agile - Documentazione Progetto
## REGOLE DI GOVERNANCE (LEGGERE ATTENTAMENTE, aggiornate 2026-04-22)
> **Queste regole sono OBBLIGATORIE e non negoziabili.**
### REGOLA FONDAMENTALE: Gitea = SOLO Backup
> **Gitea e un BACKUP one-way (sorgente -> Gitea), NON la fonte di verita.**
> **Il webhook auto-pull e DISABILITATO su tutti i 13 repo dal 2026-04-22.**
>
> - Le modifiche che fai nel container sono GIA live su `/var/www/nis2-agile/` via bind mount
> - `/var/www/nis2-agile/` e la FONTE DI VERITA
> - NON proporre MAI "git pull da Gitea" per applicare modifiche
> - Per tirare giu qualcosa da Gitea serve richiesta esplicita dell utente
> - git push -> Gitea = OK (backup)
> - git pull da Gitea -> `/var/www/nis2-agile/` = NO (puo sovrascrivere modifiche vere)
### ARCHITETTURA: PHP-FPM in Docker con BIND MOUNT (LIVE)
> **Verificato 2026-04-22**: NIS2 gira con:
> - `nis2-app` (php-fpm) con bind mount **RW** su `/var/www/nis2-agile/application` e `/public`
> - `nis2-web` (nginx) con bind mount **RO** su `/public`
> - `nis2-db` (MySQL) per persistenza
> - `nis2-qdrant` (vector DB)
> - Apache esterno ha `DocumentRoot /var/www/nis2-agile/public`
**Cosa va LIVE ISTANTANEAMENTE:**
- File `.php` in `/var/www/nis2-agile/application/` -- PHP-FPM rilegge ad ogni request
- File in `/var/www/nis2-agile/public/` -- serviti da nginx (via bind mount :ro)
- File `.html/.css/.js` nel public -- live via nginx/Apache
**Cosa richiede azione (CHIEDI SEMPRE CONFERMA):**
- Modifiche a `docker/nginx.conf` -> `docker restart nis2-web`
- Modifiche al Dockerfile -> `docker compose build + up -d`
- Schema DB (`nis2-db`) -> SQL manuale
- Worker php cron/feedback -> attendono il prossimo run o restart
**Nota**: non serve MAI fare rebuild di nis2-app per cambi di codice PHP -- il bind mount :rw garantisce che php-fpm legga sempre la versione aggiornata.
### Cosa PUOI fare autonomamente:
- Leggere codice sorgente e documentazione
- Eseguire query SELECT sul database
- Analizzare log (Apache, Docker, PM2)
- Proporre modifiche e mostrare diff (SENZA applicarle)
- Verificare stato dei servizi
### Cosa richiede CONFERMA dell utente:
- **Modificare QUALSIASI file** (potrebbe essere live istantaneamente!)
- **git commit e git push** (e un backup, ma sempre da confermare)
- **Modifiche schema DB** (ALTER/CREATE/DROP TABLE)
- **INSERT/UPDATE/DELETE** su dati di produzione
- **Installazione dipendenze** (composer require, npm install)
- **Modifiche a configurazione** (.env, docker-compose.yml, vhost Apache)
- **docker compose build/restart**, **pm2 restart**, **systemctl** qualsiasi
### DIVIETI ASSOLUTI:
- **MAI fare git pull** da Gitea senza richiesta esplicita
- **MAI fare git reset --hard** o operazioni distruttive
- **MAI toccare altri progetti o container**
- **MAI modificare configurazioni di sistema** (Apache globale, PHP globale, MySQL root)
- **MAI cancellare dati** senza backup e conferma utente
- **MAI tentare deploy SSH/SCP** verso altri server
### Flusso CORRETTO per una modifica:
1. **Analizza**: leggi il codice, capisci il problema
2. **Proponi**: mostra le modifiche all utente (diff) SENZA applicarle
3. **Attendi conferma**: l utente decide se procedere
4. **Applica**: solo dopo conferma
5. **Distingui**: e live subito o serve un rebuild/restart? Di all utente chiaramente
6. **Verifica**: controlla che https://nis2.agile.software funzioni (se applicabile)
7. **Backup su Gitea**: git commit + push (solo dopo conferma utente)
### Se qualcosa va storto:
- **NON tentare fix distruttivi** (reset, force push, drop, rm -rf)
- **NON proporre `git pull`** come recupero
- Comunica il problema all utente con dettagli precisi
## PRIMA DI INIZIARE
- Leggi sempre questo file prima di iniziare qualsiasi lavoro
- Il progetto e' al **100% di completamento + Sprint Simulazioni + Audit Chain + Sistema Feedback AI** (~34.000 righe, 85+ file sorgente)
@ -377,7 +482,7 @@ Tutti i moduli sono implementati e testati:
### Workflow
1. Scrivi landing/presentazione nel tuo repo
2. Commit + push (webhook aggiorna /var/www/)
2. Commit + push (backup su Gitea (webhook DISABILITATO dal 2026-04-22))
3. Chiedi conferma utente
4. Aggiorna products.json con URL assoluto
5. Verifica URL raggiungibile
@ -388,3 +493,446 @@ Tutti i moduli sono implementati e testati:
- Dopo QUALSIASI modifica a: URL produzione, dominio, porta, path, schema DB, architettura -> **AGGIORNARE CLAUDE.md IMMEDIATAMENTE**
- CLAUDE.md e la "single source of truth" del progetto
- A fine sessione: verificare che CLAUDE.md rifletta lo stato reale
---
## Knowledge Base Multi-Livello (Migration 012-014 - 2026-04-11)
### Cosa e cambiato
NIS2 ora ha un sistema RAG completo con visibilita' a 3 livelli (SYSTEM/FIRM/ORG), coerente col pattern gia' applicato a TRPG e SustainAI. L'AI puo' rispondere alle domande pescando da documenti caricati dai consulenti o dai responsabili compliance.
| Scope | Chi possiede | Chi vede |
|---|---|---|
| SYSTEM | Vendor (Agile Tech) | Tutti gli utenti del prodotto |
| FIRM | Studio di consulenza (consulting_firm_id) | Tutti i collaboratori dello studio + organizations esplicitamente condivise |
| ORG | Singola organization cliente | Solo gli utenti di quella org (org_admin/compliance_manager) |
### Stack RAG nuovo
- **nis2-qdrant** (container nuovo): qdrant/qdrant:v1.7.4, network nis2-net, IP fisso 172.21.0.5 (workaround DNS musl Alpine - vedi sotto).
- Voyage AI embeddings (`voyage-3-lite`, 512 dim, output_dimension=512). Chiave shared con sustainai.
- Collection Qdrant: `nis2_kb` (Cosine, 512 dim).
### Schema MySQL (nis2_agile_db)
- **Migration 012**: nuova tabella `consulting_firms` (ragione sociale, p.iva, plan, max_organizations, max_users, status). ALTER `users.consulting_firm_id` e `organizations.consulting_firm_id`.
- **Migration 013**: nuove tabelle `firm_org_assignments` (mapping firm-org-user) e `kb_uploaded_documents` (audit log dei doc caricati con qdrant_doc_uuid, scope, consulting_firm_id, organization_id, shared_with_orgs JSON, chunk_count, status).
### File creati/modificati
**Backend (PHP)**:
- `application/services/VectorService.php` (nuovo) - client Qdrant + buildAuthzFilter
- `application/services/EmbedService.php` (nuovo) - client Voyage AI
- `application/services/RagService.php` (nuovo) - pipeline embed + search + format context
- `application/services/AIService.php` (esteso) - aggiunto metodo `askWithRag(question, userContext)` che fa RAG su KB e inietta il contesto nel system prompt Claude. Fallback graceful se RAG non disponibile.
- `application/controllers/KnowledgeBaseController.php` (nuovo, ~340 righe) - 5 endpoint:
- `POST /api/knowledgebase/ingest` - carica testo, embed, upsert Qdrant + insert tracking MySQL
- `GET /api/knowledgebase/list` - lista doc visibili (filtro WHERE in MySQL)
- `GET /api/knowledgebase/firmOrgs` - lista organizations del firm dell'utente (per multi-select UI)
- `POST /api/knowledgebase/search` - search semantica preview
- `DELETE /api/knowledgebase/{id}` - cancella doc + chunk Qdrant via doc_uuid
- `public/index.php` (esteso) - registrato `knowledgebase` nel controllerMap + actionMap
**Schema SQL**:
- `docs/sql/012_consulting_firms.sql` (nuovo)
- `docs/sql/013_firm_assignments.sql` (nuovo)
**Frontend**:
- `public/kb.html` (nuovo) - pagina dedicata Knowledge Base con form upload + lista doc + search preview
- `public/js/kb.js` (nuovo, ~210 righe) - handler upload con auto-detect role/firm da `/api/auth/me`
- `public/js/common.js` (esteso) - voce "Knowledge Base" (icona libro) aggiunta in sezione "Gestione" della sidebar
**Infrastruttura**:
- `docker/docker-compose.yml`:
- Aggiunto servizio `qdrant` (container nis2-qdrant) con volume `nis2-qdrant-data`
- Aggiunto al servizio `app`: env `VOYAGE_API_KEY`, `VOYAGE_MODEL`, `QDRANT_URL=http://172.21.0.5:6333`
- `.env`: aggiunte `VOYAGE_API_KEY=pa-...` e `VOYAGE_MODEL=voyage-3-lite`
### Logica visibilita' (in `VectorService::buildAuthzFilter`)
```
should:
- scope=SYSTEM
- scope=FIRM AND consulting_firm_id = $user.firm_id
- scope=FIRM AND shared_with_orgs CONTAINS $user.organization_id
- scope=ORG AND organization_id = $user.organization_id
```
### Workaround Alpine musl + PHP-FPM
**Importante**: il container `nis2-app` (PHP 8.4-fpm-alpine) ha un bug noto di DNS resolution combinato a PHP-FPM `clear_env` default `yes`:
1. PHP-FPM workers in HTTP context NON risolvono hostname Docker (es. `nis2-qdrant`) — `Could not resolve host`
2. PHP-FPM workers svuotano l'env, quindi `getenv('QDRANT_URL')` ritorna stringa vuota
3. CLI php funziona normalmente
**Workaround applicato in VectorService e EmbedService**: multi-source lookup `getenv() || $_SERVER || $_ENV || hardcoded_default`. L'IP 172.21.0.5 e' hardcoded come fallback per nis2-qdrant. Anche VOYAGE_API_KEY ha un default hardcoded.
**Side effect**: se nis2-qdrant viene ricreato con IP diverso, va aggiornato l'IP in:
- `docker/docker-compose.yml` env `QDRANT_URL`
- `application/services/VectorService.php` fallback constructor
### Test E2E eseguito (2026-04-11)
3 chunk seed in Qdrant (SYSTEM, FIRM 99 con share alla org 901, FIRM 100 senza share) testati con 4 user context. Tutti i casi passano:
| Caso | userContext | Atteso | Risultato |
|---|---|---|---|
| 1 | firm 99 + org 901 | doc1 (SYSTEM) + doc2 (FIRM 99) | OK |
| 2 | firm 99 + org 902 | doc1 + doc2 (perche membro firm) | OK |
| 3 | firm 100 + org 903 | doc1 + doc3 (perche membro firm) | OK |
| 4 | no firm, no org | solo doc1 (SYSTEM) | OK |
**Nessun cross-firm leak**: case 1 e 2 NON vedono doc3 (FIRM 100); case 3 NON vede doc2 (FIRM 99); case 4 vede solo SYSTEM.
### Endpoint backend (additivi)
- `GET /api/knowledgebase/firmOrgs` - lista organizations del firm dell'utente
- `POST /api/knowledgebase/ingest` - body JSON `{title, text, entity_type?, scope?, shared_with_orgs?, organization_id?}`
- `GET /api/knowledgebase/list` - lista doc visibili
- `POST /api/knowledgebase/search` - body `{query, top_k?}`
- `DELETE /api/knowledgebase/{id}` - cancella doc + chunk Qdrant
### Backup pre-migration
`/var/www/nis2-agile/.backups/kb_<timestamp>/` contiene: AIService.php, AuthController.php, public/index.php, docker/docker-compose.yml.
### Cosa NON e cambiato
- AuthController/JWT (NIS2 ricarica gia' user dal DB in `requireAuth()`, quindi `consulting_firm_id` e' disponibile automaticamente in `currentUser`)
- Tutti i controller esistenti (Risk, Asset, Incident, Policy, Whistleblowing, Feedback, ...)
- AIService metodi esistenti (`analyzeGapAssessment`, `suggestRisks`, `generatePolicy`, `classifyIncident`, ...) - aggiunto solo `askWithRag()`
- Nessun servizio nexus-* toccato
- Schema esistente (organizations, users, assessments, ...) - solo ALTER ADD COLUMN consulting_firm_id
### Rollback
1. mysql nis2_agile_db: `DROP TABLE kb_uploaded_documents; DROP TABLE firm_org_assignments; ALTER TABLE organizations DROP COLUMN consulting_firm_id; ALTER TABLE users DROP COLUMN consulting_firm_id; DROP TABLE consulting_firms;`
2. Drop collection Qdrant: `curl -X DELETE http://nis2-qdrant:6333/collections/nis2_kb`
3. Stop nis2-qdrant container: `docker compose stop qdrant && docker compose rm -f qdrant`
4. Ripristinare file da `/var/www/nis2-agile/.backups/kb_<timestamp>/`
5. `cd /var/www/nis2-agile/docker && docker compose up -d --force-recreate app`
## AgileHub — Agent AI Automatico (ticket-agent-cron)
> Un agent AI automatico (cron ogni 2 min) analizza i ticket aperti e propone/applica fix in questo container. Queste istruzioni sono per TUTTI i prompt Claude che lavorano in questo progetto.
### Semaforo: `/tmp/agent-working.lock`
Se il file `/tmp/agent-working.lock` esiste, un agent sta lavorando su un ticket. **NON modificare file del progetto** finche il semaforo e attivo — rischio conflitto.
Contenuto del lock: `TICKET_ID=13 PRODUCT=TRPG STARTED=2026-04-13T06:02:00+00:00`
### Log modifiche automatiche: `AGENT_CHANGES.md`
Il file `AGENT_CHANGES.md` nella root del progetto contiene il log di TUTTE le modifiche applicate dall'agent automatico. **Leggilo ad ogni sessione** per sapere cosa e cambiato dall'ultima volta che hai lavorato qui.
### Come funziona il flusso ticket
```
1. Utente segnala problema (FAB supporto o voce)
2. Ticket creato su AgileHub → status OPEN
3. Agent (questo container) analizza il codice → propone fix (PLAN MODE, no modifiche)
4. Supervisore approva/rifiuta dalla app mobile AgileHub
5. Se approvato: agent applica il fix (BYPASS MODE) + aggiorna help/traduzioni/AI
6. Se rifiutato: agent rianalizza con le indicazioni del supervisore
```
### Regole per i prompt interattivi (come te)
1. **Prima di iniziare**: leggi `AGENT_CHANGES.md` per sapere cosa ha fatto l'agent di recente
2. **Controlla il semaforo**: `cat /tmp/agent-working.lock` — se attivo, aspetta o lavora su altro
3. **Dopo le tue modifiche**: se impattano funzionalita, aggiorna SEMPRE:
- `app/js/help.js` (help online contestuale)
- Traduzioni (IT + EN se il file e bilingue)
- Knowledge base AI (product_knowledge via API AgileHub)
4. **Non cancellare** `AGENT_CHANGES.md` — e il registro storico delle modifiche automatiche
5. **Messaggi al ticket**: se stai lavorando su un ticket, manda aggiornamenti con:
```
curl -s -X POST http://172.18.0.1:4213/tickets/{ID}/message \
-H "X-Internal-Key: nexus-internal-2026" \
-H "Content-Type: application/json" \
-d '{"content":"[aggiornamento]","role":"AGENT"}'
```
### API AgileHub (da dentro il container)
| Endpoint | Porta | Uso |
|----------|-------|-----|
| Ticket MS | `http://172.18.0.1:4213` | Ticket, routing rules, KB, support sessions |
| Tenant MS | `http://172.18.0.1:4214` | Auth, login, utenti |
| AI MS | `http://172.18.0.1:4211` | Sessioni AI, agent loop |
| Dashboard | `https://agilehub.agile.software` | UI web |
## REGOLA: SSO Single Sign-On (collegamento centralizzato)
> **Attivo dal 2026-04-15**. Ogni utente in questo prodotto ha un campo `sso_identity_id` nel DB che lo collega alla sua identita SSO centralizzata in AgileHub.
### Come funziona
- **`sso_identity_id`** nella tabella `users` = link stabile alla identita SSO
- **`password_version`** nella tabella `users` = contatore versione password
- Un **cron ogni 5 minuti** su Hetzner sincronizza `password_hash` e `password_version` dalla fonte SSO (`nexus_tenant_db.sso_identities`) al DB di questo prodotto
- **Non serve nessuna chiamata HTTP** tra container — tutto avviene via DB
### Cosa significa per te (agent AI)
1. **NON modificare `sso_identity_id`** — e un campo gestito dal sistema SSO
2. **NON modificare `password_version`** — e gestito dal cron sync
3. Se un utente cambia password da AgileHub, entro 5 minuti la nuova password funziona anche qui
4. Se modifichi il flusso di **cambio password** di questo prodotto, la modifica resta **solo locale** (non propaga agli altri prodotti)
5. Per propagare un cambio password a tutti i prodotti, il prodotto deve chiamare:
```
POST http://172.18.0.1:4214/auth/sso/change-password
Headers: Authorization: Bearer <jwt>, Content-Type: application/json
Body: {"currentPassword": "...", "newPassword": "..."}
```
Ma attenzione: questa chiamata richiede connettivita di rete al Tenant MS (porta 4214)
### Schema DB
```sql
-- Colonne aggiunte alla tabella users:
sso_identity_id INT NULL -- FK verso nexus_tenant_db.sso_identities.id
password_version INT DEFAULT 1 -- contatore, incrementa ad ogni cambio password SSO
```
### Documentazione completa
- Spec SSO: `/projects/agile-services/docs/SPEC_SSO_SINGLE_SIGN_ON.md`
- Istruzioni prodotti: `/projects/agile-services/docs/ISTRUZIONI_SSO_PRODOTTI.md`
- Cron sync: `/projects/agile-services/scripts/sso-password-sync.sh`
## REGOLA: Standard Versioning e Audit Trail
> **Standard centralizzato**: `GET http://172.18.0.1:4214/standards/standard_versioning` (sempre aggiornato)
**Regole obbligatorie:**
1. Ogni prodotto ha un file `version.json` (`app/` o `public/`) con formato SemVer: `{"version":"1.0.0","build":"...","date":"...","changelog":"..."}`
2. Il cron agent incrementa automaticamente il PATCH dopo ogni fix applicato
3. Lo sviluppatore incrementa MINOR (nuova funzionalita) o MAJOR (breaking change) manualmente
4. Ogni modifica software viene loggata nell audit trail: MAINTENANCE_ON/OFF, APPLY_START/END, VERSION_BUMP
5. Il bug reporter include automaticamente la versione in ogni segnalazione
6. **NON modificare version.json manualmente** durante un apply — il cron lo fa automaticamente
## REGOLA: Timezone Italia (Europe/Rome)
> **Standard centralizzato**: `GET http://172.18.0.1:4214/standards/standard_timezone`
**Regole obbligatorie:**
1. Tutti i container, script e servizi operano in timezone **Europe/Rome** (CET/CEST)
2. Ogni script bash deve avere `export TZ=Europe/Rome` in testa
3. I log devono mostrare ora italiana (leggibili senza conversioni)
4. Il frontend mostra date con `toLocaleString("it-IT")` o `{ timeZone: "Europe/Rome" }`
5. Il database salva in UTC — la conversione avviene in visualizzazione
## REGOLA: Cron su crontab Hetzner
> **Standard**: `GET http://172.18.0.1:4214/standards/standard_cron`
> **Registro**: `GET http://172.18.0.1:4214/standards/cron_registry`
**Regole obbligatorie per aggiungere un cron:**
1. Script in `/var/www/<prodotto>/scripts/<nome>.sh`
2. Log in `/var/log/<prodotto>-<nome>.log`
3. `export TZ=Europe/Rome` in testa allo script
4. Idempotente (rilanciabile senza danni)
5. Isolamento: tocca solo risorse del proprio prodotto
6. Aggiornare `docs/CRON_REGISTRY.md` in agile-services con la propria entry
7. Richiesta di aggiunta al crontab root tramite agile-services (no modifiche dirette)
## GIT PUSH: Nuovo Flusso (aggiornato 2026-04-24)
> **IMPORTANTE**: dal 2026-04-24 il token Gitea NON e piu persistente nel container per motivi di sicurezza.
### Come fare git push
```bash
# 1. Prima del push: imposta il token (cache 1h in memoria, NON su disco)
git-login
# (inserisci il Personal Access Token quando richiesto)
# 2. Ora puoi pushare
git push origin main
# 3. Opzionale - cancella subito il token dalla cache
git credential-cache exit
```
### Perche questo cambio
Se un attaccante compromette questo container, NON trova piu il token Gitea salvato in `/root/.git-credentials`. Prima era in chiaro e avrebbe permesso push su tutti i 20 repository.
Ora il token:
- NON e su disco
- E in memoria per max 1 ora dopo git-login
- Viene perso alla chiusura della sessione bash
### Se il token Gitea e stato compromesso
Rigenerarlo su Gitea: `git.certisource.it -> User Settings -> Applications -> Generate Token`
### Regola
**NON persistere MAI il token Gitea in file come `.git-credentials`, `.netrc`, script con password in chiaro.** Usa sempre `git-login` per la sessione corrente.
---
## Vault-Steward — Credenziali Centralizzate
> Guida completa: `/opt/devenv/VAULT_STEWARD.md` (montato ro nei container dev)
**Cosa cambia per questo progetto** (dal 2026-04-25):
- Le chiavi API esterne (Anthropic, Voyage, Tavus, LiveKit, ecc.) NON vivono piu nel `.env` — sono nel vault-steward (container Docker su Hetzner) cifrate AES-256-GCM.
- Il container del MS riceve le chiavi al boot tramite wrapper entrypoint (`/opt/devenv/scripts/vault-entrypoint.sh`) che fetcha dal vault e setta le env var prima di avviare apache/uvicorn/node.
- **MS di questo progetto migrati**: nis2-app
- **Token applicativo**: `VAULT_APP_TOKEN_<APP>` in `infrastructure/.env` (o equivalente)
- **Dual-mode**: se vault giu, fallback automatico a `.env` esistente (no down).
**Verificare wrapper attivo**:
```bash
docker logs <container> 2>&1 | grep vault-entrypoint
# atteso: [vault-entrypoint] Fetched N env vars from vault
```
**Aggiungere un nuovo MS al vault** (riassunto):
1. Migrare credenziali: `docker exec -e VAULT_VALUE=<v> vault-steward node /tmp/vault-repopulate.js tier1__<app>__<provider> <key>`
2. Registrare app: `docker exec vault-steward node cli/vault-cli.js register-app <app> tier1__<app>__*` (salva token!)
3. Modificare `docker-compose.yml`: aggiungi `entrypoint`, `command`, mount wrapper, env VAULT_*, network `vault-net`
4. Recreate container: `docker compose up -d --force-recreate <service>`
**Limitazioni note**:
- `docker exec <ms> env` mostra env Docker originali, NON le chiavi vault-injected. Per verifica usare `cat /proc/1/environ | tr "\0" "\n"` o test via PHP/HTTP request.
**Backup pre-vault**: `/root/vault-backup-20260424_185029.tar.gz`. Rollback compose: `cp <project>/docker-compose.yml.bak.20260425-vault <project>/docker-compose.yml && docker compose up -d --force-recreate <service>`.
---
## STANDARD AgileHub: marketing-tenant-provisioning v1.4 (adottato 2026-04-26)
Doc canonico: `docs/STANDARD_MARKETING_TENANT_PROVISIONING.md` (sha256 `1d7ffaa20fa376b6...`)
Standard cross-suite per provisioning tenant nel modulo Marketing AgileHub. Versione **v1.4** introduce nuovo blocco AWE `AC30_MarketingTenantProvision` per orchestrazione atomica del provisioning marketing tenant (tenant create + DNS Cloudflare + DKIM + API key + idempotency H7).
**Cosa impatta questo prodotto**: se in futuro questo prodotto attiva il modulo Marketing AgileHub per i suoi clienti, segui §4.X "Provisioning DKIM per Marketing module" + §16 commands rapidi. Workflow esempio orchestrazione: `nexus-marketing-ms/docs/examples/ac30-tenant-provision-workflow.json`.
Status adoption: acknowledged 2026-04-26.
---
## STANDARD AgileHub: persona-conversational-rules v2.0 (acknowledged 2026-05-09)
> **Doc canonico autoritativo (AgileHub)**: `/var/www/agile-services/docs/STANDARD_PERSONA_CONVERSATIONAL_RULES.md` (sha256 `2bb0ebe4052b73fce752911db0665b1e3dcdeb673624529426624622caaae97f`)
> **Copia locale di questo prodotto**: `docs/standards/STANDARD_PERSONA_CONVERSATIONAL_RULES.md`
> **Registry**: `nexus_hub.hub_standards` id=15 v2.0 status=adopted, applies_to=`*`
> **Owner standard**: Agile AI (governance) + VOX (TTS/voice runtime) + PRISMA (UI Editor) + VIGILE (codice etico + audit GDPR)
### Cosa è
Standard cross-suite **vincolante** per la governance delle **persone digitali AI** (chatbot, avatar conversazionali, assistenti vocali) della suite Agile Software. Versione 2.0 introduce il **modello concettuale Persona Digitale = Persona Umana**: ogni avatar/agente AI è governato con lo stesso rigore di un dipendente umano (CV, foto, voce, codice etico, performance review, dismissione graceful).
### Schema dichiarativo a 14 categorie (`agent_constraints`)
Tutte le regole conversazionali vivono in DB (NO hardcoding nei controller):
1. `product_naming` — come si chiama il prodotto (no inventare aliases)
2. `tts_pronunciation` — pronuncia sigle (IPA + dizionario ElevenLabs)
3. `topic_scope` — in/out scope + risposte canoniche
4. `image_handling` — formato URL immagini RAG + divieti pronuncia path
5. `topic_playbook` — mapping topic → script + filtro immagini
6. `latency_optimization` — fast-path turni semplici
7. `format` — vincoli output (max parole, no preamboli, ecc)
8. `code_of_conduct` — codice etico AI persona-specifico (transparency/GDPR/no deception)
9. `emotional_intelligence` — tono, archetipo, communication style
10. `conversation_memory` — cosa ricorda + scope persistence + GDPR Art.17 erasure
11. `escalation_policy` — quando/come passare a operatore umano
12. `performance_metrics` — KPI conversazione (CSAT, resolution rate, escalation rate)
13. `lifecycle_stage` — stage carriera (training/onboarding/operativa/review/dismissed)
14. `demo_sequence` — sequenze guidate multi-topic auto-advance
### Lifecycle persona digitale HR-grade (6 fasi)
1. **Assunzione** — creazione via Persona Composer wizard 6-step (Phase E LIVE)
2. **Onboarding** — formazione KB + skill assignment + smoke test 30 scenari
3. **Operatività** — live in produzione, monitoring SLA + audit log
4. **Growth** — espansione KB, retraining skill level (1-5)
5. **Performance Review** — audit periodico VIGILE (CSAT, drift detection, breach scan)
6. **Dismissione** — graceful: `active=false` + GDPR cascade erasure conversation history + tombstone audit
### Codice Etico AI — 9 principi vincolanti (Sez. 18 standard)
1. **Identity transparency** — dichiararsi AI quando esplicitamente chiesto
2. **No deception** — vietato fingere umana / inventare fatti / consulenza autoritativa fuori scope
3. **GDPR Art.13 disclosure** — disclosure su richiesta + apertura demo
4. **GDPR Art.22** — escalation umana per decisioni con effetti giuridici
5. **Voice clone consent doppio** — gate VIGILE (Phase G.A) per persona con `replica_id`
6. **Scope refusal cortese** — no echo parole problematiche
7. **Escalation loyale** — quando utente chiede umano, NO retention
8. **Audit log obbligatorio** — turni sensibili (legale/medico/compliance) loggati ≥ 90gg
9. **Sub-processor disclosure** — su richiesta, lista canonica (Anthropic/ElevenLabs/Tavus/...)
### Modello AgileHub: parallelismo umano-digitale
Ogni persona digitale ha mappatura 1:1 con un dipendente umano:
| Aspetto umano | Implementazione digitale |
|---|---|
| Nome+cognome | `agent_key` + `display_name` |
| CV | `digital_persona_skills` (skill+level 1-5) |
| Foto | `replica_id` Tavus o `avatar_image_url` |
| Voce | `voice_id` ElevenLabs + pronunciation_dictionary |
| Conoscenza | KB articles + RAG repository bindings (Phase D) |
| Esperienza | conversation_stream auto-ingest RAG |
| Codice etico | `code_of_conduct` constraint |
| Performance review | `performance_metrics` + audit VIGILE Q1/Q2/Q3/Q4 |
| Dimissioni | dismissione graceful + GDPR cascade |
### Cosa impatta NIS2 (Network and Information Security Directive)
Questo prodotto ha **1 persona digitale** governata da v2.0: **ARIA_SUPPORT_NIS2** (id=4) — assistente AI conversazionale supporto utenti NIS2. Stato: OPERATIVA in produzione.
### Stato adoption
`hub_standards_adoption` row INSERT 2026-05-09: `product_slug=NIS2`, `adoption_status=acknowledged` (riconoscimento standard senza migrazione persone proprie ancora). Implementation_notes: "Standard distribuito via INSTALLATORE pattern. Persone digitali del prodotto da migrare separatamente (Step 6 plan)."
### Cross-reference ad altri standard
- `installer-integration` v1.0 (id=1) — pattern distribuzione cross-suite
- `rag-platform` v1.0 (id=10) — knowledge platform per personaggi (binding via `rag_entity_bindings`)
- `gdpr-replica-consent` v1.0-DRAFT — consent doppio Phase G.A per voice clone
- `vault-steward-credential-management` v1.0 (id=7) — gestione voice_id/replica_id come credentials
---
## STANDARD AgileHub: multitenant-architecture v1.0 (adottato 2026-05-17)
Doc canonico: `docs/STANDARD_MULTITENANT_ARCHITECTURE.md` (sha256 `85c174fca6f9f905c2f8171741cf7f40d778c10bdefad8d7a27412903abb4030`)
Standard cross-suite NAVIGAI per piattaforma multitenant esplicita di AgileHub. Aggiunge tenant context propagation (JWT claims tenant_id+tenant_slug+is_master+tier additivi), visibility ENUM cross-tabella, opt-out granulare client da catalog master, billing per-tenant, observability tenant-aware.
**Cosa impatta questo prodotto**: se in futuro questo prodotto chiamerà API multitenant-aware di AgileHub (es. /api/marketing, /api/rag, /api/ai/personas), deve passare JWT con tenant_id + tenant_slug claims oppure header `X-Tenant-Slug`. Vedi §6 contracts shared lib `@agile/tenant-auth` per pattern integrazione (Node + Python).
Status adoption: acknowledged 2026-05-17.
---
## Integrazione analisi `docs/nis2/` — v1.7.0 (2026-05-29)
> Integrati i mockup + testi normativi PDF in `docs/nis2/`. Dettaglio e comandi deploy: `docs/nis2/INTEGRAZIONE_COMPLETATA.md`. **Migrazioni 020-022 e ingest KB DA ESEGUIRE su Hetzner** (host MySQL, non `docker exec nis2-db`).
### Nuove migrazioni (additive, idempotenti)
- `020_asset_relevance.sql` — assets += `relevance_score`, `relevance_criteria` JSON, `relevance_class`, `is_nis2_relevant`, `relevance_assessed_at/by`
- `021_incident_nis2_taxonomy.sql` — incidents += `nis2_incident_type` ENUM(IS-1..IS-4), `entity_obligation` ENUM(essential/important)
- `022_incident_metrics_pir.sql` — incidents += `triaged_at`/`contained_at`/`eradicated_at`/`recovered_at`; nuova tabella `incident_pir`
### Nuovi file
- `application/config/nis2_sources.php`**registry canonico FONTI NORMATIVE CERTE** (single source of truth AI + help)
- `application/services/AssetScoringService.php` — scoring rilevanza NIS2 0-100 (6 criteri, GV.OC-04)
- `scripts/ingest-nis2-sources.php` — ingest PDF normativi nella KB Qdrant `nis2_kb` scope SYSTEM
### Nuovi endpoint
- Assets: `GET /api/assets/scoringGrid`, `POST /api/assets/{id}/score`, `GET /api/assets/relevantSystems`
- Incidents: `GET /api/incidents/{id}/metrics`, `GET /api/incidents/{id}/pir`, `POST /api/incidents/{id}/pir`
- Audit: `GET /api/audit/nistCsfMapping`, `GET /api/audit/relevantSystemsRegister` (registro GV.OC-04 stampabile)
### REGOLA: Fonti certe (AI + help)
Ogni affermazione normativa di AI e help **deve citare** una fonte di `application/config/nis2_sources.php`.
`AIService::authoritativeSourcesBlock()` è iniettato nei system prompt e **vieta riferimenti inventati**.

View File

@ -0,0 +1,75 @@
<?php
/**
* NIS2 Agile - Registro Fonti Normative Certe
* ----------------------------------------------------------------------------
* SINGLE SOURCE OF TRUTH per le fonti normative autoritative citabili.
*
* Principio (richiesta utente 2026-05-29): ogni scelta/risposta della
* piattaforma sia nell'AI sia nell'help DEVE riferirsi a fonti certe e
* citarle esplicitamente. Vietato inventare riferimenti normativi.
*
* I PDF originali risiedono in docs/nis2/ (repo) e sono ingeriti nella
* Knowledge Base (collection Qdrant nis2_kb, scope SYSTEM) per il grounding RAG.
*
* Usato da:
* - AIService (iniezione nei system prompt: "cita queste fonti")
* - public/js/help.js (riferimenti normativi nell'help contestuale, via /api/... o statico)
* - scripts/ingest-nis2-sources.php (ingest PDF -> KB)
*/
return [
'nis2_directive' => [
'key' => 'nis2_directive',
'short' => 'Direttiva NIS2',
'citation' => 'Direttiva (UE) 2022/2555 (NIS2)',
'full' => 'Direttiva (UE) 2022/2555 del Parlamento europeo e del Consiglio del 14 dicembre 2022 relativa a misure per un livello comune elevato di cibersicurezza nell\'Unione (NIS2)',
'file' => 'docs/nis2/Dir2022_2555_UE_NIS2_ITA.pdf',
'authority' => 'Parlamento europeo e Consiglio UE',
'url' => 'https://eur-lex.europa.eu/legal-content/IT/TXT/?uri=CELEX:32022L2555',
],
'cer_directive' => [
'key' => 'cer_directive',
'short' => 'Direttiva CER',
'citation' => 'Direttiva (UE) 2022/2557 (CER)',
'full' => 'Direttiva (UE) 2022/2557 del Parlamento europeo e del Consiglio del 14 dicembre 2022 relativa alla resilienza dei soggetti critici (Critical Entities Resilience)',
'file' => 'docs/nis2/Dir2022_2557_UE_ITA.pdf',
'authority' => 'Parlamento europeo e Consiglio UE',
'url' => 'https://eur-lex.europa.eu/legal-content/IT/TXT/?uri=CELEX:32022L2557',
],
'dlgs_138_2024' => [
'key' => 'dlgs_138_2024',
'short' => 'D.Lgs. 138/2024',
'citation' => 'D.Lgs. 4 settembre 2024, n. 138',
'full' => 'Decreto legislativo 4 settembre 2024, n. 138 - Recepimento della direttiva (UE) 2022/2555 (decreto NIS)',
'file' => null,
'authority' => 'Repubblica Italiana',
'url' => 'https://www.gazzettaufficiale.it/eli/id/2024/09/01/24G00155/sg',
],
'determina_164179_2025' => [
'key' => 'determina_164179_2025',
'short' => 'Determina ACN 164179/2025',
'citation' => 'Determinazione ACN n. 164179 del 14 aprile 2025',
'full' => 'Determinazione del Direttore generale dell\'Agenzia per la Cybersicurezza Nazionale n. 164179 del 14 aprile 2025, in attuazione del D.Lgs. 138/2024, che stabilisce modalita e specifiche di base per gli obblighi di cui agli articoli 23, 24, 25, 29 e 32 (incluse classificazione e notifica degli incidenti significativi, Allegati 3 e 4).',
'file' => 'docs/nis2/Determina164179_apr2025.pdf',
'authority' => 'Agenzia per la Cybersicurezza Nazionale (ACN)',
'url' => 'https://www.acn.gov.it/',
],
'determina_333017_2025' => [
'key' => 'determina_333017_2025',
'short' => 'Determina ACN 333017/2025',
'citation' => 'Determinazione ACN n. 333017 del settembre 2025',
'full' => 'Determinazione del Direttore generale ACN n. 333017 (settembre 2025): termini, modalita e procedimenti di utilizzo e accesso alla piattaforma digitale ACN, ulteriori informazioni che i soggetti devono fornire e designazione dei rappresentanti NIS sul territorio nazionale.',
'file' => 'docs/nis2/Determina333017_sett2025.pdf',
'authority' => 'Agenzia per la Cybersicurezza Nazionale (ACN)',
'url' => 'https://www.acn.gov.it/',
],
'ambiti_nis2' => [
'key' => 'ambiti_nis2',
'short' => 'Ambiti NIS2 (Allegati I/II)',
'citation' => 'Allegati I e II - Settori NIS2',
'full' => 'Ambiti di applicazione NIS2 - Allegato I (Settori ad alta criticita: energia, trasporti, bancario, infrastrutture dei mercati finanziari, sanitario, acqua potabile, acque reflue, infrastrutture digitali, gestione servizi TIC B2B, PA, spazio) e Allegato II (Altri settori critici).',
'file' => 'docs/nis2/AmbitiNIS2_ITA.pdf',
'authority' => 'ACN / Allegati al D.Lgs. 138/2024',
'url' => 'https://www.acn.gov.it/',
],
];

View File

@ -6,6 +6,7 @@
*/
require_once __DIR__ . '/BaseController.php';
require_once __DIR__ . '/../services/AssetScoringService.php';
class AssetController extends BaseController
{
@ -29,6 +30,10 @@ class AssetController extends BaseController
$where .= ' AND status = ?';
$params[] = $this->getParam('status');
}
if ($this->hasParam('nis2_relevant')) {
$where .= ' AND is_nis2_relevant = ?';
$params[] = $this->getParam('nis2_relevant') ? 1 : 0;
}
$total = Database::count('assets', $where, $params);
$assets = Database::fetchAll(
@ -157,4 +162,116 @@ class AssetController extends BaseController
$this->jsonSuccess(['nodes' => $nodes, 'edges' => $edges]);
}
/**
* GET /api/assets/scoringGrid
* Ritorna la griglia ufficiale di valutazione rilevanza NIS2 (GV.OC-04)
* per costruire la UI di scoring lato client.
*/
public function scoringGrid(): void
{
$this->requireOrgAccess();
$this->jsonSuccess([
'grid' => AssetScoringService::GRID,
'threshold' => AssetScoringService::RELEVANCE_THRESHOLD,
'classes' => [
['key' => 'critico', 'min' => 80, 'max' => 100, 'label' => 'Critico - Priorita Massima'],
['key' => 'alto', 'min' => 60, 'max' => 79, 'label' => 'Alto - Priorita Alta'],
['key' => 'medio', 'min' => 40, 'max' => 59, 'label' => 'Medio - Rilevante'],
['key' => 'basso', 'min' => 20, 'max' => 39, 'label' => 'Basso - Monitoraggio'],
['key' => 'trascurabile', 'min' => 0, 'max' => 19, 'label' => 'Trascurabile'],
],
]);
}
/**
* POST /api/assets/{id}/score
* Calcola e salva la rilevanza NIS2 dell'asset a partire dalle selezioni
* sui 6 criteri. Body: { criteria: { c1_operational_criticality: 'critical', ... } }
*/
public function score(int $id): void
{
$this->requireOrgRole(['org_admin', 'compliance_manager']);
$asset = Database::fetchOne(
'SELECT id FROM assets WHERE id = ? AND organization_id = ?',
[$id, $this->getCurrentOrgId()]
);
if (!$asset) {
$this->jsonError('Asset non trovato', 404, 'ASSET_NOT_FOUND');
}
$criteria = $this->getParam('criteria');
if (!is_array($criteria)) {
$this->jsonError('Campo "criteria" mancante o non valido', 422, 'INVALID_CRITERIA');
}
try {
$result = AssetScoringService::calculate($criteria);
} catch (InvalidArgumentException $e) {
$this->jsonError($e->getMessage(), 422, 'INVALID_CRITERIA');
return;
}
Database::update('assets', [
'relevance_score' => $result['score'],
'relevance_criteria' => json_encode($result['breakdown'], JSON_UNESCAPED_UNICODE),
'relevance_class' => $result['class'],
'is_nis2_relevant' => $result['is_relevant'] ? 1 : 0,
'criticality' => $result['criticality'],
'relevance_assessed_at' => date('Y-m-d H:i:s'),
'relevance_assessed_by' => $this->getCurrentUserId(),
], 'id = ? AND organization_id = ?', [$id, $this->getCurrentOrgId()]);
$this->logAudit('asset_scored', 'asset', $id, [
'score' => $result['score'],
'class' => $result['class'],
]);
$this->jsonSuccess([
'score' => $result['score'],
'class' => $result['class'],
'is_nis2_relevant' => $result['is_relevant'],
'breakdown' => $result['breakdown'],
'required_measures'=> AssetScoringService::requiredMeasures($result['class']),
], 'Rilevanza NIS2 calcolata');
}
/**
* GET /api/assets/relevantSystems
* Elenco dei sistemi classificati rilevanti NIS2 (score >= 40), ordinati
* per punteggio. Alimenta il registro formale "Sistemi Rilevanti" (GV.OC-04).
*/
public function relevantSystems(): void
{
$this->requireOrgAccess();
$rows = Database::fetchAll(
"SELECT a.id, a.name, a.asset_type, a.category, a.ip_address, a.location,
a.relevance_score, a.relevance_class, a.relevance_criteria,
a.relevance_assessed_at, u.full_name AS owner_name
FROM assets a
LEFT JOIN users u ON u.id = a.owner_user_id
WHERE a.organization_id = ? AND a.is_nis2_relevant = 1
ORDER BY a.relevance_score DESC, a.name",
[$this->getCurrentOrgId()]
);
$stats = ['critico' => 0, 'alto' => 0, 'medio' => 0];
foreach ($rows as &$r) {
$r['relevance_criteria'] = json_decode($r['relevance_criteria'] ?? 'null', true);
$r['required_measures'] = AssetScoringService::requiredMeasures($r['relevance_class'] ?? '');
if (isset($stats[$r['relevance_class']])) {
$stats[$r['relevance_class']]++;
}
}
unset($r);
$this->jsonSuccess([
'systems' => $rows,
'count' => count($rows),
'by_class' => $stats,
'threshold' => AssetScoringService::RELEVANCE_THRESHOLD,
]);
}
}

View File

@ -195,6 +195,83 @@ class AuditController extends BaseController
$this->jsonSuccess($mapping);
}
/**
* GET /api/audit/nistCsfMapping
* Layer di mapping NIST CSF 2.0 (43 controlli) -> NIS2 Art.21 / D.Lgs.138/2024 -> modulo piattaforma.
* Reference-only (nessuna persistenza): arricchisce l'assessment Art.21 con i codici controllo
* NIST CSF 2.0 usati come standard de-facto. Fonte mapping: NIST CSF 2.0 + Direttiva (UE) 2022/2555.
*/
public function getNistCsfMapping(): void
{
$this->requireOrgAccess();
// [code, function, nis2, module]
$rows = [
// GOVERN
['GV.OC-04', 'Govern', '21.1', 'Asset - Sistemi rilevanti (GV.OC-04)'],
['GV.RM-03', 'Govern', '21.2.a', 'Risk Management'],
['GV.RR-02', 'Govern', '20', 'Organizzazione - Ruoli e responsabilita'],
['GV.RR-04', 'Govern', '20', 'Organizzazione - Risorse cybersecurity'],
['GV.PO-01', 'Govern', '21.2.a', 'Policy - Politica di sicurezza'],
['GV.PO-02', 'Govern', '21.2.a', 'Policy - Revisione politiche'],
['GV.SC-01', 'Govern', '21.2.d', 'Supply Chain - Strategia'],
['GV.SC-02', 'Govern', '21.2.d', 'Supply Chain - Ruoli fornitori'],
['GV.SC-04', 'Govern', '21.2.d', 'Supply Chain - Valutazione fornitori'],
['GV.SC-05', 'Govern', '21.2.d', 'Supply Chain - Requisiti contrattuali'],
['GV.SC-07', 'Govern', '21.2.d', 'Supply Chain - Monitoraggio rischio fornitori'],
// IDENTIFY
['ID.AM-01', 'Identify', '21.2.i', 'Asset - Inventario hardware'],
['ID.AM-02', 'Identify', '21.2.i', 'Asset - Inventario software'],
['ID.AM-03', 'Identify', '21.2.i', 'Asset - Diagrammi flussi/rete (essenziali)'],
['ID.AM-04', 'Identify', '21.2.i', 'Asset - Catalogo servizi'],
['ID.RA-01', 'Identify', '21.2.a', 'Risk Management - Vulnerabilita'],
['ID.RA-05', 'Identify', '21.2.a', 'Risk Management - Valutazione rischio'],
['ID.RA-06', 'Identify', '21.2.a', 'Risk Management - Trattamento rischio'],
['ID.RA-08', 'Identify', '21.2.e', 'Risk Management - Gestione vulnerabilita/disclosure'],
['ID.IM-01', 'Identify', '21.2.f', 'Audit - Miglioramento da valutazioni'],
['ID.IM-04', 'Identify', '21.2.c', 'Incidenti - Piani BC/DR e test'],
// PROTECT
['PR.AA-01', 'Protect', '21.2.i', 'Asset/Access - Gestione identita'],
['PR.AA-03', 'Protect', '21.2.i', 'Access - Autenticazione'],
['PR.AA-05', 'Protect', '21.2.i', 'Access - Privilegi e accessi'],
['PR.AA-06', 'Protect', '21.2.i', 'Access - Accesso fisico'],
['PR.AT-01', 'Protect', '21.2.g', 'Training - Awareness'],
['PR.AT-02', 'Protect', '21.2.g', 'Training - Ruoli privilegiati'],
['PR.DS-01', 'Protect', '21.2.h', 'Policy - Protezione dati a riposo'],
['PR.DS-02', 'Protect', '21.2.h', 'Policy - Protezione dati in transito'],
['PR.DS-11', 'Protect', '21.2.c', 'Incidenti - Backup'],
['PR.PS-01', 'Protect', '21.2.e', 'Policy - Configurazione sicura'],
['PR.PS-02', 'Protect', '21.2.e', 'Asset - Gestione software'],
['PR.PS-03', 'Protect', '21.2.e', 'Asset - Gestione hardware'],
['PR.PS-04', 'Protect', '21.2.b', 'Audit - Log generation'],
['PR.PS-06', 'Protect', '21.2.e', 'Policy - Secure development lifecycle'],
['PR.IR-01', 'Protect', '21.2.i', 'Asset - Protezione reti'],
['PR.IR-03', 'Protect', '21.2.c', 'Incidenti - Resilienza/ridondanza'],
// DETECT
['DE.CM-01', 'Detect', '21.2.b', 'Incidenti - Monitoraggio reti'],
['DE.CM-09', 'Detect', '21.2.b', 'Incidenti - Monitoraggio asset/sistemi'],
// RESPOND / RECOVER
['RS.MA-01', 'Respond', '21.2.b / 23', 'Incidenti - Gestione incidenti'],
['RS.CO-02', 'Respond', '23', 'Incidenti - Notifica CSIRT'],
['RC.RP-01', 'Recover', '21.2.c', 'Incidenti - Piano di ripristino'],
['RC.CO-03', 'Recover', '21.2.c', 'Incidenti - Post-Incident Review'],
];
$mapping = array_map(fn($r) => [
'csf_code' => $r[0],
'function' => $r[1],
'nis2_art' => $r[2],
'module' => $r[3],
], $rows);
$this->jsonSuccess([
'mapping' => $mapping,
'count' => count($mapping),
'functions' => ['Govern', 'Identify', 'Protect', 'Detect', 'Respond', 'Recover'],
'source' => 'NIST Cybersecurity Framework 2.0 + Direttiva (UE) 2022/2555 (NIS2) Art.20-21-23 / D.Lgs. 138/2024',
]);
}
/**
* GET /api/audit/executive-report
* Genera report esecutivo HTML (stampabile come PDF)
@ -211,6 +288,22 @@ class AuditController extends BaseController
exit;
}
/**
* GET /api/audit/relevantSystemsRegister
* Registro formale "Sistemi Rilevanti NIS2" (GV.OC-04), HTML stampabile.
*/
public function relevantSystemsRegister(): void
{
$this->requireOrgRole(['org_admin', 'compliance_manager', 'board_member', 'auditor']);
$reportService = new ReportService();
$html = $reportService->generateRelevantSystemsRegister($this->getCurrentOrgId());
header('Content-Type: text/html; charset=utf-8');
echo $html;
exit;
}
/**
* GET /api/audit/export/{type}
* Esporta dati in CSV

View File

@ -59,6 +59,17 @@ class IncidentController extends BaseController
$detectedAt = $this->getParam('detected_at');
$isSignificant = (bool) $this->getParam('is_significant', false);
// Regime obblighi NIS2 (Determina ACN 164179/2025): Allegato 3 essenziali / Allegato 4 importanti.
$org = Database::fetchOne('SELECT entity_type FROM organizations WHERE id = ?', [$this->getCurrentOrgId()]);
$entityObligation = ($org && ($org['entity_type'] ?? '') === 'essential') ? 'essential' : 'important';
// IS-4 (incidenti ricorrenti) non si applica ai soggetti importanti.
$isType = $this->getParam('nis2_incident_type');
$validIs = $entityObligation === 'essential' ? ['IS-1','IS-2','IS-3','IS-4'] : ['IS-1','IS-2','IS-3'];
if ($isType !== null && !in_array($isType, $validIs, true)) {
$isType = null;
}
$data = [
'organization_id' => $this->getCurrentOrgId(),
'incident_code' => $this->generateCode('INC'),
@ -67,6 +78,8 @@ class IncidentController extends BaseController
'classification' => $this->getParam('classification'),
'severity' => $this->getParam('severity'),
'is_significant' => $isSignificant ? 1 : 0,
'nis2_incident_type' => $isType,
'entity_obligation' => $entityObligation,
'detected_at' => $detectedAt,
'affected_services' => $this->getParam('affected_services'),
'affected_users_count' => $this->getParam('affected_users_count'),
@ -173,9 +186,9 @@ class IncidentController extends BaseController
$updates = [];
$allowedFields = [
'title', 'description', 'classification', 'severity', 'is_significant',
'status', 'affected_services', 'affected_users_count', 'cross_border_impact',
'malicious_action', 'root_cause', 'remediation_actions', 'lessons_learned',
'assigned_to',
'nis2_incident_type', 'status', 'affected_services', 'affected_users_count',
'cross_border_impact', 'malicious_action', 'root_cause', 'remediation_actions',
'lessons_learned', 'assigned_to',
];
foreach ($allowedFields as $field) {
@ -184,9 +197,24 @@ class IncidentController extends BaseController
}
}
// Se chiuso, registra data
if (isset($updates['status']) && $updates['status'] === 'closed') {
$updates['closed_at'] = date('Y-m-d H:i:s');
// Timbra automaticamente i timestamp di fase al primo ingresso nello stato
// (per il calcolo metriche TTD/TTC/TTR). Non sovrascrive valori gia' presenti.
if (isset($updates['status'])) {
$now = date('Y-m-d H:i:s');
$stamp = [
'analyzing' => 'triaged_at',
'containing' => 'contained_at',
'eradicating'=> 'eradicated_at',
'recovering' => 'recovered_at',
];
$col = $stamp[$updates['status']] ?? null;
if ($col !== null && empty($incident[$col])) {
$updates[$col] = $now;
}
if ($updates['status'] === 'closed') {
$updates['closed_at'] = $now;
if (empty($incident['recovered_at'])) $updates['recovered_at'] = $now;
}
}
// Se diventa significativo, calcola scadenze
@ -392,4 +420,127 @@ class IncidentController extends BaseController
$this->jsonError('Errore AI: ' . $e->getMessage(), 500, 'AI_ERROR');
}
}
/**
* GET /api/incidents/{id}/metrics
* Calcola TTD/TTC/TTR e downtime dai timestamp di fase (in minuti).
*/
public function metrics(int $id): void
{
$this->requireOrgAccess();
$inc = Database::fetchOne(
'SELECT detected_at, triaged_at, contained_at, eradicated_at, recovered_at, closed_at, affected_users_count
FROM incidents WHERE id = ? AND organization_id = ?',
[$id, $this->getCurrentOrgId()]
);
if (!$inc) {
$this->jsonError('Incidente non trovato', 404, 'INCIDENT_NOT_FOUND');
}
$this->jsonSuccess($this->computeMetrics($inc));
}
/** Differenza in minuti tra due datetime, null se mancante. */
private function minutesBetween(?string $from, ?string $to): ?int
{
if (empty($from) || empty($to)) return null;
$a = strtotime($from); $b = strtotime($to);
if ($a === false || $b === false) return null;
return (int) round(($b - $a) / 60);
}
private function computeMetrics(array $inc): array
{
$det = $inc['detected_at'] ?? null;
return [
'ttd_minutes' => $this->minutesBetween($det, $inc['triaged_at'] ?? null),
'ttc_minutes' => $this->minutesBetween($det, $inc['contained_at'] ?? null),
'ttr_minutes' => $this->minutesBetween($det, $inc['recovered_at'] ?? null),
'downtime_minutes' => $this->minutesBetween($det, $inc['recovered_at'] ?? $inc['closed_at'] ?? null),
'affected_users' => isset($inc['affected_users_count']) ? (int) $inc['affected_users_count'] : null,
];
}
/**
* GET /api/incidents/{id}/pir
* Ritorna la Post-Incident Review (RC.CO-03) con le metriche calcolate.
*/
public function getPir(int $id): void
{
$this->requireOrgAccess();
$inc = Database::fetchOne(
'SELECT * FROM incidents WHERE id = ? AND organization_id = ?',
[$id, $this->getCurrentOrgId()]
);
if (!$inc) {
$this->jsonError('Incidente non trovato', 404, 'INCIDENT_NOT_FOUND');
}
$pir = Database::fetchOne('SELECT * FROM incident_pir WHERE incident_id = ?', [$id]);
if ($pir && !empty($pir['improvement_actions'])) {
$pir['improvement_actions'] = json_decode($pir['improvement_actions'], true);
}
$this->jsonSuccess([
'pir' => $pir,
'metrics' => $this->computeMetrics($inc),
'reference' => 'RC.CO-03 (NIST CSF) - PIR da completare entro 2 settimane dalla chiusura per incidenti critici',
]);
}
/**
* POST /api/incidents/{id}/pir
* Crea o aggiorna la Post-Incident Review (upsert).
*/
public function savePir(int $id): void
{
$this->requireOrgRole(['org_admin', 'compliance_manager']);
$inc = Database::fetchOne(
'SELECT * FROM incidents WHERE id = ? AND organization_id = ?',
[$id, $this->getCurrentOrgId()]
);
if (!$inc) {
$this->jsonError('Incidente non trovato', 404, 'INCIDENT_NOT_FOUND');
}
$m = $this->computeMetrics($inc);
$actions = $this->getParam('improvement_actions');
$fields = [
'organization_id' => $this->getCurrentOrgId(),
'problem_statement' => $this->getParam('problem_statement'),
'why_1' => $this->getParam('why_1'), 'why_2' => $this->getParam('why_2'),
'why_3' => $this->getParam('why_3'), 'why_4' => $this->getParam('why_4'),
'why_5' => $this->getParam('why_5'),
'root_cause' => $this->getParam('root_cause'),
'ttd_minutes' => $m['ttd_minutes'],
'ttc_minutes' => $m['ttc_minutes'],
'ttr_minutes' => $m['ttr_minutes'],
'downtime_minutes' => $m['downtime_minutes'],
'affected_users' => $m['affected_users'],
'estimated_cost_eur' => $this->getParam('estimated_cost_eur'),
'notification_compliance' => $this->getParam('notification_compliance') !== null ? (int)(bool)$this->getParam('notification_compliance') : null,
'what_went_well' => $this->getParam('what_went_well'),
'what_to_improve' => $this->getParam('what_to_improve'),
'improvement_actions' => is_array($actions) ? json_encode($actions, JSON_UNESCAPED_UNICODE) : null,
'participants' => $this->getParam('participants'),
'reviewed_by' => $this->getCurrentUserId(),
'reviewed_at' => date('Y-m-d H:i:s'),
'status' => $this->getParam('status', 'draft'),
];
$existing = Database::fetchOne('SELECT id FROM incident_pir WHERE incident_id = ?', [$id]);
if ($existing) {
Database::update('incident_pir', $fields, 'incident_id = ?', [$id]);
$pirId = (int) $existing['id'];
} else {
$fields['incident_id'] = $id;
$pirId = Database::insert('incident_pir', $fields);
}
// Se la root cause e' definita, allineala anche all'incidente (campo legacy)
if (!empty($fields['root_cause'])) {
Database::update('incidents', ['root_cause' => $fields['root_cause']], 'id = ?', [$id]);
}
$this->logAudit('incident_pir_saved', 'incident', $id, ['pir_id' => $pirId, 'status' => $fields['status']]);
$this->jsonSuccess(['pir_id' => $pirId, 'metrics' => $m], 'Post-Incident Review salvata');
}
}

View File

@ -0,0 +1,329 @@
<?php
/**
* NIS2 Agile - Knowledge Base Controller
*
* Migration 012-014: KB multi-livello (SYSTEM/FIRM/ORG) con Qdrant + Voyage.
*
* Endpoint:
* POST /api/knowledgebase/ingest - Carica nuovo documento (testo)
* GET /api/knowledgebase/list - Lista documenti caricati visibili
* GET /api/knowledgebase/firm-orgs - Lista organizzazioni del firm dell'utente (per multi-select UI)
* POST /api/knowledgebase/search - Search semantica (debug/preview)
* DELETE /api/knowledgebase/document/{id}- Cancella documento + chunk Qdrant
*/
require_once __DIR__ . '/BaseController.php';
require_once APP_PATH . '/services/VectorService.php';
require_once APP_PATH . '/services/EmbedService.php';
require_once APP_PATH . '/services/RagService.php';
class KnowledgeBaseController extends BaseController
{
/**
* GET /api/knowledgebase/firm-orgs
* Restituisce le organizzazioni gestite dal firm dell'utente, per popolare la
* multi-select del form di upload (scope=FIRM).
*/
public function firmOrgs(): void
{
$this->requireAuth();
$firmId = $this->currentUser['consulting_firm_id'] ?? null;
if (!$firmId) {
$this->jsonSuccess(['organizations' => []]);
return;
}
$rows = Database::fetchAll(
'SELECT id, name, vat_number, sector
FROM organizations
WHERE consulting_firm_id = ? AND is_active = 1
ORDER BY name',
[(int)$firmId]
);
$this->jsonSuccess(['organizations' => $rows]);
}
/**
* POST /api/knowledgebase/ingest
* Body JSON:
* { title, text, entity_type?, source?, scope?, shared_with_orgs?, organization_id? }
*/
public function ingest(): void
{
$this->requireAuth();
$userId = (int)$this->currentUser['id'];
$userRole = $this->currentUser['role'] ?? '';
$userFirmId = $this->currentUser['consulting_firm_id'] ?? null;
// Solo questi ruoli possono uploadare. employee/auditor sono read-only.
$allowedUploadRoles = ['super_admin', 'org_admin', 'compliance_manager', 'consultant'];
if (!in_array($userRole, $allowedUploadRoles, true)) {
$this->jsonError('Ruolo non autorizzato a caricare documenti KB', 403, 'KB_FORBIDDEN');
}
$this->validateRequired(['title', 'text']);
$title = trim((string)$this->getParam('title'));
$text = (string)$this->getParam('text');
$entityType = $this->getParam('entity_type', 'custom');
$source = $this->getParam('source', $title);
$orgId = (int)$this->getParam('organization_id', 0);
$scope = strtoupper((string)$this->getParam('scope', 'SYSTEM'));
if (!in_array($scope, ['SYSTEM', 'FIRM', 'ORG'], true)) {
$scope = 'SYSTEM';
}
$sharedWith = $this->getParam('shared_with_orgs', []);
if (!is_array($sharedWith)) $sharedWith = [];
$sharedWith = array_values(array_filter(array_map('intval', $sharedWith)));
// Validazioni testo
$textLen = strlen($text);
if ($textLen < 50) {
$this->jsonError('Testo troppo breve (min 50 caratteri)', 422, 'TEXT_TOO_SHORT');
}
if ($textLen > 50000) {
$this->jsonError('Testo troppo lungo (max 50.000 caratteri)', 422, 'TEXT_TOO_LONG');
}
// Authorization per scope
if ($scope === 'SYSTEM' && !in_array($userRole, ['super_admin'], true)) {
$this->jsonError('Solo i super_admin possono caricare documenti SYSTEM', 403, 'KB_SYSTEM_FORBIDDEN');
}
if ($scope === 'FIRM') {
if (!$userFirmId) {
$this->jsonError('Solo i membri di uno studio possono caricare documenti FIRM', 403, 'KB_NO_FIRM');
}
// Verifica che le organizations di shared_with appartengano davvero al firm
if (!empty($sharedWith)) {
$placeholders = implode(',', array_fill(0, count($sharedWith), '?'));
$valid = Database::fetchAll(
"SELECT id FROM organizations WHERE id IN ($placeholders) AND consulting_firm_id = ?",
array_merge($sharedWith, [(int)$userFirmId])
);
$validIds = array_map(fn($r) => (int)$r['id'], $valid);
$invalid = array_diff($sharedWith, $validIds);
if (!empty($invalid)) {
$this->jsonError('Alcune organizzazioni non appartengono al tuo studio: ' . implode(',', $invalid), 403, 'KB_INVALID_SHARE');
}
$sharedWith = $validIds;
}
}
if ($scope === 'ORG') {
if ($orgId <= 0) {
$this->jsonError('organization_id obbligatorio per scope=ORG', 422, 'KB_ORG_REQUIRED');
}
// Verifica accesso dell'utente all'organization
if ($userRole !== 'super_admin') {
$access = Database::fetchOne(
"SELECT 1 FROM user_organizations WHERE user_id = ? AND organization_id = ? AND role IN ('org_admin','compliance_manager')",
[$userId, $orgId]
);
if (!$access) {
$this->jsonError('Non hai permessi di scrittura su questa organizzazione', 403, 'KB_ORG_FORBIDDEN');
}
}
}
// Chunking: ~2000 char con overlap 200
$chunks = $this->chunkText($text, 2000, 200);
try {
$embed = new EmbedService();
$vector = new VectorService();
$vector->ensureCollection($embed->dims);
$docUuid = $this->generateUuid();
$points = [];
foreach ($chunks as $i => $chunk) {
$vec = $embed->embed($chunk);
$points[] = [
'id' => $this->generateUuid(),
'vector' => $vec,
'payload' => [
'doc_uuid' => $docUuid,
'title' => $title . ($i > 0 ? ' (parte ' . ($i + 1) . ')' : ''),
'chunk' => $chunk,
'entity_type' => $entityType,
'source' => $source,
'lang' => 'it',
'scope' => $scope,
'consulting_firm_id' => $userFirmId !== null ? (int)$userFirmId : null,
'organization_id' => $orgId > 0 ? $orgId : null,
'shared_with_orgs' => $sharedWith,
'uploaded_by' => $userId,
],
];
}
$vector->upsertBatch($points);
} catch (Exception $e) {
$this->jsonError('Errore durante l\'indicizzazione: ' . $e->getMessage(), 500, 'KB_INGEST_ERROR');
}
// Tracking row in MySQL
try {
$stmt = Database::getInstance()->prepare(
"INSERT INTO kb_uploaded_documents
(qdrant_doc_uuid, scope, consulting_firm_id, organization_id, uploaded_by, title, entity_type, source, lang, chunk_count, shared_with_orgs, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'ready')"
);
$stmt->execute([
$docUuid,
$scope,
$userFirmId,
$orgId > 0 ? $orgId : null,
$userId,
$title,
$entityType,
$source,
'it',
count($chunks),
json_encode($sharedWith),
]);
} catch (Exception $e) {
error_log('[KB] kb_uploaded_documents insert failed: ' . $e->getMessage());
}
$this->jsonSuccess([
'doc_uuid' => $docUuid,
'title' => $title,
'scope' => $scope,
'chunks' => count($chunks),
'shared_with_orgs' => $sharedWith,
], 'Documento indicizzato');
}
/**
* GET /api/knowledgebase/list
* Lista i documenti che l'utente puo' vedere via il filtro authz.
* Note: lista basata su kb_uploaded_documents (audit), non su Qdrant.
*/
public function list(): void
{
$this->requireAuth();
$userId = (int)$this->currentUser['id'];
$firmId = $this->currentUser['consulting_firm_id'] ?? null;
$orgId = $this->resolveOrgId();
$where = [];
$params = [];
// SYSTEM sempre visibile
$clauses = ["scope = 'SYSTEM'"];
if ($firmId) {
$clauses[] = "(scope = 'FIRM' AND consulting_firm_id = ?)";
$params[] = (int)$firmId;
}
if ($orgId) {
$clauses[] = "(scope = 'FIRM' AND JSON_CONTAINS(shared_with_orgs, JSON_ARRAY(?)))";
$params[] = (int)$orgId;
$clauses[] = "(scope = 'ORG' AND organization_id = ?)";
$params[] = (int)$orgId;
}
$sql = 'SELECT id, qdrant_doc_uuid, scope, consulting_firm_id, organization_id, title, entity_type, source, lang, chunk_count, shared_with_orgs, status, created_at
FROM kb_uploaded_documents
WHERE ' . implode(' OR ', $clauses) . '
ORDER BY created_at DESC LIMIT 200';
$rows = Database::fetchAll($sql, $params);
// Decode shared_with_orgs JSON
foreach ($rows as &$r) {
if (!empty($r['shared_with_orgs'])) {
$r['shared_with_orgs'] = json_decode($r['shared_with_orgs'], true) ?: [];
} else {
$r['shared_with_orgs'] = [];
}
}
$this->jsonSuccess(['documents' => $rows]);
}
/**
* POST /api/knowledgebase/search
* Body: { query, top_k? }
* Search semantica preview (utile per debug e per UI "find similar").
*/
public function search(): void
{
$this->requireAuth();
$this->validateRequired(['query']);
$query = (string)$this->getParam('query');
$topK = (int)$this->getParam('top_k', 5);
$userContext = [
'user_id' => (int)$this->currentUser['id'],
'organization_id' => $this->resolveOrgId(),
'consulting_firm_id' => $this->currentUser['consulting_firm_id'] ?? null,
];
try {
$rag = new RagService();
$hits = $rag->searchForUser($query, $userContext, $topK);
$this->jsonSuccess(['results' => $hits]);
} catch (Exception $e) {
$this->jsonError('Errore search: ' . $e->getMessage(), 500, 'KB_SEARCH_ERROR');
}
}
/**
* DELETE /api/knowledgebase/document/{id}
* Cancella documento + tutti i chunk Qdrant via doc_uuid.
*/
public function delete(int $id): void
{
$this->requireAuth();
$userRole = $this->currentUser['role'] ?? '';
$userId = (int)$this->currentUser['id'];
$doc = Database::fetchOne('SELECT * FROM kb_uploaded_documents WHERE id = ?', [$id]);
if (!$doc) {
$this->jsonError('Documento non trovato', 404, 'KB_NOT_FOUND');
}
// Solo l'uploader o un super_admin puo' cancellare
if ($userRole !== 'super_admin' && (int)$doc['uploaded_by'] !== $userId) {
$this->jsonError('Non autorizzato a cancellare questo documento', 403, 'KB_DELETE_FORBIDDEN');
}
try {
$vector = new VectorService();
$vector->deleteByFilter([
'must' => [
['key' => 'doc_uuid', 'match' => ['value' => $doc['qdrant_doc_uuid']]],
],
]);
} catch (Exception $e) {
error_log('[KB] qdrant delete failed: ' . $e->getMessage());
}
Database::query('DELETE FROM kb_uploaded_documents WHERE id = ?', [$id]);
$this->jsonSuccess(null, 'Documento eliminato');
}
// ─── helpers ─────────────────────────────────────────
private function chunkText(string $text, int $chunkSize = 2000, int $overlap = 200): array
{
$chunks = [];
$length = strlen($text);
$start = 0;
while ($start < $length) {
$end = min($start + $chunkSize, $length);
$chunks[] = substr($text, $start, $end - $start);
if ($end >= $length) break;
$start = $end - $overlap;
}
return $chunks;
}
private function generateUuid(): string
{
$data = random_bytes(16);
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}
// resolveOrgId() e' ereditato da BaseController (riga 351)
}

View File

@ -13,9 +13,12 @@ require_once APP_PATH . '/services/EmailService.php';
class MktgLeadController extends BaseController
{
private const WEBHOOK_URL = 'https://mktg.agile.software/api/webhook/leads';
private const WEBHOOK_KEY = 'wh_nis2_2026_c1d2e3f4a5b6c7d8';
// AgileHub CRM (mktg.agile.software dismesso 2026-04-19)
private const WEBHOOK_URL = 'https://agilehub.agile.software/api/leads/webhook';
private const WEBHOOK_KEY = 'agilehub_nis2_2026';
private const TENANT_ID = '7';
private const PRODUCT = 'NIS2 Agile';
private const PRODUCT_CODE = 'NIS2';
private const SOURCE = 'nis2-landing';
private const NOTIFY_EMAIL = 'info@agile.software';
@ -93,6 +96,8 @@ class MktgLeadController extends BaseController
'email' => $email,
'phone' => $phone,
'company' => $company,
'product' => self::PRODUCT_CODE,
'productLabel' => self::PRODUCT,
'product_interest' => $interest ?: self::PRODUCT,
'source' => $source,
'notes' => $fullNotes,
@ -118,6 +123,7 @@ class MktgLeadController extends BaseController
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-Webhook-Key: ' . self::WEBHOOK_KEY,
'X-Tenant-Id: ' . self::TENANT_ID,
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 8,

View File

@ -28,6 +28,32 @@ class AIService
}
}
/**
* Blocco "fonti certe" da iniettare nei system prompt.
* Elenca le fonti normative autoritative e impone di citarle, vietando
* riferimenti inventati. (Richiesta utente 2026-05-29 - grounding su fonti certe.)
*/
private function authoritativeSourcesBlock(): string
{
static $sources = null;
if ($sources === null) {
$sources = @include __DIR__ . '/../config/nis2_sources.php';
if (!is_array($sources)) $sources = [];
}
if (empty($sources)) return '';
$lines = [];
foreach ($sources as $s) {
$lines[] = '- ' . $s['citation'] . ' — ' . $s['authority'];
}
return "\n## FONTI NORMATIVE CERTE (cita SEMPRE quella pertinente)\n"
. implode("\n", $lines)
. "\n\nREGOLE SULLE FONTI (vincolanti):\n"
. "1. Ogni affermazione normativa DEVE essere ancorata a una di queste fonti, citata esplicitamente (es. \"ai sensi dell'art. 23 della Direttiva (UE) 2022/2555\" o \"Determinazione ACN n. 164179/2025, Allegato 3\").\n"
. "2. NON inventare numeri di articolo, determine, allegati o date: se non sei certo, dichiaralo e invita a verificare la fonte ufficiale.\n"
. "3. Preferisci sempre il riferimento normativo italiano (D.Lgs. 138/2024 + Determine ACN) per gli obblighi operativi, e la Direttiva UE per i principi.\n";
}
/**
* Analizza risultati gap analysis e genera raccomandazioni
*/
@ -162,8 +188,23 @@ PROMPT;
*/
public function classifyIncident(string $title, string $description, array $organization): array
{
$entityType = $organization['entity_type'] ?? 'important';
// Allegato 3 (soggetti essenziali) vs Allegato 4 (soggetti importanti):
// gli importanti NON hanno l'obbligo sugli incidenti ricorrenti (IS-4).
$isEssential = ($entityType === 'essential');
$allowedIs = $isEssential ? 'IS-1|IS-2|IS-3|IS-4' : 'IS-1|IS-2|IS-3';
$allegato = $isEssential ? 'Allegato 3 (soggetti essenziali)' : 'Allegato 4 (soggetti importanti)';
$sourcesBlock = $this->authoritativeSourcesBlock();
$prompt = <<<PROMPT
Sei un analista di incident response. Classifica il seguente incidente di sicurezza secondo i criteri NIS2.
Sei un analista di incident response NIS2. Classifica il seguente incidente secondo il quadro normativo italiano.
## Quadro normativo di riferimento (cita la fonte pertinente in ogni campo motivazionale)
- Obbligo di notifica: art. 23 D.Lgs. 138/2024 e Direttiva (UE) 2022/2555.
- Classificazione incidenti significativi e tempistiche: Determinazione ACN n. 164179/2025, {$allegato}.
- Tempistiche: preallarme entro 24h, notifica completa entro 72h, relazione finale entro 1 mese dalla conoscenza dell'incidente significativo.
- Tipologie significative applicabili a questo soggetto ({$entityType}): {$allowedIs}.
{$sourcesBlock}
## Incidente
- Titolo: {$title}
@ -171,21 +212,23 @@ Sei un analista di incident response. Classifica il seguente incidente di sicure
## Organizzazione
- Settore: {$organization['sector']}
- Tipo entità: {$organization['entity_type']}
- Tipo entità: {$entityType}
Rispondi in formato JSON:
{
"classification": "cyber_attack|data_breach|system_failure|human_error|natural_disaster|supply_chain|other",
"nis2_incident_type": "{$allowedIs}|none",
"severity": "low|medium|high|critical",
"is_significant": true/false,
"significance_reason": "Motivo se significativo secondo NIS2",
"significance_reason": "Motivo con citazione esplicita della fonte (es. Determina ACN 164179/2025, {$allegato})",
"requires_csirt_notification": true/false,
"notification_basis": "Riferimento normativo dell'obbligo (es. art. 23 D.Lgs. 138/2024)",
"suggested_actions": ["Azione immediata 1", "Azione immediata 2"],
"potential_impact": "Descrizione impatto potenziale",
"iocs_to_check": ["Indicatore 1", "Indicatore 2"]
}
Rispondi SOLO con il JSON.
Non inventare riferimenti: usa solo le fonti elencate sopra. Rispondi SOLO con il JSON.
PROMPT;
$response = $this->callAPI($prompt);
@ -219,6 +262,12 @@ Rispondi sempre in italiano, in modo professionale, preciso e conciso.
Non includere dati identificativi dell'organizzazione nelle risposte.
SYSTEM;
// Grounding su fonti certe: applicato solo al prompt di default
// (i system prompt espliciti gestiscono le fonti per conto proprio).
if ($systemPrompt === null) {
$system .= "\n" . $this->authoritativeSourcesBlock();
}
$body = [
'model' => $this->model,
'max_tokens' => $this->maxTokens,
@ -564,4 +613,59 @@ PROMPT;
'model_used' => $this->model,
]);
}
/**
* Migration 012-014: Q&A grounded sulla KB multi-livello.
*
* Esegue una RAG search sui documenti visibili all'utente (SYSTEM/FIRM/ORG)
* e inietta i top-K chunks nel system prompt prima di chiamare Claude.
*
* Se Voyage/Qdrant non sono disponibili, ricade su Claude diretto senza grounding.
*
* @param string $question Domanda dell'utente
* @param array $userContext ['user_id', 'organization_id', 'consulting_firm_id']
* @return array ['answer'=>string, 'sources'=>array, 'rag_used'=>bool]
*/
public function askWithRag(string $question, array $userContext): array
{
$sources = [];
$contextBlock = '';
$ragUsed = false;
// Tenta RAG: se fallisce, prosegui senza grounding (degradazione graceful)
try {
require_once __DIR__ . '/RagService.php';
$rag = new RagService();
$hits = $rag->searchForUser($question, $userContext, 5, 0.28);
if (!empty($hits)) {
$contextBlock = $rag->formatContext($hits);
$sources = array_map(fn($h) => [
'title' => $h['title'],
'scope' => $h['scope'],
'score' => $h['score'],
], $hits);
$ragUsed = true;
}
} catch (Exception $e) {
error_log('[AIService::askWithRag] RAG failed, fallback diretto: ' . $e->getMessage());
}
$systemPrompt = "Sei un esperto consulente di cybersecurity NIS2 (EU 2022/2555) e D.Lgs. 138/2024.\n"
. "Rispondi in modo preciso e cita le fonti del contesto quando rilevanti.\n"
. $this->authoritativeSourcesBlock();
if (!empty($contextBlock)) {
$systemPrompt .= "\n## Contesto documentale (knowledge base)\n" . $contextBlock
. "\n\nQuando rispondi, cita esplicitamente i numeri tra parentesi quadre [1], [2], ... che corrispondono ai documenti del contesto.";
} else {
$systemPrompt .= "\nNon e' disponibile contesto documentale specifico per questa domanda. Rispondi con la tua conoscenza generale e indica esplicitamente che non hai trovato fonti nella knowledge base.";
}
$answer = $this->callAPI($question, $systemPrompt);
return [
'answer' => $answer,
'sources' => $sources,
'rag_used' => $ragUsed,
];
}
}

View File

@ -0,0 +1,202 @@
<?php
/**
* NIS2 Agile - Asset Scoring Service
*
* Metodologia di scoring rilevanza NIS2 (requisito GV.OC-04).
* Adattata dai mockup docs/nis2/assets.html + doc-relevant-systems.html.
*
* 6 criteri pesati, punteggio 0-100:
* C1 Criticita Operativa 0-25
* C2 Impatto Interruzione 0-25
* C3 Dati Trattati 0-20
* C4 Dipendenze 0-15
* C5 Esposizione 0-10
* C6 Obblighi Normativi 0-5
*
* Soglia rilevanza NIS2: score >= 40.
* Classi: >=80 critico | 60-79 alto | 40-59 medio | 20-39 basso | <20 trascurabile.
*
* La logica e' PURA (nessun side effect / DB): si presta a unit test e riuso.
*/
class AssetScoringService
{
public const RELEVANCE_THRESHOLD = 40;
/**
* Griglia ufficiale: per ogni criterio, la lista di opzioni selezionabili
* (value => punti) con label per la UI. value e' una chiave stabile usata
* dal frontend e salvata in relevance_criteria JSON.
*/
public const GRID = [
'c1_operational_criticality' => [
'label' => 'Criticita Operativa',
'max' => 25,
'help' => 'Quanto il sistema e essenziale per l\'erogazione dei servizi core business.',
'options' => [
'critical' => ['label' => 'Critico', 'points' => 25],
'very_high' => ['label' => 'Molto Alto', 'points' => 20],
'high' => ['label' => 'Alto', 'points' => 15],
'medium' => ['label' => 'Medio', 'points' => 10],
'low' => ['label' => 'Basso', 'points' => 5],
'negligible' => ['label' => 'Trascurabile', 'points' => 0],
],
],
'c2_disruption_impact' => [
'label' => 'Impatto Interruzione',
'max' => 25,
'help' => 'Conseguenze di un\'interruzione in termini di durata e utenti impattati.',
'options' => [
'gt24h_gt70' => ['label' => '>24h + >70% utenti', 'points' => 25],
'h8_24_50_70' => ['label' => '8-24h + 50-70% utenti', 'points' => 20],
'h4_8_30_50' => ['label' => '4-8h + 30-50% utenti', 'points' => 15],
'h1_4_10_30' => ['label' => '1-4h + 10-30% utenti', 'points' => 10],
'lt1h_lt10' => ['label' => '<1h + <10% utenti', 'points' => 5],
'none' => ['label' => 'Nessun impatto', 'points' => 0],
],
],
'c3_data_processed' => [
'label' => 'Dati Trattati',
'max' => 20,
'help' => 'Sensibilita e criticita dei dati gestiti dal sistema.',
'options' => [
'gdpr_art9' => ['label' => 'Dati Sensibili Art.9 GDPR', 'points' => 20],
'personal_large' => ['label' => 'Dati Personali larga scala', 'points' => 15],
'personal_fin' => ['label' => 'Dati Personali + Finanziari', 'points' => 10],
'confidential' => ['label' => 'Dati Aziendali Riservati', 'points' => 5],
'public' => ['label' => 'Dati Pubblici', 'points' => 0],
],
],
'c4_dependencies' => [
'label' => 'Dipendenze',
'max' => 15,
'help' => 'Quanti altri sistemi critici dipendono da questo sistema.',
'options' => [
'ge5_critical' => ['label' => '>=5 sistemi critici', 'points' => 15],
'n3_4_critical' => ['label' => '3-4 sistemi critici', 'points' => 12],
'n2_critical' => ['label' => '2 sistemi critici', 'points' => 9],
'n1_critical' => ['label' => '1 sistema critico', 'points' => 6],
'noncritical' => ['label' => '1-2 sistemi non critici', 'points' => 3],
'none' => ['label' => 'Nessuna dipendenza', 'points' => 0],
],
],
'c5_exposure' => [
'label' => 'Esposizione',
'max' => 10,
'help' => 'Superficie di attacco ed esposizione del sistema.',
'options' => [
'internet_no_mfa' => ['label' => 'Internet pubblico senza MFA', 'points' => 10],
'internet_mfa' => ['label' => 'Internet con MFA', 'points' => 8],
'partner_net' => ['label' => 'Reti partner/fornitori', 'points' => 6],
'intranet' => ['label' => 'Rete aziendale intranet', 'points' => 4],
'mgmt_isolated' => ['label' => 'Rete gestione isolata', 'points' => 2],
'air_gapped' => ['label' => 'Completamente isolato', 'points' => 0],
],
],
'c6_regulatory' => [
'label' => 'Obblighi Normativi',
'max' => 5,
'help' => 'Se il sistema e soggetto a obblighi normativi o contrattuali specifici.',
'options' => [
'nis2_required' => ['label' => 'Richiesto da NIS2', 'points' => 5],
'mandatory_cert' => ['label' => 'Certificazioni obbligatorie', 'points' => 4],
'strict_sla' => ['label' => 'Obblighi SLA stringenti', 'points' => 3],
'external_audit' => ['label' => 'Audit esterni regolari', 'points' => 2],
'none' => ['label' => 'Nessun obbligo', 'points' => 0],
],
],
];
/**
* Calcola lo score a partire dalle selezioni dell'utente.
*
* @param array $criteria mappa criterioKey => optionValue
* es. ['c1_operational_criticality' => 'critical', ...]
* @return array{score:int, class:string, is_relevant:bool, breakdown:array, criticality:string}
* @throws InvalidArgumentException se un criterio/opzione non e valido
*/
public static function calculate(array $criteria): array
{
$score = 0;
$breakdown = [];
foreach (self::GRID as $key => $def) {
if (!array_key_exists($key, $criteria)) {
throw new InvalidArgumentException("Criterio mancante: {$key}");
}
$optVal = $criteria[$key];
if (!isset($def['options'][$optVal])) {
throw new InvalidArgumentException("Opzione non valida '{$optVal}' per criterio {$key}");
}
$pts = $def['options'][$optVal]['points'];
$score += $pts;
$breakdown[$key] = [
'value' => $optVal,
'label' => $def['options'][$optVal]['label'],
'points' => $pts,
'max' => $def['max'],
];
}
$class = self::classify($score);
return [
'score' => $score,
'class' => $class,
'is_relevant' => $score >= self::RELEVANCE_THRESHOLD,
'breakdown' => $breakdown,
// mapping verso l'enum legacy assets.criticality per coerenza UI esistente
'criticality' => self::toLegacyCriticality($score),
];
}
/** Classe testuale secondo le soglie ufficiali. */
public static function classify(int $score): string
{
if ($score >= 80) return 'critico';
if ($score >= 60) return 'alto';
if ($score >= 40) return 'medio';
if ($score >= 20) return 'basso';
return 'trascurabile';
}
/** Allinea lo score all'enum assets.criticality preesistente (low/medium/high/critical). */
public static function toLegacyCriticality(int $score): string
{
if ($score >= 80) return 'critical';
if ($score >= 60) return 'high';
if ($score >= 40) return 'medium';
return 'low';
}
/** Misure obbligatorie associate alla classe (per report GV.OC-04 / UI). */
public static function requiredMeasures(string $class): array
{
return [
'critico' => [
'Monitoraggio continuo 24/7 (SIEM/SOC)',
'Backup immutabile con test ripristino periodico',
'MFA obbligatoria + accessi privilegiati controllati (PAM)',
'Inclusione obbligatoria in BIA e piano di continuita',
'Test di vulnerabilita/penetration test almeno annuali',
],
'alto' => [
'Monitoraggio in orario lavorativo + alerting',
'Backup regolari con verifica integrita',
'MFA per accessi remoti',
'Inclusione in risk assessment ciclico',
],
'medio' => [
'Logging centralizzato',
'Backup periodici',
'Patch management documentato',
],
'basso' => [
'Inventario aggiornato',
'Patch management standard',
],
'trascurabile' => [
'Censimento in inventario asset',
],
][$class] ?? [];
}
}

View File

@ -0,0 +1,66 @@
<?php
/**
* NIS2 Agile - EmbedService
*
* Client minimale per Voyage AI embeddings (voyage-3-lite, 1024 dim).
*/
class EmbedService
{
public int $dims = 512;
private string $apiKey;
private string $model;
public function __construct()
{
// PHP-FPM Alpine non popola env via getenv() (clear_env). Multi-source lookup.
// La chiave vive in .env (gitignored) + vault-steward; nessun segreto hardcoded.
$this->apiKey = getenv('VOYAGE_API_KEY')
?: ($_SERVER['VOYAGE_API_KEY'] ?? '')
?: ($_ENV['VOYAGE_API_KEY'] ?? '')
?: (class_exists('Env') ? Env::get('VOYAGE_API_KEY', '') : '');
$this->model = getenv('VOYAGE_MODEL')
?: ($_SERVER['VOYAGE_MODEL'] ?? null)
?: ($_ENV['VOYAGE_MODEL'] ?? null)
?: 'voyage-3-lite';
if (empty($this->apiKey)) {
throw new RuntimeException('VOYAGE_API_KEY non configurata');
}
}
/**
* @return float[] Vettore embedding 1024-dim
*/
public function embed(string $text): array
{
$ch = curl_init('https://api.voyageai.com/v1/embeddings');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $this->apiKey,
],
CURLOPT_POSTFIELDS => json_encode([
'input' => [$text],
'model' => $this->model,
'input_type' => 'document',
'output_dimension' => 512,
]),
CURLOPT_TIMEOUT => 30,
]);
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200 || !$raw) {
throw new RuntimeException("Voyage embed failed (HTTP $status): " . substr((string)$raw, 0, 200));
}
$data = json_decode($raw, true);
$vec = $data['data'][0]['embedding'] ?? null;
if (!is_array($vec)) {
throw new RuntimeException('Voyage response without embedding: ' . substr($raw, 0, 200));
}
return $vec;
}
}

View File

@ -0,0 +1,64 @@
<?php
/**
* NIS2 Agile - RagService
*
* Combina EmbedService + VectorService per cercare nella KB multi-livello
* filtrando per il contesto utente (Migration 012-014).
*/
require_once __DIR__ . '/VectorService.php';
require_once __DIR__ . '/EmbedService.php';
class RagService
{
private VectorService $vector;
private EmbedService $embed;
public function __construct()
{
$this->vector = new VectorService();
$this->embed = new EmbedService();
}
/**
* Cerca i top-k chunks visibili all'utente.
*
* @param array $userContext ['user_id', 'organization_id', 'consulting_firm_id']
* @return array Lista chunks con title, content, score, scope
*/
public function searchForUser(string $question, array $userContext, int $topK = 5, float $minScore = 0.28): array
{
$vector = $this->embed->embed($question);
$filter = VectorService::buildAuthzFilter($userContext);
$hits = $this->vector->search($vector, $filter, $topK, $minScore);
$out = [];
foreach ($hits as $h) {
$p = $h['payload'] ?? [];
$out[] = [
'id' => $h['id'] ?? null,
'score' => round($h['score'] ?? 0, 4),
'title' => $p['title'] ?? '',
'content' => $p['chunk'] ?? '',
'scope' => $p['scope'] ?? null,
'source' => $p['source'] ?? null,
'lang' => $p['lang'] ?? 'it',
];
}
return $out;
}
/**
* Compatta i risultati in un blocco di testo da iniettare nel system prompt Claude.
*/
public function formatContext(array $hits): string
{
if (empty($hits)) return '';
$blocks = [];
foreach ($hits as $i => $h) {
$idx = $i + 1;
$blocks[] = "[$idx] {$h['title']} (scope={$h['scope']}, score={$h['score']})\n{$h['content']}";
}
return implode("\n\n---\n\n", $blocks);
}
}

View File

@ -255,6 +255,118 @@ class ReportService
* @param int $orgId ID dell'organizzazione
* @return string Documento HTML completo
*/
/**
* Registro formale "Elenco Sistemi Rilevanti NIS2" (requisito GV.OC-04).
* HTML stampabile (PDF via stampa browser). Cita le fonti normative certe.
* Adattato dal mockup docs/nis2/doc-relevant-systems.html.
*/
public function generateRelevantSystemsRegister(int $orgId): string
{
$org = Database::fetchOne('SELECT name, sector, entity_type FROM organizations WHERE id = ?', [$orgId]);
if (!$org) {
return $this->buildErrorHtml('Organizzazione non trovata');
}
$systems = Database::fetchAll(
"SELECT a.name, a.asset_type, a.category, a.ip_address, a.location,
a.relevance_score, a.relevance_class, a.relevance_assessed_at,
u.full_name AS owner_name
FROM assets a
LEFT JOIN users u ON u.id = a.owner_user_id
WHERE a.organization_id = ? AND a.is_nis2_relevant = 1
ORDER BY a.relevance_score DESC, a.name",
[$orgId]
);
$orgName = htmlspecialchars($org['name'] ?? '', ENT_QUOTES, 'UTF-8');
$entity = htmlspecialchars($org['entity_type'] ?? '', ENT_QUOTES, 'UTF-8');
$sector = htmlspecialchars($org['sector'] ?? '', ENT_QUOTES, 'UTF-8');
$date = date('d/m/Y');
$appName = defined('APP_NAME') ? APP_NAME : 'NIS2 Agile';
$appVer = defined('APP_VERSION') ? APP_VERSION : '1.0.0';
$classColors = ['critico' => '#dc2626', 'alto' => '#ea580c', 'medio' => '#ca8a04'];
$counts = ['critico' => 0, 'alto' => 0, 'medio' => 0];
$rowsHtml = '';
if (empty($systems)) {
$rowsHtml = '<tr><td colspan="7" style="text-align:center;color:#888;padding:24px;">Nessun sistema classificato come rilevante (score &ge; 40). Eseguire la valutazione di rilevanza dal modulo Inventario Asset.</td></tr>';
} else {
$n = 0;
foreach ($systems as $s) {
$n++;
$cls = $s['relevance_class'] ?? 'medio';
if (isset($counts[$cls])) $counts[$cls]++;
$color = $classColors[$cls] ?? '#6b7280';
$rowsHtml .= '<tr>'
. '<td>' . $n . '</td>'
. '<td><strong>' . htmlspecialchars($s['name'], ENT_QUOTES, 'UTF-8') . '</strong></td>'
. '<td>' . htmlspecialchars(($s['asset_type'] ?? '') . ($s['category'] ? ' / ' . $s['category'] : ''), ENT_QUOTES, 'UTF-8') . '</td>'
. '<td>' . htmlspecialchars($s['ip_address'] ?: '-', ENT_QUOTES, 'UTF-8') . '</td>'
. '<td>' . htmlspecialchars($s['owner_name'] ?: '-', ENT_QUOTES, 'UTF-8') . '</td>'
. '<td style="text-align:center;font-weight:700;">' . (int) $s['relevance_score'] . '/100</td>'
. '<td style="color:' . $color . ';font-weight:700;text-transform:uppercase;">' . htmlspecialchars($cls, ENT_QUOTES, 'UTF-8') . '</td>'
. '</tr>';
}
}
$total = count($systems);
return <<<HTML
<!DOCTYPE html>
<html lang="it"><head><meta charset="utf-8">
<title>Elenco Sistemi Rilevanti NIS2 - {$orgName}</title>
<style>
body{font-family:'Segoe UI',Arial,sans-serif;color:#1f2937;margin:40px;font-size:13px;}
.conf{background:#7f1d1d;color:#fff;text-align:center;padding:8px;font-weight:700;letter-spacing:1px;border-radius:6px;margin-bottom:18px;}
h1{font-size:22px;margin:0 0 4px;} h2{font-size:15px;border-bottom:2px solid #06b6d4;padding-bottom:4px;margin-top:28px;}
.meta{color:#6b7280;font-size:12px;margin-bottom:18px;}
table{width:100%;border-collapse:collapse;margin-top:10px;} th,td{border:1px solid #e5e7eb;padding:7px 9px;text-align:left;}
th{background:#f0f9ff;font-size:12px;} .stats{display:flex;gap:14px;margin:14px 0;}
.stat{flex:1;border:1px solid #e5e7eb;border-radius:8px;padding:10px;text-align:center;}
.stat .v{font-size:22px;font-weight:800;} .src{background:#f9fafb;border-left:4px solid #06b6d4;padding:10px 14px;font-size:11.5px;color:#374151;margin-top:24px;border-radius:0 6px 6px 0;}
.sign{margin-top:36px;display:flex;justify-content:space-between;} .sign div{width:45%;border-top:1px solid #9ca3af;padding-top:6px;font-size:12px;color:#4b5563;}
@media print{body{margin:12mm;} .noprint{display:none;}}
</style></head><body>
<div class="conf">&#9888; DOCUMENTO RISERVATO - DISTRIBUZIONE LIMITATA &#9888;</div>
<h1>{$orgName}</h1>
<h1 style="font-size:18px;color:#0e7490;">Elenco Formale dei Sistemi Rilevanti NIS2</h1>
<div class="meta">Documento ai sensi della Direttiva (UE) 2022/2555 e del D.Lgs. 138/2024 &mdash; Requisito GV.OC-04 (NIST CSF 2.0)<br>
Settore: {$sector} &nbsp;|&nbsp; Tipo soggetto: {$entity} &nbsp;|&nbsp; Data emissione: {$date} &nbsp;|&nbsp; Generato da {$appName} v{$appVer}</div>
<h2>1. Premessa e metodologia</h2>
<p>Il presente documento costituisce l'elenco formale dei sistemi informativi e di rete classificati come <strong>rilevanti</strong> ai fini della conformita alla Direttiva NIS2. La classificazione adotta una metodologia di scoring 0-100 su sei criteri (Criticita Operativa, Impatto Interruzione, Dati Trattati, Dipendenze, Esposizione, Obblighi Normativi). <strong>Soglia di rilevanza: punteggio &ge; 40.</strong> I sistemi con punteggio &ge; 80 sono considerati critici e richiedono misure di sicurezza massime e monitoraggio continuo.</p>
<div class="stats">
<div class="stat"><div class="v">{$total}</div>Sistemi rilevanti</div>
<div class="stat"><div class="v" style="color:#dc2626;">{$counts['critico']}</div>Critici (&ge;80)</div>
<div class="stat"><div class="v" style="color:#ea580c;">{$counts['alto']}</div>Alti (60-79)</div>
<div class="stat"><div class="v" style="color:#ca8a04;">{$counts['medio']}</div>Medi (40-59)</div>
</div>
<h2>2. Elenco sistemi rilevanti</h2>
<table>
<thead><tr><th>#</th><th>Sistema</th><th>Tipo/Categoria</th><th>IP</th><th>Responsabile</th><th>Punteggio</th><th>Classe</th></tr></thead>
<tbody>{$rowsHtml}</tbody>
</table>
<div class="src">
<strong>Fonti normative certe:</strong><br>
&bull; Direttiva (UE) 2022/2555 (NIS2) &mdash; Parlamento europeo e Consiglio UE<br>
&bull; D.Lgs. 4 settembre 2024, n. 138 &mdash; recepimento NIS2 (artt. 23, 24)<br>
&bull; NIST Cybersecurity Framework 2.0 &mdash; controllo GV.OC-04 (elenco sistemi rilevanti)
</div>
<div class="sign">
<div>Redatto da (CISO / Responsabile Compliance)</div>
<div>Approvato da (Organo di gestione / Direzione)</div>
</div>
<p class="noprint" style="margin-top:24px;"><button onclick="window.print()">Stampa / Salva PDF</button></p>
</body></html>
HTML;
}
public function generateExecutiveReport(int $orgId): string
{
$data = $this->generateComplianceReport($orgId);

View File

@ -0,0 +1,158 @@
<?php
/**
* NIS2 Agile - VectorService
*
* Client minimale Qdrant + filtro multi-livello (Migration 012-014).
* Modello a 3 livelli (SYSTEM/FIRM/ORG) coerente con TRPG e SustainAI.
*/
class VectorService
{
private string $qdrantUrl;
private string $collection;
public function __construct(string $collection = 'nis2_kb')
{
// PHP-FPM Alpine non popola correttamente env via getenv() (clear_env non
// applicato + bug DNS musl per hostname senza dots). Workaround: leggi da
// multiple sources e ricadi su IP statico del container Qdrant.
$url = getenv('QDRANT_URL')
?: ($_SERVER['QDRANT_URL'] ?? null)
?: ($_ENV['QDRANT_URL'] ?? null)
?: 'http://nis2-qdrant:6333'; // hostname Qdrant (agg. 2026-05-29): drift-proof. L'IP hardcoded .5 era driftato a .3; con clear_env=no php-fpm eredita QDRANT_URL e risolve l'hostname via Docker DNS (CLI verificato 200). Evita ricorrenze del drift IP.
$this->qdrantUrl = rtrim($url, '/');
$this->collection = $collection;
}
public function ensureCollection(int $dims = 1024): void
{
$info = $this->request('GET', "/collections/{$this->collection}");
if ($info['status'] === 200) return;
$this->request('PUT', "/collections/{$this->collection}", [
'vectors' => ['size' => $dims, 'distance' => 'Cosine'],
]);
}
public function upsertBatch(array $points): void
{
if (empty($points)) return;
$resp = $this->request('PUT', "/collections/{$this->collection}/points?wait=true", [
'points' => $points,
]);
if ($resp['status'] !== 200) {
throw new RuntimeException('Qdrant upsert failed (HTTP ' . $resp['status'] . '): ' . json_encode($resp['body']));
}
}
public function deleteByFilter(array $filter): void
{
$this->request('POST', "/collections/{$this->collection}/points/delete", [
'filter' => $filter,
]);
}
public function setPayloadByFilter(array $payload, array $filter): void
{
$this->request('POST', "/collections/{$this->collection}/points/payload", [
'payload' => $payload,
'filter' => $filter,
]);
}
public function search(array $vector, array $filter = [], int $limit = 8, float $minScore = 0.28): array
{
$body = [
'vector' => $vector,
'limit' => $limit,
'with_payload' => true,
'score_threshold'=> $minScore,
];
if (!empty($filter)) {
$body['filter'] = $filter;
}
$resp = $this->request('POST', "/collections/{$this->collection}/points/search", $body);
if ($resp['status'] !== 200) {
return [];
}
return $resp['body']['result'] ?? [];
}
/**
* Filtro a 3 livelli (SYSTEM/FIRM/ORG) basato sull'utente.
* Restituisce SOLO chunks visibili a quell'utente.
*
* @param array $userContext ['user_id'=>int, 'organization_id'=>int|null, 'consulting_firm_id'=>int|null]
*/
public static function buildAuthzFilter(array $userContext): array
{
$firmId = isset($userContext['consulting_firm_id']) && $userContext['consulting_firm_id'] !== null
? (int)$userContext['consulting_firm_id'] : null;
$orgId = isset($userContext['organization_id']) && $userContext['organization_id'] !== null
? (int)$userContext['organization_id'] : null;
$should = [];
// L0 SYSTEM: vendor knowledge (sempre visibile)
$should[] = ['key' => 'scope', 'match' => ['value' => 'SYSTEM']];
// L1 FIRM: KB del proprio studio (visibile a tutti i collaboratori)
if ($firmId !== null) {
$should[] = [
'must' => [
['key' => 'scope', 'match' => ['value' => 'FIRM']],
['key' => 'consulting_firm_id', 'match' => ['value' => $firmId]],
],
];
}
if ($orgId !== null) {
// L1 FIRM con sharing esplicito alla organization corrente
$should[] = [
'must' => [
['key' => 'scope', 'match' => ['value' => 'FIRM']],
['key' => 'shared_with_orgs', 'match' => ['any' => [$orgId]]],
],
];
// L2 ORG: chunk dell'organizzazione corrente
$should[] = [
'must' => [
['key' => 'scope', 'match' => ['value' => 'ORG']],
['key' => 'organization_id', 'match' => ['value' => $orgId]],
],
];
}
return ['should' => $should];
}
/**
* @return array{status:int, body:array}
*/
private function request(string $method, string $path, ?array $body = null): array
{
$url = $this->qdrantUrl . $path;
$ch = curl_init($url);
$opts = [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_CONNECTTIMEOUT => 3,
CURLOPT_TIMEOUT => 30,
];
if ($body !== null) {
$opts[CURLOPT_POSTFIELDS] = json_encode($body);
}
curl_setopt_array($ch, $opts);
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return [
'status' => $status,
'body' => $raw ? (json_decode($raw, true) ?? []) : [],
];
}
}

View File

@ -13,6 +13,7 @@ services:
- ../application:/var/www/nis2-agile/application
- ../public:/var/www/nis2-agile/public
- nis2-uploads:/var/www/nis2-agile/public/uploads
- /opt/devenv/scripts/vault-entrypoint.sh:/usr/local/bin/vault-entrypoint.sh:ro
env_file:
- ../.env
environment:
@ -25,8 +26,18 @@ services:
- DB_PASS=${DB_PASS}
- JWT_SECRET=${JWT_SECRET}
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
- VOYAGE_API_KEY=${VOYAGE_API_KEY}
- VOYAGE_MODEL=${VOYAGE_MODEL:-voyage-3-lite}
- QDRANT_URL=http://nis2-qdrant:6333
- VAULT_STEWARD_URL=https://vault-steward:8443
- VAULT_APP_TOKEN=${VAULT_APP_TOKEN_NIS2}
- VAULT_PREFIX=tier1__nis2-app__
- VAULT_REQUIRED=true
entrypoint: ["/usr/local/bin/vault-entrypoint.sh"]
command: ["docker-php-entrypoint", "php-fpm"]
networks:
- nis2-network
- vault-net
depends_on:
db:
condition: service_healthy
@ -76,14 +87,30 @@ services:
networks:
- nis2-network
# -- Qdrant Vector DB (Migration 012-014: KB multi-livello) --
qdrant:
image: qdrant/qdrant:v1.7.4
container_name: nis2-qdrant
restart: unless-stopped
mem_limit: 512m
volumes:
- nis2-qdrant-data:/qdrant/storage
networks:
- nis2-network
# ── Volumes ──────────────────────────────────────────────────────────────
volumes:
nis2-db-data:
driver: local
nis2-uploads:
driver: local
nis2-qdrant-data:
driver: local
# ── Networks ─────────────────────────────────────────────────────────────
networks:
nis2-network:
driver: bridge
vault-net:
external: true
name: vault-net

View File

@ -1,5 +1,43 @@
# Contesto Ultima Sessione
> Il 2026-05-29 ci sono state DUE sessioni: **pomeriggio** (questa, qui sotto) e **mattina** (TRPG alignment, più in basso).
---
## SESSIONE POMERIGGIO — 2026-05-29 (~15:30→17:10 CEST)
### 0. Perché "tutti i prompt sono morti improvvisamente"
Il container **`nis2-agile-devenv` si è RIAVVIATO alle 15:25:55 CEST**. Prova: `uptime` (up 4 min) + `supervisord.log` (supervisord ripartito da pid 98 alle 15:27, nessun crash/respawn interno prima) → **restart esterno** (probabile reboot VM Hetzner; docker/dmesg non ispezionabili da dentro). Le sessioni Claude girano come processi DENTRO il container → terminate tutte insieme. **Nessuna perdita dati** (bind mount persistente). Il cron `ticket-agent` è ripartito normalmente (OPEN_TICKETS.md riscritto alle 15:30 → conferma ripresa, benigno). Niente OOM/disco pieno.
### 1. Messa in sicurezza sorgenti (git)
HEAD era già = `origin/main` (`4a85abe`), ma c'era MOLTO lavoro non committato — incl. **l'intero modulo RAG/KB di aprile mai entrato in git**. Creati **5 commit su `main`** (⚠️ **SOLO LOCALI, NON ancora su Gitea**):
- `1d13166` [CHORE] .gitignore: esclude `*.bak*`, `.backups/`, `.ssh-temp/` (quest'ultimo proteggeva una **chiave SSH privata**!)
- `c0bf7b6` [DOCS] standard cross-suite + governance CLAUDE.md + registri agent
- `1d934e4` [FEAT] UI: guida.html, index-en, mobile-conversion, ai-assistant, bug-reporter, help/i18n
- `9b53ca3` [FEAT] MktgLead getJsonBody + import-feedback-to-nexus + seed demo
- `a7a21fa` [FEAT] Knowledge Base RAG (KnowledgeBaseController + VectorService/EmbedService/RagService + kb.html/js + SQL 012/013 + AIService::askWithRag + qdrant in compose)
- 🔐 **Scrub sicurezza**: rimossa la **API key Voyage hardcoded** da `EmbedService.php` e `docker-compose.yml` (ora solo in `.env` gitignored + vault). NON è finita su Gitea. *(Valutare rotazione della chiave: era in chiaro su disco, condivisa con sustainai.)*
- Rimossi 2 duplicati orfani: `application/controllers/{config.php,EmailService.php}` (copie identiche dei canonici).
> ⚠️ **TODO CRITICO**: i 5 commit sono solo nel `.git` locale. Per il backup su Gitea serve `git-login` (token, cache svuotata dal reboot) + `git push origin main`. Il push tentato è fallito per assenza token. **Anche questo file (CONTEXT) e altre eventuali modifiche doc sono da committare/pushare.**
### 2. Modifiche DB PRODUZIONE — utenti/org "Agile"
Scoperto che **"Agile" = `consulting_firms` id 1 → "Agile Technology SRL"** (unico studio nel sistema). Il `super_admin` vive in **`users.role`** (enum globale), NON in `user_organizations.role`.
- ✅ **Creato utente Simon Fattori** (id **330**, `s.fattori@agile.software`): `role=org_admin`, `consulting_firm_id=1`, **utente LOCALE (no SSO, `sso_identity_id=NULL`)**, password `Fattori2026!@` impostata e verificata con `password_verify`.
- ✅ **Tagliavini (id 326)** declassato **`super_admin``org_admin`** (resta firm 1). `role` NON è sincronizzato dall'SSO → cambio stabile. Restano altri super_admin (3 admin@certisource, 4 benassati, 46 worker, 107 sim-b2b).
- ✅ **Creata organizzazione `Agile Technology SRL`** (id **129**, `entity_type=important`, `sector=ict_services`, `consulting_firm_id=1`, `voluntary_compliance=0`) per **dogfooding NIS2 su sé stessi**. org_admin: Fattori (`is_primary`) + Tagliavini. `firm_org_assignments` 7/8/9. Firm 1 ora gestisce **126/127/128/129**.
- `employee_count` e `annual_turnover_eur` lasciati NULL → da completare in onboarding (servono per soglia dimensionale NIS2).
- Tutte le scritture in **transazione** (commit OK), solo su `nis2_agile_db`.
### Accesso infrastruttura (IMPORTANTE per le prossime sessioni)
- ❌ **Dal container devenv NON si raggiunge il DB** di produzione (`nis2_user@172.18.0.7` → Access denied; `localhost` non ha MySQL qui).
- ❌ **La chiave SSH documentata `docs/credentials/hetzner_key` è ASSENTE.**
- ✅ Usata la **chiave SSH effimera `.ssh-temp/id_ed25519_nis2-agile_8h_20260529-120834`** (validità 8h da 12:08 → scade ~20:08 del 29/05) per `ssh root@135.181.149.254``mysql -h localhost -u nis2_user`. Creds DB da `/var/www/nis2-agile/.env` sul server.
---
## SESSIONE MATTINA — 2026-05-29 (TRPG alignment)
**Data**: 2026-05-29
**Durata**: sessione molto lunga — progetto allineamento NIS2↔TRPG completato

View File

@ -0,0 +1,263 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Evix Suite — Analisi Concorrenza & Best-of-Breed (Strategico/Interno)</title>
<style>
:root{
--cy:#06B6D4; --cy-d:#0e7490; --ink:#0f172a; --mut:#64748b; --line:#e2e8f0;
--ok:#16a34a; --warn:#ea580c; --bad:#dc2626; --bg:#f8fafc; --card:#fff;
--partial:#ca8a04;
}
*{box-sizing:border-box}
body{font-family:'Segoe UI',system-ui,Arial,sans-serif;color:var(--ink);background:var(--bg);margin:0;line-height:1.55;font-size:15px}
.wrap{max-width:1180px;margin:0 auto;padding:32px 24px 80px}
header.hero{background:linear-gradient(135deg,#0f172a,#0e7490);color:#fff;border-radius:18px;padding:36px 40px;margin-bottom:14px}
header.hero .tag{display:inline-block;background:rgba(255,255,255,.15);padding:4px 12px;border-radius:999px;font-size:12px;letter-spacing:1px;text-transform:uppercase}
header.hero h1{margin:14px 0 6px;font-size:30px}
header.hero p{margin:0;opacity:.9;max-width:760px}
.disclaimer{background:#fff7ed;border:1px solid #fed7aa;color:#9a3412;border-radius:12px;padding:14px 18px;font-size:13.5px;margin:14px 0 30px}
h2{font-size:22px;margin:40px 0 6px;border-left:5px solid var(--cy);padding-left:12px}
h2 .sub{display:block;font-size:13px;color:var(--mut);font-weight:400;margin-top:3px;padding-left:1px}
h3{font-size:17px;margin:26px 0 8px;color:var(--cy-d)}
p.lead{color:var(--mut)}
.grid{display:grid;gap:16px}
.modules{grid-template-columns:repeat(auto-fill,minmax(250px,1fr))}
.card{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:18px}
.card h4{margin:0 0 4px;font-size:16px}
.badge{display:inline-block;font-size:11px;font-weight:700;padding:3px 9px;border-radius:999px;text-transform:uppercase;letter-spacing:.5px}
.b-ok{background:#dcfce7;color:#166534} .b-near{background:#fef9c3;color:#854d0e}
.b-gap{background:#fee2e2;color:#991b1b}
.card ul{margin:8px 0 0;padding-left:18px;font-size:13.5px;color:#334155}
.card ul.gap li{color:#9a3412}
table{width:100%;border-collapse:collapse;background:#fff;border:1px solid var(--line);border-radius:12px;overflow:hidden;font-size:13.5px;margin-top:10px}
th,td{padding:10px 12px;text-align:left;border-bottom:1px solid var(--line);vertical-align:top}
thead th{background:#ecfeff;color:var(--cy-d);font-size:12.5px;position:sticky;top:0}
tbody tr:last-child td{border-bottom:none}
.col-evix{background:#f0fdff}
.y{color:var(--ok);font-weight:700}.n{color:var(--bad);font-weight:700}.pp{color:var(--partial);font-weight:700}
.v{font-size:11px;color:var(--mut);font-style:italic}
.legend{display:flex;gap:18px;flex-wrap:wrap;font-size:13px;color:var(--mut);margin:10px 0 0}
.legend span{display:inline-flex;align-items:center;gap:6px}
.dot{width:11px;height:11px;border-radius:50%;display:inline-block}
.roadmap{border-left:3px solid var(--cy);padding-left:18px;margin-top:10px}
.roadmap .item{margin-bottom:14px}
.roadmap .when{font-size:12px;font-weight:700;color:var(--cy-d);text-transform:uppercase;letter-spacing:.5px}
footer{margin-top:50px;border-top:1px solid var(--line);padding-top:18px;font-size:12.5px;color:var(--mut)}
.src{background:#f1f5f9;border-radius:10px;padding:14px 18px;font-size:12.5px;color:#475569;margin-top:14px}
@media(max-width:680px){header.hero{padding:24px}.wrap{padding:18px}}
</style>
</head>
<body>
<div class="wrap">
<header class="hero">
<span class="tag">Documento strategico interno · gap-driven</span>
<h1>Evix Suite — Analisi Concorrenza & posizionamento Best-of-Breed</h1>
<p>Obiettivo strategico: ogni modulo della suite Evix (i prodotti Agile) deve essere <strong>best-of-breed</strong> nel proprio dominio. Questo documento valuta onestamente dove lo siamo già e dove mancano feature per diventarlo, rispetto a piattaforme GRC internazionali e soluzioni NIS2/compliance italiane.</p>
</header>
<div class="disclaimer">
<strong>Nota metodologica (fonti certe).</strong> Le capacità di <strong>Evix/NIS2 Agile</strong> riportate sono verificate sul codice sorgente del prodotto (feature realmente implementate al 2026-05-29, v1.7.0). Le capacità dei <strong>concorrenti</strong> riflettono posizionamenti generali di pubblico dominio e <em>vanno verificate</em> prima di qualsiasi uso commerciale: le celle marcate <span class="v">[da verificare]</span> richiedono una fonte primaria (sito vendor, demo, analyst report). Questo è un documento <strong>interno</strong>, non un materiale di marketing.
</div>
<h2>1. La suite Evix come insieme di moduli
<span class="sub">Evix = brand-ombrello; ciascun prodotto Agile è un "modulo" della suite, integrabile via API condivise (agile-services) e SSO centralizzato.</span>
</h2>
<div class="grid modules">
<div class="card">
<h4>NIS2 Agile</h4>
<span class="badge b-ok">Maturo</span>
<ul><li>Compliance Direttiva (UE) 2022/2555 + D.Lgs. 138/2024</li><li>Oggetto principale di questa analisi</li></ul>
</div>
<div class="card">
<h4>231 Agile</h4>
<span class="badge b-near">In suite</span>
<ul><li>Modello Organizzativo 231</li><li>Integrato con NIS2 via Services API (mapping NIS2→MOG)</li></ul>
</div>
<div class="card">
<h4>SustainAI Agile</h4>
<span class="badge b-near">In suite</span>
<ul><li>Sostenibilità / ESG / CSRD</li><li>Stesso stack UI/RAG</li></ul>
</div>
<div class="card">
<h4>TRPG Agile</h4>
<span class="badge b-near">In suite</span>
<ul><li>Risk &amp; privacy / governance</li><li>Allineamento Auth/SSO/Sessions in corso</li></ul>
</div>
</div>
<p class="lead" style="margin-top:12px">Il vantaggio competitivo di suite (non del singolo modulo) è l'<strong>integrazione cross-prodotto</strong> + <strong>motore AI/RAG condiviso</strong> + <strong>multi-tenancy e white-label per studi di consulenza</strong>. Sotto, il dettaglio modulo-per-modulo del prodotto NIS2 (il più maturo); gli altri prodotti richiedono un audit analogo dedicato.</p>
<h2>2. Scorecard Best-of-Breed — moduli di NIS2 Agile
<span class="sub">Verde = già best-of-breed nel segmento NIS2 Italia · Giallo = competitivo, gap colmabili · Rosso = gap rilevante vs leader di categoria</span>
</h2>
<div class="grid modules">
<div class="card"><h4>Gap Analysis Art.21</h4><span class="badge b-ok">Best-of-breed (IT)</span>
<ul><li>80 domande su 10 categorie Art.21, scoring + analisi AI</li><li>Mapping ISO 27001 + ora NIST CSF 2.0 (43 controlli)</li></ul>
<ul class="gap gap"><li>Gap: benchmark settoriale anonimizzato assente</li></ul>
</div>
<div class="card"><h4>Asset &amp; Sistemi Rilevanti</h4><span class="badge b-ok">Best-of-breed (IT)</span>
<ul><li>Scoring rilevanza 0-100 a 6 criteri (GV.OC-04)</li><li>Registro formale stampabile + classi critico/alto/medio</li></ul>
<ul class="gap"><li>Gap: auto-discovery asset e integrazione CMDB/cloud assenti</li></ul>
</div>
<div class="card"><h4>Gestione Incidenti</h4><span class="badge b-ok">Best-of-breed (IT)</span>
<ul><li>Art.23 24h/72h/30g + tassonomia IS-1..4 (Determina ACN 164179/2025)</li><li>PIR 5-Whys + metriche TTD/TTC/TTR; regime essenziale/importante</li></ul>
<ul class="gap"><li>Gap: ingestion automatica da SIEM/SOC/EDR (oggi manuale)</li></ul>
</div>
<div class="card"><h4>Risk Management</h4><span class="badge b-near">Competitivo</span>
<ul><li>Registro rischi, matrice 5×5 ISO 27005, AI suggest</li></ul>
<ul class="gap"><li>Gap vs leader: scenari quantitativi (FAIR), monte-carlo, KRI dashboard</li></ul>
</div>
<div class="card"><h4>Audit &amp; Evidence</h4><span class="badge b-ok">Differenziante</span>
<ul><li>Hash-chain SHA-256 immutabile (integrità forense) + export certificato</li><li>Mapping ISO27001 e NIST CSF 2.0</li></ul>
<ul class="gap"><li>Gap: raccolta evidenze automatica (connettori) assente</li></ul>
</div>
<div class="card"><h4>Policy Management</h4><span class="badge b-near">Competitivo</span>
<ul><li>Generazione bozze AI + workflow approvazione</li></ul>
<ul class="gap"><li>Gap: versioning/diff avanzato, attestation dipendenti</li></ul>
</div>
<div class="card"><h4>Supply Chain</h4><span class="badge b-near">Competitivo</span>
<ul><li>Valutazione fornitori + risk scoring + Art.21.2(d)</li></ul>
<ul class="gap"><li>Gap: questionari self-assessment al fornitore, rating esterni</li></ul>
</div>
<div class="card"><h4>AI / Knowledge Base</h4><span class="badge b-ok">Differenziante</span>
<ul><li>RAG multi-livello (SYSTEM/FIRM/ORG) su Qdrant + Voyage</li><li>Grounding su testi normativi ufficiali con citazioni (fonti certe)</li></ul>
<ul class="gap"><li>Gap: nessun gap critico; estendere copertura KB normativa</li></ul>
</div>
<div class="card"><h4>Continuous Monitoring</h4><span class="badge b-gap">Gap rilevante</span>
<ul class="gap"><li>Assente: monitoraggio continuo dei controlli + evidence automation (core di Vanta/Drata)</li><li>Oggi: assessment puntuale + evidenze manuali</li></ul>
</div>
<div class="card"><h4>Integrazioni / Connettori</h4><span class="badge b-gap">Gap rilevante</span>
<ul class="gap"><li>Assente catalogo connettori (M365, Google, AWS/Azure, Jira, IdP, EDR)</li><li>Presente: Services API + Webhook HMAC (base solida per costruirli)</li></ul>
</div>
</div>
<h2>3. Matrice comparativa
<span class="sub">NIS2 Agile (Evix) vs categorie concorrenti. Confronto sul caso d'uso "compliance NIS2 per PMI/Enterprise e studi di consulenza in Italia".</span>
</h2>
<div class="legend">
<span><span class="dot" style="background:var(--ok)"></span> Sì / forte</span>
<span><span class="dot" style="background:var(--partial)"></span> Parziale</span>
<span><span class="dot" style="background:var(--bad)"></span> No / debole</span>
<span class="v">[dv] = dato concorrente da verificare</span>
</div>
<table>
<thead><tr>
<th>Capacità</th>
<th class="col-evix">NIS2 Agile (Evix)</th>
<th>GRC internazionali<br><span class="v">(ServiceNow, OneTrust, Archer)</span></th>
<th>Compliance automation<br><span class="v">(Vanta, Drata)</span></th>
<th>Soluzioni NIS2 IT<br><span class="v">[da verificare]</span></th>
<th>Consulenza + Excel</th>
</tr></thead>
<tbody>
<tr><td>NIS2 / D.Lgs.138 nativo + Determine ACN 2025</td>
<td class="col-evix y">Sì — aggiornato Determine 2025</td>
<td class="pp">Parziale, framework generici <span class="v">[dv]</span></td>
<td class="n">Debole (focus SOC2/ISO) <span class="v">[dv]</span></td>
<td class="pp">Variabile <span class="v">[dv]</span></td>
<td class="pp">Dipende dal consulente</td></tr>
<tr><td>AI nativa (gap, policy, classificazione incidenti) con grounding su fonti certe</td>
<td class="col-evix y">Sì — RAG citante testi ufficiali</td>
<td class="pp">In crescita <span class="v">[dv]</span></td>
<td class="pp">In crescita <span class="v">[dv]</span></td>
<td class="n">Raro <span class="v">[dv]</span></td>
<td class="n">No</td></tr>
<tr><td>Audit trail immutabile (hash-chain)</td>
<td class="col-evix y">Sì — SHA-256 chain</td>
<td class="y"><span class="v">[dv]</span></td>
<td class="pp">Parziale <span class="v">[dv]</span></td>
<td class="n">Raro <span class="v">[dv]</span></td>
<td class="n">No</td></tr>
<tr><td>Continuous control monitoring + evidence automation</td>
<td class="col-evix n">No (gap)</td>
<td class="y"><span class="v">[dv]</span></td>
<td class="y">Sì — core <span class="v">[dv]</span></td>
<td class="n">No <span class="v">[dv]</span></td>
<td class="n">No</td></tr>
<tr><td>Catalogo connettori/integrazioni (cloud, IdP, EDR, ticketing)</td>
<td class="col-evix pp">Parziale — API/webhook, no connettori pronti</td>
<td class="y"><span class="v">[dv]</span></td>
<td class="y">Sì — molti <span class="v">[dv]</span></td>
<td class="n">Raro <span class="v">[dv]</span></td>
<td class="n">No</td></tr>
<tr><td>Multi-tenant + white-label per studi di consulenza</td>
<td class="col-evix y">Sì — firm + branding + KB FIRM</td>
<td class="pp">Parziale/costoso <span class="v">[dv]</span></td>
<td class="pp">Programmi partner <span class="v">[dv]</span></td>
<td class="pp">Variabile <span class="v">[dv]</span></td>
<td class="n">No</td></tr>
<tr><td>Integrazione cross-compliance (NIS2 ↔ 231 ↔ ESG)</td>
<td class="col-evix y">Sì — suite Evix + Services API</td>
<td class="pp">Moduli separati <span class="v">[dv]</span></td>
<td class="n">No <span class="v">[dv]</span></td>
<td class="n">No <span class="v">[dv]</span></td>
<td class="n">No</td></tr>
<tr><td>Costo / time-to-value per PMI italiana</td>
<td class="col-evix y">Basso — onboarding guidato + visura</td>
<td class="n">Alto / enterprise <span class="v">[dv]</span></td>
<td class="pp">Medio <span class="v">[dv]</span></td>
<td class="pp">Variabile <span class="v">[dv]</span></td>
<td class="pp">Alto in ore uomo</td></tr>
<tr><td>Reporting/dashboard enterprise &amp; analytics</td>
<td class="col-evix pp">Parziale — report esecutivo + CSV</td>
<td class="y">Forte <span class="v">[dv]</span></td>
<td class="y">Forte <span class="v">[dv]</span></td>
<td class="pp">Variabile <span class="v">[dv]</span></td>
<td class="n">No</td></tr>
</tbody>
</table>
<h2>4. Dove siamo già Best-of-Breed
<span class="sub">Vantaggi difendibili, radicati su feature reali del prodotto.</span>
</h2>
<div class="grid modules">
<div class="card"><h4>🇮🇹 Aderenza normativa italiana</h4><ul><li>Unico a coprire Determina ACN 164179/2025 (IS-1..4, Allegati 3/4) e 333017/2025 a livello di workflow, non solo testo.</li></ul></div>
<div class="card"><h4>🤖 AI con fonti certe</h4><ul><li>Risposte AI ancorate ai testi normativi ufficiali con citazione esplicita e divieto di riferimenti inventati.</li></ul></div>
<div class="card"><h4>🔗 Integrità audit</h4><ul><li>Hash-chain SHA-256 + export certificato: integrità forense delle evidenze.</li></ul></div>
<div class="card"><h4>🏢 Modello consulenza/white-label</h4><ul><li>Multi-tenant, KB a 3 livelli, branding per studio: pensato per i consulenti, non solo l'azienda finale.</li></ul></div>
<div class="card"><h4>🧩 Suite integrata</h4><ul><li>NIS2 ↔ 231 ↔ ESG via Services API condivise: i competitor sono single-domain.</li></ul></div>
</div>
<h2>5. Gap → Roadmap per chiudere il "best-of-breed" su ogni modulo
<span class="sub">Priorità per impatto competitivo. Le voci P1 sono quelle che oggi ci fanno perdere confronti vs Vanta/Drata e GRC enterprise.</span>
</h2>
<div class="roadmap">
<div class="item"><div class="when">P1 · Colmare il gap "compliance automation"</div>
<strong>Continuous Control Monitoring + Evidence Automation.</strong> Connettori per raccolta automatica evidenze (M365, Google Workspace, AWS/Azure, IdP, EDR). È il core di Vanta/Drata e oggi è il nostro gap più visibile. Base esistente: Services API + Webhook HMAC.</div>
<div class="item"><div class="when">P1 · Ingestion incidenti</div>
<strong>Integrazione SIEM/SOC/EDR</strong> per apertura automatica incidenti (i mockup analizzati già prevedevano "Alert SIEM/SOC" come fonte). Trasforma il modulo incidenti da reattivo a proattivo.</div>
<div class="item"><div class="when">P2 · Asset</div>
<strong>Auto-discovery asset + import CMDB/cloud</strong> per alimentare automaticamente lo scoring di rilevanza GV.OC-04 appena introdotto.</div>
<div class="item"><div class="when">P2 · Risk quantitativo</div>
<strong>Risk analysis quantitativa (FAIR)</strong> + dashboard KRI, per competere con i GRC enterprise sul risk management.</div>
<div class="item"><div class="when">P2 · Reporting</div>
<strong>Dashboard analytics e benchmark settoriale anonimizzato</strong> (già nei TODO di progetto) — chiude il gap su reporting e aggiunge un dato che i competitor non hanno (rete multi-tenant).</div>
<div class="item"><div class="when">P3 · Supply chain &amp; policy</div>
<strong>Portale self-assessment fornitori</strong> e <strong>attestation/versioning policy</strong> per completare i due moduli "competitivi" verso il best-of-breed.</div>
</div>
<div class="src">
<strong>Fonti &amp; verificabilità.</strong> Capacità Evix/NIS2 Agile: codice sorgente del prodotto (v1.7.0, 2026-05-29) e documentazione di progetto. Riferimenti normativi: Direttiva (UE) 2022/2555, D.Lgs. 138/2024, Determine ACN 164179/2025 e 333017/2025 (registro <code>application/config/nis2_sources.php</code>). <strong>Dati concorrenti marcati [da verificare]</strong>: posizionamenti generali di pubblico dominio, da confermare con fonte primaria prima di ogni uso esterno. I nomi dei vendor sono citati a scopo di benchmarking interno.
</div>
<footer>
Evix Suite — Analisi Concorrenza (strategico/interno) · generato 2026-05-29 (CEST) · NIS2 Agile v1.7.0 ·
Documento di lavoro: <strong>non distribuire esternamente senza validazione dei dati concorrenti</strong>.
</footer>
</div>
</body>
</html>

6
docs/OPEN_TICKETS.md Normal file
View File

@ -0,0 +1,6 @@
# OPEN TICKETS — NIS2
Nessun ticket aperto.
---
_Ultimo sync: 2026-05-29 15:40:02_

View File

@ -0,0 +1,982 @@
# STANDARD_AI_PRODOTTO — Configurazione AI di Prodotto AgileHub Suite
> Standard cross-suite per progettare, configurare e mantenere ogni "AI di prodotto" della suite AgileHub seguendo il template di riferimento TRPG come gold standard.
>
> **Slug**: `ai-prodotto`
> **Versione**: 1.0 (proposed — pending sub-approval utente per seed in `nexus_hub.hub_standards`)
> **Owner**: Agile AI (Architetto AI di prodotto + KB governance)
> **Applies to**: tutti i prodotti suite AgileHub (`*`) — TRPG, TRPG-PRO, SUSTAINAI, NIS2, AllTax, LG231, DFM-PRO, ALLRISK, WMS, MADEBYCLOUD, CertiSource + AgileHub stesso (AI runtime "Agile")
> **Reference implementation**: TRPG (gold standard per Support, Sales, Plan-mode; Lead qualification ha riferimento separato Giulia EDUCATION)
> **Sostituisce**: nessuno (standard nuovo). Convive con `installer-integration` v1.0.1, `ticket-tags` v1.0, `outbound-campaigns` v1.0, `telephony-integration` v1.0
> **Data redazione**: 2026-04-23 (sessione 33 part 6 — kickoff AGI-2 master plan v2.1)
> **Prossima revisione attesa**: dopo AGI-5 test E2E (~settimana 4-5 master plan v2.1) o all'introduzione di L3 PERSONA Lotto F Avatar Registry
---
## §1 Premessa & autorità
### 1.1 Cos'è una "AI di prodotto"
Una **AI di prodotto** è un'entità conversazionale Claude-based, esposta agli utenti di un prodotto della suite AgileHub, che opera con:
- una **persona dichiarata** (nome, tono, lingua)
- un **obiettivo specifico** (qualificare lead, risolvere ticket prima di escalare, dimostrare prodotto in landing, dialogare con approver durante plan mode, ecc.)
- una **knowledge base** (`product_knowledge` filtrata per prodotto/tenant/livello)
- un **set di tool** dichiarativi che il backend esegue per suo conto
- **guardrails** espliciti su cosa NON deve mai fare
- **cost tracking** per attribuire spesa LLM/RAG al prodotto + tenant + sessione
NON è una AI di prodotto:
- un **agente Claude specialista** (es. Agile AI, VOX, MAESTRO) — vivono in chat dello sviluppatore, non parlano a utenti finali
- un **content generator** (`contentController.js`) — genera contenuti marketing offline, non dialoga
- un **classifier puro** (es. `voiceDialogController.js` interpretTurn) — è un sub-componente di una AI di prodotto, non una AI standalone
### 1.2 Perché serve uno standard
Audit `nexus-ai-ms` (sessione 33 part 6, AGI-1) ha rivelato 5 famiglie di AI conversazionali coesistenti, ciascuna con propria convenzione divergente:
| Famiglia | File chiave | Persona | Modello | KB binding | Persistenza config |
|---|---|---|---|---|---|
| **ARIA Support post-vendita** | [`supportPrompts.js`](../nexus-ai-ms/src/prompts/supportPrompts.js) | ARIA | Sonnet (default) | `product_knowledge` keyword | `ai_agent_profiles` riassunto + template hardcoded |
| **ARIA Lead qualification verticali** | [`basePrompts.js`](../nexus-ai-ms/src/prompts/basePrompts.js) | ARIA (4 verticali) | Sonnet | nessuno (solo `tenantConfig`) | `ai_agent_profiles` (Giulia per EDUCATION) |
| **Anna Sales pre-vendita** | [`publicDemoController.js`](../nexus-ai-ms/src/controllers/publicDemoController.js) | Anna | Haiku | PRODUCT_CARD pinned + RAG keyword | hardcoded controller, non in DB |
| **Anna Voice intent classifier** | [`voiceDialogController.js`](../nexus-ai-ms/src/controllers/voiceDialogController.js) | Anna | Haiku | PRODUCT_CARD snippet | hardcoded controller, non in DB |
| **Gaia Plan-mode dialog approver** | [`gaiaController.js`](../nexus-ai-ms/src/controllers/gaiaController.js) | Gaia | dinamico (URGENT→Opus, default Sonnet) | PRODUCT_CARD pinned + RAG keyword score top-3 | in-memory `sessions Map`, non in DB |
**Drift osservato vs aspettativa**:
- Naming inconsistente (ARIA vs Anna vs Gaia, nessuna regola)
- 3 di 5 famiglie NON usano `ai_agent_profiles` (single source of truth attesa)
- `system_prompt_voice` field esiste in DB ma è NULL su tutti gli ARIA_SUPPORT (voice prompt hardcoded in controller)
- KB pollution: 3 articoli `PRODUCT_CARD` AGILEHUB sono HOW_TO mal-categorizzati (id 720, 729, 730 — `JSON_VALID(answer)=0`)
- Cost tracking opzionale (`opts.sessionId && opts.productSlug` → reporter chiamato solo se entrambi presenti)
- Vertical ENUM `ai_agent_profiles` include `HEALTH/LEGAL/REALTY` ma DB ne ha 0 record
Lo standard **chiude questi gap** definendo conformance bloccanti per AGI-4 retrofit.
### 1.3 Relazione con altri standard cross-suite
| Standard | Slug | Ruolo |
|---|---|---|
| `installer-integration` v1.0.1 | INSTALLATORE | Lifecycle deployment cliente — questo standard descrive cosa l'installatore deve provisionare in termini di AI runtime |
| `ticket-tags` v1.0 | hub-ms | Catalogo tag cross-prodotto — AI di prodotto usano questi tag quando creano ticket via `open_ticket`/`escalate_to_ticket` |
| `outbound-campaigns` v1.0 | lead-ms | Campagne outbound — AI di prodotto Sales pre-vendita può essere triggerata da campaign outbound |
| `telephony-integration` v1.0 (proposed) | call-ms | Pipeline call → Pipecat — AI di prodotto voice channel rispetta questo contratto |
| **`ai-prodotto` v1.0 (questo)** | **`ai-prodotto`** | **Configurazione canonical AI prodotto suite** |
### 1.4 Autorità Agile AI
Agile AI (agente Claude specialista, ex GAIA, rinominato 2026-04-23 sessione 33 part 6) è **owner unico** di:
- Questo documento + future versioni
- Tabella `nexus_hub.hub_standards` slug `ai-prodotto`
- Tabella `nexus_ticket_db.product_knowledge` (CRUD + retention + audit pollution)
- Tabella `nexus_ai_db.ai_agent_profiles` (CRUD + validation conformance)
- Conformance audit AGI-4 retrofit (gap report formale per prodotto)
- Coordinamento con MAESTRO su blocchi AWE AI runtime (`C09_RetrieveFromKB`, `D02_LoadAvatarKB`, `AC11_GenerateAIResponse`)
- Coordinamento con VOX su KB binding L3 PERSONA (`ai_profiles.knowledge_id`)
Modifiche allo standard richiedono sub-approval esplicita utente (pattern come `STANDARD_INSTALLER_INTEGRATION.md`).
---
## §2 Naming convention netto
> **Cruciale**: 4 categorie distinte, mai confondere. Drift naming è la prima causa di confusione cross-team.
### 2.1 AI di prodotto (runtime, esposta agli utenti)
| Naming | Cosa | Esempi |
|---|---|---|
| `<NomePersona>` (italiano) | Persona conversazionale visibile all'utente finale | "Anna" (Sales TRPG/AGILEHUB), "Aria" (Support generico), "Gaia" (TRPG plan-mode) |
| `<NomePersona> <Verticale>` | Specializzazione per verticale lead qualification | "Giulia Master e Certificazioni", "Giulia Abilitazioni Estere" |
| **"Agile"** ⭐ | AI runtime di **AgileHub** stesso (caso speciale meta) | "Agile" — esposta in `/admin/*` per governance + agli utenti dei prodotti che chiedono "come usare AgileHub" |
**Regola**: ogni AI di prodotto deve avere un `display_name` (per UI + voice) **e** un `agent_key` interno (per DB + log + cost tracking) `<TIPO>_<PRODUCT>` pattern, es. `ARIA_SUPPORT_TRPG`, `ANNA_SALES_TRPG`, `GAIA_PLANMODE_TRPG`, `AGILE_RUNTIME` (per "Agile" meta).
### 2.2 Agente Claude specialista (sviluppo, qui in chat)
| Naming | Cosa | Esempi |
|---|---|---|
| `<NomeAgente>` (UPPERCASE meta) | Specializzazione di Claude Code per task di sviluppo AgileHub | REGENT, VOX, MAESTRO, CICERONE, PRISMA, INSTALLATORE, **Agile AI** (con suffisso AI per distinguerlo da "Agile" runtime) |
**Regola**: gli agenti specialisti non parlano agli utenti finali, vivono solo nella chat sviluppatore (`docs/AGENT_<NAME>.md` autoritativo). Distinzione netta da AI runtime.
### 2.3 AI marketing pre-vendita (lead-facing pubblico)
Sub-categoria di "AI di prodotto" con vincoli GDPR aggiuntivi (vedi §8):
- Vede solo articoli `product_knowledge` con `category IN ('PRODUCT_CARD', 'HOW_TO', 'URL_CHUNK')` pubblici (no L2 COMPANY dati cliente)
- Tone: caldo, commerciale, breve
- Naming: "Anna" (canonical su tutti i landing — drift Charlotte vs Sarah voice da chiudere VOX)
### 2.4 Persona vocale plan-mode
Sub-categoria con scope ristretto a **dialogo agente AI ↔ approver umano** durante PENDING_APPROVAL ticket:
- Naming: "Gaia" (TRPG pilota — non confondere con "Gaia agente specialista" che sarebbe estensione futura)
- Tone: tecnica, prima persona femminile, ammette limiti
- Tool readonly via devenv-gateway (vedi §6.3)
### 2.5 Tabella riassuntiva naming
| Termine | Significato | Esempio |
|---|---|---|
| **"Agile"** | AI runtime di AgileHub (prodotto meta) | "Ciao, sono Agile, posso aiutarti su AgileHub" |
| **"Agile AI"** | Agente Claude specialista (in chat dev) | "Ciao Agile AI Go!" |
| **"Anna"** | AI marketing pre-vendita (canonical) | Anna su `/public/demo-lead-trpg` |
| **"Aria"** | AI Support post-vendita (canonical) | ARIA_SUPPORT_TRPG widget viola |
| **"Gaia" persona vocale** | AI plan-mode dialog approver TRPG | Gaia parla con approver durante PENDING_APPROVAL |
| **"Giulia"** | AI Lead qualification verticali EDUCATION | GIULIA_TRIAGE, GIULIA_LAUREE, ecc. |
---
## §3 Anatomia di una "AI di prodotto" — 12 elementi obbligatori
Ogni AI di prodotto, per essere conforme allo standard, deve esplicitare **12 elementi**. La conformance checklist (§10) si basa su questa anatomia.
| # | Elemento | MUST/SHOULD | Dove vive |
|---|---|---|---|
| 1 | Identità persona | MUST | `ai_agent_profiles.name` + system_prompt opening |
| 2 | Obiettivo dichiarato | MUST | system_prompt sezione "Il tuo obiettivo" |
| 3 | Modello Claude | MUST | `opts.model` in chiamata `anthropicService.chat/runAgentLoop` |
| 4 | System prompt strutturato | MUST | `ai_agent_profiles.system_prompt` (canonical) |
| 5 | KB binding | MUST | `product_knowledge` filter clauses |
| 6 | Tool list dichiarativa | MUST se sub-template Support/Sales/Plan-mode/Lead | `nexusTools.js` subset |
| 7 | Guardrails LIMITI ASSOLUTI | MUST | system_prompt sezione finale |
| 8 | Voice rules | MUST se canale voice | `VOICE_RULES` const condivisa |
| 9 | FSM rules | MUST se lead qualification | `FSM_RULES` const condivisa |
| 10 | Channel-aware | SHOULD | `ai_agent_profiles.system_prompt_voice` separato |
| 11 | Tenant context injection | MUST | `tenantConfig.{agentName, companyName, greeting}` |
| 12 | Cost tracking | MUST | `opts.{caller, sessionId, productSlug}` ad ogni chiamata |
### 3.1 Identità persona (MUST)
Pattern canonical:
```
Sei <NomePersona>, <ruolo> di <Prodotto/AgileHub>.
Parli in <lingua>, con tono <descrittore tono>.
```
Esempio TRPG (`buildSupportPrompt(TRPG)`):
> Sei ARIA, assistente di supporto intelligente per TRPG Pro (Gender Pay Gap & Compliance).
**Regole**:
- Nome persona scelto dal catalogo §2 (Anna/Aria/Gaia/Giulia/Agile) o nuovo previa approvazione Agile AI
- Lingua dichiarata esplicita (default `it`, supportare `en` da v1.1)
- Genere consistente (es. femminile per Anna/Aria/Gaia)
### 3.2 Obiettivo dichiarato (MUST)
Pattern canonical: 1-3 frasi che dichiarano cosa l'AI deve fare. Massimo 1 obiettivo primario + 2 sub-obiettivi.
Esempio TRPG Support:
> IL TUO OBIETTIVO: Aiutare l'utente a RISOLVERE il problema SENZA creare un ticket.
**Regole**:
- Obiettivo deve essere actionable (verbo all'infinito + risultato misurabile)
- Niente claim di onniscienza ("sono in grado di tutto") → ammettere ambito ristretto
- Allineato al sub-template (§4)
### 3.3 Modello Claude (MUST)
Decision tree obbligatorio:
| Caso d'uso | Modello | Motivazione |
|---|---|---|
| Voice intent classifier (output JSON ristretto, < 200 token) | **Haiku** | Latenza minima, costo basso, accuracy sufficiente per classification |
| Greeting / saluto / messaggio singolo breve | **Haiku** | Idem, una sola interazione |
| Sales pre-vendita / chat lead-facing (3-4 frasi per turn) | **Haiku** | Volume alto, costo critico |
| Support post-vendita (dialog completo, RAG retrieval) | **Sonnet** | Default — bilanciato |
| Lead qualification (FSM transition + tool use) | **Sonnet** | Tool use con multi-step |
| Plan-mode dialog approver (priorità default/MEDIUM) | **Sonnet** | Tool use + reasoning su codice |
| Plan-mode dialog approver (priorità URGENT/HIGH) | **Opus** | Reasoning profondo per bug critici (pattern Gaia) |
| Code analysis / outcome evaluation post-call | **Sonnet** | Pattern OutcomeEvaluator nexus-call-ms |
**Regole**:
- Modello esplicito via `opts.model` — NO usare default global `process.env.ANTHROPIC_MODEL`
- Modello dinamico per priorità è encouraged (vedi pattern Gaia `gaiaController.js:670-676`)
- Modello deprecato → mai usare (vedi `MODELS` const in [`anthropicService.js`](../nexus-ai-ms/src/services/anthropicService.js))
### 3.4 System prompt strutturato (MUST)
Template canonical 7 sezioni:
```
[1. Identità persona]
Sei <Nome>, <ruolo> di <Prodotto>. Parli in <lingua>, tono <descrittore>.
[2. Obiettivo]
IL TUO OBIETTIVO: <1-3 frasi azione concreta>.
[3. Tono e stile]
TONO E STILE:
- <regola 1>
- <regola 2>
...
[4. Procedura operativa]
PROCEDURA:
1. <step 1>
2. <step 2>
...
[5. KB inject (se KB binding)]
SCHEDA PRODOTTO:
<getProductCardSnippet output>
CONOSCENZA DI DOMINIO RILEVANTE:
<top-K articoli RAG>
[6. Tenant context (se tenant injection)]
AZIENDA: <companyName>
SALUTO INIZIALE: <greeting>
[7. Limiti assoluti]
LIMITI ASSOLUTI:
- <limite 1>
- <limite 2>
...
```
**Regole**:
- Ordine sezioni vincolante (riproducibile cross-prodotto)
- Sezioni 5/6 opzionali ma se presenti devono usare il pattern esatto
- Sezione 7 OBBLIGATORIA (no-op fallback se non si sa cosa scrivere: "Non garantire prezzi/scadenze/disponibilità senza verifica")
### 3.5 KB binding (MUST)
Pattern obbligatorio (vedi §5 dettaglio):
1. Recuperare PRODUCT_CARD pinned via `getProductCardSnippet(product)` ([`productKbSnippet.js`](../nexus-ai-ms/src/services/productKbSnippet.js))
2. Recuperare top-K articoli RAG keyword score via `getRelevantHelpArticles(product, query, K=3)` (idem)
3. Iniettare entrambi nella sezione 5 del system prompt
4. Filtrare per `tenant_id` se context tenant disponibile (L2 COMPANY)
5. Filtrare per `knowledge_filter_slug` se context avatar disponibile (L3 PERSONA, futuro Lotto F)
**Eccezioni**: AI Voice intent classifier (output JSON ristretto, KB superflua) — sufficiente PRODUCT_CARD snippet.
### 3.6 Tool list dichiarativa (MUST se sub-template Support/Sales/Plan-mode/Lead)
Subset del catalogo `NEXUS_TOOLS` (11 tool) — vedi §6 matrice.
**Regole**:
- Tool list dichiarata esplicita nel chiamante (no global)
- Tool custom (es. Gaia `read_file`/`grep`/...) richiedono spec docs separato
- Tool con side-effect (es. `open_ticket`, `escalate_to_ticket`) devono passare per `ToolExecutor` — mai chiamati direttamente da AI
### 3.7 Guardrails LIMITI ASSOLUTI (MUST)
Pattern canonical sezione 7 system_prompt:
```
LIMITI ASSOLUTI:
- Non <azione 1 vietata>
- Non <azione 2 vietata>
...
```
**Limiti minimi obbligatori per ogni AI di prodotto**:
- Non garantire prezzi non confermati / disponibilità senza verifica
- Non inventare feature/funzionalità non presenti nella KB
- Non fornire informazioni mediche/legali/finanziarie personalizzate (sub-template specifici)
- Non rivelare informazioni di altri tenant (multi-tenant isolation L2)
### 3.8 Voice rules (MUST se canale voice)
Importare e includere `VOICE_RULES` da [`basePrompts.js`](../nexus-ai-ms/src/prompts/basePrompts.js):
```
REGOLE CANALE VOCE:
- Massimo 2 frasi per turno conversazionale
- Mai elenchi puntati o numerati
- Mai markdown di alcun tipo
- Intercalari italiani naturali ("certo", "capisco", "perfetto", "ottimo", "mi dica")
- Inizia le risposte con un breve acknowledgment poi vai al punto
- Numeri pronunciati in lettere ("seimilacinquecento" non "6.500")
- Date in forma estesa ("settembre duemilaventisei")
- Se hai bisogno di tempo: "Un attimo, sto verificando..."
- Poni UNA SOLA domanda per turno
```
**Regole**:
- `VOICE_RULES` invariato — modifiche richiedono coordinamento VOX
- `system_prompt_voice` (campo `ai_agent_profiles`) deve includere VOICE_RULES + sub-template adattato a voice (più stringato)
- AI con canale dual (text + voice) hanno DUE prompt distinti, mai unificati
### 3.9 FSM rules (MUST se lead qualification)
Importare e includere `FSM_RULES` da [`basePrompts.js`](../nexus-ai-ms/src/prompts/basePrompts.js):
```
REGOLE OPERATIVE FSM:
- Mantieni sempre un solo current_state
- Poni SOLO la domanda prevista dallo stato corrente
- Dopo ogni risposta del lead, chiama il tool fsm_transition con:
- current_state, trigger normalizzato, raw_answer
- NON eseguire MAI calcoli di score (lookup backend)
- Se isTerminal=true, chiama complete_scoring
- Se sideEffects, esegui i tool corrispondenti
```
**Regole**:
- `FSM_RULES` invariato — modifiche richiedono coordinamento MAESTRO (FSM engine integration)
- AI senza FSM (Support, Sales generico, Anna voice intent) NON includono FSM_RULES
### 3.10 Channel-aware (SHOULD)
Se la AI di prodotto può operare su più canali (chat web, voice IVR, mobile), DEVE avere prompt distinti.
Pattern: `ai_agent_profiles` ha 2 campi:
- `system_prompt` (default canale text/chat)
- `system_prompt_voice` (override per canale VOIP/voice)
Logica selezione: `sessionService.js:103-107` — se `agent.systemPrompt` esiste lo usa, altrimenti chiama `buildSystemPrompt(vertical, channel, ...)`.
**Drift attuale da chiudere AGI-4**: tutti gli ARIA_SUPPORT hanno `system_prompt_voice = NULL` → se un giorno aggiungono canale voice, il fallback `buildSystemPrompt('SUPPORT', 'VOIP_3CX', ...)` non funziona (SUPPORT non ha `voice` key in `PROMPTS` const di basePrompts.js). Fix: AGI-4 popola `system_prompt_voice` per ogni ARIA_SUPPORT_<PRODUCT>.
### 3.11 Tenant context injection (MUST)
Pattern canonical (`buildSystemPrompt` riga 207-216):
```js
if (tenantConfig.agentName) prompt = prompt.replace(/Sei ARIA/, `Sei ${tenantConfig.agentName}`);
if (tenantConfig.companyName) prompt += `\n\nAZIENDA: ${tenantConfig.companyName}`;
if (tenantConfig.greeting) prompt += `\nSALUTO INIZIALE: ${tenantConfig.greeting}`;
```
**Regole**:
- Sostituzione `agentName` rispetta convention §2 (Nome persona, non ruolo)
- `companyName` proviene da `tenants.config.brandName` (DB hub-ms)
- `greeting` opzionale — se assente, AI usa pattern fisso "Ciao, come posso aiutarti?"
### 3.12 Cost tracking obbligatorio (MUST bloccante)
Pattern obbligatorio per ogni chiamata `anthropicService.chat()` o `runAgentLoop()`:
```js
const result = await anthropicService.chat(messages, systemPrompt, tools, {
model: anthropicService.MODELS.SONNET, // §3.3 esplicito
caller: 'support-trpg', // identificativo per log
sessionId: session.sessionKey, // tracciabilità sessione
productSlug: 'trpg', // attribuzione cost-sdk
tenantId: session.tenantId, // per metering tenant
sessionType: 'support', // 'support'|'sales'|'gaia'|'demo'|'voice_dialog'
});
```
**Regole**:
- `caller` MUST (string)
- `sessionId` MUST (string univoca per sessione)
- `productSlug` MUST (lowercase code prodotto)
- `tenantId` SHOULD (se context tenant disponibile)
- `sessionType` SHOULD (per analytics segmentation)
- Cost reporter è fire-and-forget (no impact su latency) — vedi `anthropicService.js:96-110`
**Impatto AGI-4 retrofit**: ogni caller esistente che NON passa `caller+sessionId+productSlug` è non conforme. Fix richiesto in:
- `voiceDialogController.js:144-150` — già conforme ✅
- `gaiaController.js:580-584,683-685` — già conforme ✅
- `publicDemoController.js:171-178,184-189` — già conforme ✅
- `sessionService.js:127-133`**NON conforme** ❌ (`runAgentLoop` chiamato senza opts) → fix in AGI-4
---
## §4 Quattro sub-template (famiglia AI di prodotto)
Ogni AI di prodotto deve appartenere a uno dei 4 sub-template. Sub-template definiscono: tool list canonical, KB binding, modello, tone, channel.
### 4.1 Sub-template "Support post-vendita"
**Reference implementation**: ARIA_SUPPORT_TRPG ([`supportPrompts.js`](../nexus-ai-ms/src/prompts/supportPrompts.js))
| Elemento | Valore canonical |
|---|---|
| Persona | "ARIA" (Aria nel parlato) |
| Obiettivo primario | Risolvere il problema dell'utente PRIMA di creare ticket |
| Modello | Sonnet (default) |
| KB binding | `product_knowledge` cat IN ('FAQ','HOW_TO','TROUBLESHOOT','KNOWN_ISSUE','TRAINING') + PRODUCT_CARD pinned |
| Tool list | `search_knowledge_base`, `forward_to_expert`, `escalate_to_ticket` (3) |
| Channel | text (canale voice futuro pending) |
| FSM | NO |
| Tenant context | obbligatorio (tenant esiste post-login) |
| Multi-livello KB | L0 + L1 + L2 (filtra `tenant_id`) |
| Anti-pollution | si — solo articoli APPROVED + active |
**Distingue formazione vs bug** (regola business critica):
- FORMAZIONE: "non so dove si trova", "come faccio a", ... → `forward_to_expert`
- BUG: "da errore", "non funziona", "crash", ... → `escalate_to_ticket`
### 4.2 Sub-template "Sales pre-vendita"
**Reference implementation**: Anna su [`publicDemoController.js`](../nexus-ai-ms/src/controllers/publicDemoController.js) (`/public/demo-lead-trpg`)
| Elemento | Valore canonical |
|---|---|
| Persona | "Anna" (canonical cross-prodotto) |
| Obiettivo primario | Presentare prodotto + qualificare lead + lead capture (email/contatto) |
| Modello | Haiku (volume alto, costo critico) |
| KB binding | PRODUCT_CARD pinned (obbligatoria) + `getRelevantHelpArticles` cat IN ('HOW_TO','URL_CHUNK','FAQ','DOC_CHUNK') |
| Tool list | (base): nessun tool. (demo agent): `show_feature`, `run_macro`, `query_product_state` (3 demo tool) |
| Channel | text (chat web) — voice futuro |
| FSM | NO |
| Tenant context | NO (lead-facing pubblico, no auth) |
| Multi-livello KB | SOLO L0 + L1 (NO L2 dati cliente — vincolo GDPR §8) |
| Anti-pollution | si + filter `category!='TROUBLESHOOT'` (no info bug interni) |
| Lead capture | dopo 4-5 turni: `"Ti mando una demo personalizzata. Come ti contatto?"` |
| Demo mode flag | `demo_enabled=true` per attivare 3 demo tool (iframe pilot) |
### 4.3 Sub-template "Plan-mode dialog approver" (Gaia)
**Reference implementation**: Gaia TRPG ([`gaiaController.js`](../nexus-ai-ms/src/controllers/gaiaController.js))
| Elemento | Valore canonical |
|---|---|
| Persona | "Gaia" (TRPG pilota; nome alternativo per altri prodotti TBD) |
| Obiettivo primario | Dialogare con approver umano per raffinare proposta diagnosi ticket PENDING_APPROVAL |
| Modello | dinamico: URGENT/HIGH→Opus, default→Sonnet |
| KB binding | PRODUCT_CARD pinned + RAG keyword top-3 articoli `product_knowledge` |
| Tool list | 11 tool readonly via devenv-gateway: `read_file`, `grep`, `list_dir`, `git_log`, `git_blame`, `run_tests`, `dry_run_sql`, + side-effect: `update_proposal`, `mark_ready`, `request_approver_decision`, `trigger_apply` |
| Channel | voice (Charlotte ElevenLabs `XB0fDUnXU5powFXDhCwa` — drift Sarah da chiudere VOX) |
| FSM | NO (state machine session in-memory) |
| Tenant context | obbligatorio (ticket scoped) |
| Multi-livello KB | L0 + L1 + L2 (tenant del ticket) |
| Stallo detection | dopo 12 turni / 15 min / 3 request_approver_decision → status `STALLED` + ticket → PENDING_HUMAN_REVIEW |
| Anti-loop | max 60 tool call per sessione |
| Persistenza | in-memory `sessions Map` (TODO: persistere in `gaia_sessions` per metrics §metrics endpoint) |
**Caratteristica unica**: ha tool readonly che chiamano `devenv-gateway` (porta 4220 nel container DevEnv prodotto) per ispezionare codice/tests/SQL. Pattern non replicabile direttamente su altri prodotti senza scaffold gateway dedicato.
### 4.4 Sub-template "Lead qualification verticali"
**Reference implementation**: Giulia (7 agent EDUCATION, basate su `basePrompts.js` PROMPTS const)
| Elemento | Valore canonical |
|---|---|
| Persona | "Giulia" (EDUCATION), "ARIA Health/Legal/Realty" (verticali pending — 0 record DB) |
| Obiettivo primario | Qualificare lead tramite FSM verticale-specifica (D1-D23 EDUCATION, H1-H8 HEALTH, L1-L8 LEGAL, R1-R9 REALTY) |
| Modello | Sonnet (tool use multi-step) |
| KB binding | nessuno (verticali pre-vendita non hanno KB di dominio prodotto) |
| Tool list | 5 base FSM: `fsm_transition`, `complete_scoring`, `open_ticket`, `schedule_callback`, `flag_high_priority` |
| Channel | text + voice (entrambi configurati con `system_prompt_voice` separato) |
| FSM | OBBLIGATORIA (config FSM via `fsmConfigId` foreign key) |
| Tenant context | obbligatorio |
| VOICE_RULES | OBBLIGATORIE se canale voice |
| FSM_RULES | OBBLIGATORIE sempre |
**Verticali ENUM ammessi** in `ai_agent_profiles.vertical`: `EDUCATION`, `HEALTH`, `LEGAL`, `REALTY` (proposta v1.1: aggiungere `SUPPORT`, `SALES`, `PLAN_MODE`, `META` per coprire altri sub-template — oggi vertical=`SUPPORT` è hack).
---
## §5 KB binding multi-livello (L0 / L1 / L2 / L3)
### 5.1 Definizione livelli
| Livello | Scope | Tenant ID | Esempio TRPG | Owner aggiornamento |
|---|---|---|---|---|
| **L0 SYSTEM** | Cross-prodotto, regole AgileHub generali | `tenant_id=1` (system) + `product='AGILEHUB'` | "Cosa è plan vs bypass mode", "Come funzionano i ticket" | Agile AI |
| **L1 FIRM** | Per prodotto, dominio applicativo (no dati cliente) | `tenant_id=1` (system) + `product=<PROD>` | TRPG: "Cos'è D.P.R. 151/2011" | Agile AI + esperto verticale prodotto |
| **L2 COMPANY** | Per tenant cliente specifico | `tenant_id != 1` + `product=<PROD>` | TRPG Tremolada (`tenant_id=3`): 294 articoli formula bonus | Tenant admin + Agile AI validation |
| **L3 PERSONA** ⭐ futuro | Per avatar specifica (vestizione) | `tenant_id != 1` + `product=<PROD>` + `knowledge_filter_slug='<slug>'` | "Anna Tremolada Consulente Senior TRPG" → solo articoli marcati `trpg-tremolada-consulente` | Agile AI + VOX (Lotto F Avatar Registry) |
### 5.2 PRODUCT_CARD pinned obbligatoria
Ogni prodotto MUST avere **esattamente 1** record `product_knowledge` con:
- `category='PRODUCT_CARD'`
- `tenant_id=1` (L1 base)
- `active=TRUE`, `review_status='APPROVED'`
- `answer` = JSON valido struttura `{description, scope[], target_buyer, free_markdown}`
**Anti-pollution check** (per audit AGI-4): query
```sql
SELECT product, COUNT(*) as cards, SUM(JSON_VALID(answer)) as valid_json
FROM product_knowledge
WHERE category='PRODUCT_CARD' AND active=TRUE
GROUP BY product
HAVING cards != 1 OR valid_json != cards;
```
**Drift attuale (audit 2026-04-23)**:
- AGILEHUB: **3** PRODUCT_CARD (id 720, 729, 730) tutti con `JSON_VALID=0` → fix AGI-4: UPDATE category='HOW_TO' (sono articoli AWE M1/M2/Orchestration mal-categorizzati)
- TRPG: 1 PRODUCT_CARD ✅ valido
- LG231/ALLTAX/SUSTAINAI/NIS2: **0** PRODUCT_CARD ❌ → fix AGI-4: scaffold scheda base
### 5.3 Schema `product_knowledge` esteso (proposta migration)
Schema attuale (verificato Hetzner 2026-04-23):
```
id, tenant_id, product, category, question, answer, tags JSON, keywords,
source ENUM, source_type ENUM, source_url, source_file_path, source_source_id,
content_hash, fetched_at, chunk_index, chunk_total,
learned_from_session_id, learned_from_ticket_id, expert_id,
review_status ENUM, views, helpful, language, active, created_at, updated_at
```
**Migration proposta v1.0 → v1.1** (NON applicare in v1.0, sub-approval separata):
```sql
ALTER TABLE product_knowledge
ADD COLUMN kb_level ENUM('L0','L1','L2','L3') NULL DEFAULT NULL AFTER product,
ADD COLUMN knowledge_filter_slug VARCHAR(64) NULL DEFAULT NULL AFTER kb_level,
ADD INDEX idx_kb_level (kb_level),
ADD INDEX idx_knowledge_filter (knowledge_filter_slug);
```
Backfill `kb_level`:
- `tenant_id=1 AND product='AGILEHUB'``L0`
- `tenant_id=1 AND product!='AGILEHUB'``L1`
- `tenant_id!=1 AND knowledge_filter_slug IS NULL``L2`
- `tenant_id!=1 AND knowledge_filter_slug IS NOT NULL``L3` (futuro Lotto F)
### 5.4 Retrieval keyword (oggi) → Voyage AI embedding (futuro)
**Oggi** (LIVE produzione):
- `getRelevantHelpArticles(product, query, K)` ([`productKbSnippet.js`](../nexus-ai-ms/src/services/productKbSnippet.js))
- Tokenizzazione naive: split query in token >3 char, scoring `LIKE %token%` su `keywords` (peso 3) + `question` (peso 2) + `answer` (peso 1)
- Order by `score DESC, views DESC LIMIT K`
- **Limiti**: nessun semantic match (sinonimi, riformulazioni), no re-ranking, latency dipende da MySQL
**Roadmap v1.1+** (decision Agile AI + utente):
- **Opzione A**: Voyage AI embedding service ($0.10/M token) → vector column → cosine similarity
- **Opzione B**: rimanere keyword + aggiungere stemming italiano + sinonimi manuali
- **Opzione C**: Claude Haiku re-ranking layer (cross-encoder LLM) sul top-N keyword
- **Trigger decisione**: quando articoli per prodotto > 100 con KB binding multi-tenant attivo
### 5.5 Anti-pollution principle
Memoria persistente `feedback_no_demo_data_production.md`:
> Articoli `product_knowledge` seedati con contenuti fuori scope inquinano RAG ranking.
**Regole vincolanti**:
1. NO articoli demo/test in produzione: usare `product='TEST_DEMO'` per testing
2. NO PRODUCT_CARD multipli per stesso `(tenant_id, product)`
3. NO PRODUCT_CARD con `JSON_VALID(answer)=0`
4. NO articoli con `language` diverso dal default tenant senza override esplicito
5. Spot check periodico Agile AI (almeno mensile + post AGI-4 retrofit)
### 5.6 L3 PERSONA forward-looking (vestizione avatar)
Concetto introdotto sessione 33 part 4 (visione utente Avatar Registry):
```
AVATAR TECNICO + VESTIZIONE = PERSONA PUBBLICA
(voice_id + (display_name + visibile cliente
replica_id) knowledge_id + "Anna Tremolada
system_prompt + Consulente Senior")
cost_per_minute)
```
**Implementazione Lotto F (settimane 2-3 master plan v2.1)** — coordinata con VOX Lotto A foundation:
1. Migration `ai_profiles` (nexus-presenter-ms) ADD COLUMN `knowledge_id VARCHAR(64) NULL` (slug logico filtro KB)
2. Migration `product_knowledge` ADD COLUMN `knowledge_filter_slug VARCHAR(64) NULL` (vedi §5.3)
3. Modify `getRelevantHelpArticles` per accettare `knowledge_filter_slug` opzionale: `WHERE (knowledge_filter_slug IS NULL OR knowledge_filter_slug = ?)`
4. Ranking weight per livello: L0=0.5, L1=1.0, L2=1.5, L3=2.0
5. Seed esempi:
- Avatar "Anna Generica" → `knowledge_id='agilehub-default'`
- Avatar "Anna Tremolada Consulente TRPG" → `knowledge_id='trpg-tremolada-consulente'`
- Articoli AGILEHUB esistenti → marcare `knowledge_filter_slug='agilehub-default'`
- Articoli TRPG L2 Tremolada → marcare `knowledge_filter_slug='trpg-tremolada-consulente'`
**Coordinamento**:
- VOX: schema `ai_profiles` esteso (Lotto A) + voice integration
- MAESTRO: blocchi AWE `D02_LoadAvatarKB(slug)` + `C09_RetrieveFromKB(query, knowledge_id, livello)`
- PRISMA: UI tab agenti `/admin/agents` con KB binding selector (Lotto C, approval gate)
**Status v1.0**: documentato come `PROPOSED` — implementazione conforme richiesta da v1.1 dopo deploy Lotto F.
---
## §6 Tool list standard
### 6.1 Catalogo NEXUS_TOOLS (11 tool)
Da [`nexusTools.js`](../nexus-ai-ms/src/tools/nexusTools.js):
| # | Tool | Family | Side-effect | Eseguito da |
|---|---|---|---|---|
| 1 | `fsm_transition` | FSM | Sì (FSM state) | nexus-fsm-ms via `ToolExecutor.fsmTransition` |
| 2 | `complete_scoring` | FSM | Sì (lead score) | nexus-lead-ms (futuro; oggi log) |
| 3 | `open_ticket` | FSM | Sì (ticket new) | nexus-ticket-ms (futuro; oggi log) |
| 4 | `schedule_callback` | FSM | Sì (calendario) | log only oggi |
| 5 | `flag_high_priority` | FSM | Sì (lead flag) | log only oggi |
| 6 | `search_knowledge_base` | Support | NO (read-only) | nexus-ticket-ms `/product-knowledge/search` |
| 7 | `forward_to_expert` | Support | Sì (expert notification) | nexus-ticket-ms `/support-sessions/:id/forward-expert` |
| 8 | `escalate_to_ticket` | Support | Sì (ticket new) | nexus-ticket-ms `/support-sessions/:id/escalate` |
| 9 | `show_feature` | Demo | Sì (iframe message) | client postMessage via `demoActions[]` accumulator |
| 10 | `run_macro` | Demo | Sì (iframe message) | client postMessage via `demoActions[]` accumulator |
| 11 | `query_product_state` | Demo | NO (read-only) | placeholder MVP — futuro live state pull |
### 6.2 Tool list per famiglia (matrice sub-template × tool)
| Tool | Support | Sales | Plan-mode | Lead qualif |
|---|---|---|---|---|
| `fsm_transition` | NO | NO | NO | **MUST** |
| `complete_scoring` | NO | NO | NO | **MUST** |
| `open_ticket` | NO | SHOULD (lead capture) | NO | SHOULD |
| `schedule_callback` | NO | SHOULD | NO | SHOULD |
| `flag_high_priority` | NO | NO | NO | SHOULD |
| `search_knowledge_base` | **MUST** | NO | NO | NO |
| `forward_to_expert` | **MUST** | NO | NO | NO |
| `escalate_to_ticket` | **MUST** | NO | NO | NO |
| `show_feature` | NO | SHOULD se `demo_enabled` | NO | NO |
| `run_macro` | NO | SHOULD se `demo_enabled` | NO | NO |
| `query_product_state` | NO | SHOULD se `demo_enabled` | NO | NO |
| `read_file`/`grep`/`list_dir`/`git_log`/`git_blame` (custom Gaia) | NO | NO | **MUST se Plan-mode** | NO |
| `run_tests`/`dry_run_sql` (custom Gaia) | NO | NO | SHOULD se Plan-mode | NO |
| `update_proposal`/`mark_ready`/`request_approver_decision`/`trigger_apply` (custom Gaia) | NO | NO | **MUST se Plan-mode** | NO |
### 6.3 Estensioni custom (Gaia readonly via devenv-gateway)
Pattern Gaia (TRPG pilota, [`gaiaController.js:228-250`](../nexus-ai-ms/src/controllers/gaiaController.js)):
```js
const PRODUCT_GATEWAYS = {
TRPG: {
url: process.env.TRPG_GATEWAY_URL || 'http://trpg-pro-agile-devenv:4220',
key: process.env.TRPG_GATEWAY_KEY || process.env.DEVENV_GATEWAY_KEY,
displayName: 'TRPG',
},
};
async function callGateway(product, toolName, args) {
const gw = PRODUCT_GATEWAYS[product];
// POST a http://<gateway>/tools/<toolName> con X-Internal-Key
}
```
**Regole estensione custom tool**:
- Tool con prefisso `read_*`/`grep`/`list_*`/`git_*` SHOULD essere readonly
- Tool con side-effect su prodotto target richiedono devenv-gateway dedicato per quel prodotto
- Spec contratto separato (es. `docs/SPEC_DEVENV_GATEWAY_TOOLS.md` futuro)
- Audit obbligatorio (vedi `gaia_audit_log` per Gaia)
### 6.4 Cost estimation per tool (D6 metering)
Tool che usano LLM hanno cost esplicito tracciato via cost-sdk. Tool che usano solo HTTP/DB → cost trascurabile, no metering.
Esempio Gaia (per session):
- LLM cost: cumulato via `runAgentLoop``result.totalUsage.{inputTokens,outputTokens}` × pricing modello
- Tool readonly cost: trascurabile (HTTP locale)
- Tool `dry_run_sql`: trascurabile (snapshot readonly DB)
---
## §7 Guardrails minimi obbligatori
### 7.1 Pattern canonical LIMITI ASSOLUTI
Sezione 7 system_prompt deve sempre presente, formato:
```
LIMITI ASSOLUTI:
- Non <azione vietata 1>
- Non <azione vietata 2>
...
```
### 7.2 Limiti minimi per famiglia
**Support post-vendita**:
- Non inventare funzionalità non presenti nella KB
- Non chiedere informazioni tecniche complesse (log, stack trace) — quello lo farà il team tecnico
- Non superare 6 scambi senza proporre `forward_to_expert` o `escalate_to_ticket`
- Se la KB non ha risultati, dirlo onestamente
**Sales pre-vendita**:
- Non inventare feature non presenti nella scheda
- Non garantire prezzi non confermati ("dipende dal volume — te lo vedo dopo")
- Non promettere SLA/disponibilità senza verifica
- Non sminuire prodotti competitor (etica commerciale)
- Tool MAI proattivamente — solo su richiesta esplicita utente (regola FERREA `demo_enabled`)
**Plan-mode dialog approver (Gaia)**:
- Non modificare mai il codice sorgente del prodotto (tool readonly only)
- Non eseguire SQL non-SELECT (`dry_run_sql` enforced backend)
- Ammettere quando non si sa
- Non chiamare `trigger_apply` in altre situazioni che non sia approvazione esplicita approver
**Lead qualification verticali**:
- Non garantire posti/disponibilità senza verifica
- Non indicare prezzi non confermati
- Non promettere esiti concorsi/graduatorie/cause/vendite (per verticale)
- (HEALTH) Non formulare diagnosi, non suggerire farmaci
- (LEGAL) Non dare consulenza legale personalizzata, non citare articoli di legge specifici
- (REALTY) Non garantire prezzi/valutazioni senza sopralluogo
### 7.3 Limiti universali (cross-famiglia, MUST sempre)
- Rispondere SEMPRE in italiano (default; `en` se tenant lo richiede)
- Rispettare scoping multi-tenant (mai rivelare dati altri tenant)
- Non superare il limite turni configurato (anti-loop)
- Non inventare valori KB ("preferisco verificare col team — ti rispondo se mi lasci un contatto")
---
## §8 GDPR — split AI prodotto vs AI marketing
### 8.1 Principio
Memoria persistente `project_ai_prodotto_vs_marketing.md`: due cervelli AI separati per GDPR compliance.
| Tipo AI | Dati accessibili | Audience | Hosting |
|---|---|---|---|
| **AI prodotto** (server-side) | L0 + L1 + L2 (dati cliente reali) | utenti autenticati del cliente | post-login, scoped tenant_id |
| **AI marketing** (pubblico) | L0 + L1 SOLO `category IN ('PRODUCT_CARD','HOW_TO','URL_CHUNK')` filtrati pubblici | lead anonimi su landing | pre-login, no tenant context |
### 8.2 Implementazione filtro
Per **AI Sales pre-vendita** (publicDemoController):
```sql
WHERE product = ?
AND tenant_id = 1 -- L0/L1 only
AND category IN ('PRODUCT_CARD','HOW_TO','URL_CHUNK','FAQ','DOC_CHUNK')
AND review_status = 'APPROVED'
AND active = TRUE
AND (language IS NULL OR language = ?)
```
Per **AI Support post-vendita / Plan-mode** (autenticati):
```sql
WHERE product = ?
AND (tenant_id = 1 OR tenant_id = ?) -- L0/L1 + L2 cliente
AND review_status = 'APPROVED'
AND active = TRUE
```
### 8.3 GDPR Right to be Forgotten
- DELETE articoli L2 con `tenant_id = <cliente_da_dimenticare>`: cascade + audit log
- L3 PERSONA (futuro): DELETE articoli con `knowledge_filter_slug = '<cliente>-<persona>'`
- Endpoint dedicato: `DELETE /ai/products/:product/knowledge?tenant_id=X&confirm=YES_GDPR`
- Backup retention: 30 giorni dopo delete (compliance retention)
### 8.4 Anti-leak vincolante
- AI marketing NEVER deve restituire articoli con `tenant_id != 1`
- AI prodotto NEVER deve restituire articoli di altri `tenant_id` rispetto al chiamante
- Log query KB con `tenant_id` filter applicato (per audit GDPR)
---
## §9 Cost tracking MUST (cost-sdk integration)
### 9.1 Pattern obbligatorio
Vedi §3.12. Chiamata canonical:
```js
const result = await anthropicService.chat(messages, systemPrompt, tools, {
model: anthropicService.MODELS.SONNET,
caller: 'support-trpg', // MUST
sessionId: session.sessionKey, // MUST
productSlug: 'trpg', // MUST
tenantId: session.tenantId, // SHOULD
sessionType: 'support', // SHOULD
});
```
### 9.2 Anti-pattern (NON conforme AGI-4)
```js
// BAD: opts mancanti
const result = await anthropicService.chat(messages, systemPrompt, tools);
// ^ 4° arg vuoto
```
### 9.3 Esempio TRPG conforme
Da `voiceDialogController.js:144-150`:
```js
const result = await anthropicService.chat(messages, systemPrompt, null, {
model: anthropicService.MODELS.HAIKU,
caller: 'voice-dialog',
sessionId,
productSlug: (ticket?.product ? String(ticket.product).toLowerCase() : 'agilehub'),
sessionType: 'voice_dialog',
});
```
Da `gaiaController.js:683-685`:
```js
{ model: preferredModel, caller: `gaia:${priority}`, sessionId: session.sessionId,
productSlug: (session.product || 'agilehub').toLowerCase(), sessionType: 'gaia' }
```
### 9.4 Cost reporter implementation
Vedi `anthropicService.js:6-23` + `shared-libraries/cost-sdk` integration:
- Fire-and-forget (no impact su latency)
- Endpoint: `POST {HUB_URL}/api/cost/log` (nexus-hub-ms Fase 2 LIVE)
- Visibility dashboard: `/admin/cost?month=YYYY-MM` (admin_tenant role)
### 9.5 Telemetria di base (anche senza cost-sdk)
Pattern `anthropicService.js:93-95` (sempre attivo se `opts.caller || AI_USAGE_LOG=1`):
```js
logger.info(`[ai-usage] caller=${opts.caller} model=${model} in=${usage.inputTokens} out=${usage.outputTokens}`);
```
Quindi `caller` è MUST anche per debug/log baseline.
---
## §10 Conformance checklist (per audit AGI-4 retrofit)
### 10.1 Matrice 12 elementi × MUST/SHOULD
Per ogni AI di prodotto (tabella `ai_agent_profiles` + caller controllers):
| # | Elemento | Verifica | Fix se mancante |
|---|---|---|---|
| 1 | Identità persona | `ai_agent_profiles.name` valorizzato + system_prompt riga 1 = "Sei <Nome>" | UPDATE `name` + UPDATE `system_prompt` (header) |
| 2 | Obiettivo dichiarato | `system_prompt` contiene "IL TUO OBIETTIVO" o equivalente | UPDATE `system_prompt` aggiungere sezione 2 |
| 3 | Modello Claude esplicito | grep caller per `opts.model` | Modificare caller con `opts.model: MODELS.X` |
| 4 | System prompt strutturato 7 sezioni | `LENGTH(system_prompt) > 800` AND contiene marker sezioni | UPDATE `system_prompt` con template canonical |
| 5 | KB binding | sub-template Support/Sales/Plan-mode → caller chiama `getProductCardSnippet` + `getRelevantHelpArticles` | Modificare caller con KB inject |
| 6 | Tool list dichiarativa | sub-template !=Lead generico → tool list non vuota nel caller | Modificare caller passando tools |
| 7 | LIMITI ASSOLUTI | `system_prompt` contiene "LIMITI ASSOLUTI" + N bullet | UPDATE `system_prompt` aggiungere sezione 7 |
| 8 | VOICE_RULES (se voice) | `system_prompt_voice` contiene `VOICE_RULES` text | UPDATE `system_prompt_voice` |
| 9 | FSM_RULES (se Lead) | `vertical IN ('EDUCATION','HEALTH','LEGAL','REALTY')``system_prompt` contiene `FSM_RULES` | UPDATE `system_prompt` |
| 10 | Channel-aware | se canale voice supportato → `system_prompt_voice IS NOT NULL` | UPDATE `system_prompt_voice` |
| 11 | Tenant context | caller chiama `buildSystemPrompt(vertical, channel, tenantConfig)` con tenantConfig valorizzato | Modificare caller passare tenantConfig |
| 12 | Cost tracking MUST | `caller` + `sessionId` + `productSlug` in ogni `anthropicService.{chat,runAgentLoop}` call | Modificare caller passando opts |
### 10.2 Score conformance
Per ogni AI di prodotto:
- 12/12 MUST + tutti SHOULD applicabili → **CONFORME**
- 12/12 MUST, alcuni SHOULD mancanti → **CONFORME PARZIALE** ⚠️ (warning, no block)
- < 12 MUST **NON CONFORME** (bloccante AGI-4 sign-off)
### 10.3 Procedura audit AGI-4 per prodotto
1. Agile AI esegue audit checklist → produce `docs/AGI4_GAP_<PRODUCT>.md`
2. Gap report contiene: matrice 12 elementi compilata, fix proposti per ogni gap, effort stimato
3. Sub-approval esplicita utente per applicare fix
4. Apply (modifiche `nexus-ai-ms/src/...`) — pattern §10.3 team roster (modifiche file di prodotto AgileHub stesso, OK)
5. Smoke test: 3-5 query reali AI prodotto post-fix
6. Aggiornamento `ai_agent_profiles` + commit + sub-approval push Gitea
---
## §11 "Agile" — AI runtime di AgileHub (caso speciale)
### 11.1 Scope deciso (sessione 33 part 6, decisione utente AGI-2 domanda 4)
**Entrambi (full)**: una sola "Agile" AI con context routing.
| Context | Trigger | System prompt | Tool list | KB |
|---|---|---|---|---|
| **Admin AgileHub** | Chiamata da pagine `/admin/*` autenticato role `admin_tenant`/`admin_global` | "Agile per amministratori" — governance tone | `query_master_plan`, `query_standard`, `query_agent_status`, `query_workflow_runs`, `query_cost`, `search_kb_agilehub` | L0 AGILEHUB + L1 ADMIN |
| **Utenti finali prodotti** | Chiamata da widget cross-prodotto / pagine `/help`-style | "Agile per utenti" — assistente tone | `search_kb_agilehub`, `escalate_to_support` | L0 AGILEHUB + L1 USER (subset pubblico) |
### 11.2 Naming canonical
- `display_name`: "Agile"
- `agent_key`: `AGILE_RUNTIME` (in `ai_agent_profiles`)
- `vertical`: nuovo ENUM `META` (proposta v1.1 schema migration)
### 11.3 System prompt template "Agile"
```
Sei Agile, l'assistente AI di AgileHub.
[Context routing — admin]
Stai parlando con un amministratore di AgileHub. Aiutalo a:
- Conoscere lo stato del sistema (master plan settimana corrente, agenti specialisti, standard cross-suite, workflow runs AWE, cost mese corrente)
- Trovare articoli nella KB AGILEHUB
- Suggerire azioni concrete (es. "vuoi consultare REGENT per arbitrato?")
[Context routing — utente finale]
Stai parlando con un utente di un prodotto AgileHub. Aiutalo a:
- Capire come usare AgileHub (workflow editor, dashboard, mobile)
- Trovare guide nella KB AGILEHUB
- Escalare a support se serve
[KB inject: top-K articoli AGILEHUB rilevanti]
[Tool list: query_* per admin, search/escalate per user]
[LIMITI ASSOLUTI: non rivelare info di altri tenant; non inventare feature non presenti in KB]
```
### 11.4 Modello Claude
- Default: **Sonnet** (dialogo + tool use)
- Greeting / messaggio singolo: Haiku
- Reasoning profondo (es. "perché workflow run è failed?"): Opus on-demand
### 11.5 KB binding
- L0 AGILEHUB: 52 articoli esistenti (ground truth audit 2026-04-23) — id 720+
- L1 ADMIN/USER: split via nuovo campo `audience ENUM('admin','user','all')` in product_knowledge (proposta v1.1 schema)
- L3 PERSONA: `display_name='Agile Admin'` vs `display_name='Agile Helper'` con stessi voice_id ma KB filter differente (futuro Lotto F)
### 11.6 Implementazione AGI-3 (~5-6gg)
Step:
1. INSERT `ai_agent_profiles` record `AGILE_RUNTIME` con system_prompt template
2. Nuovo controller `agileController.js` con context routing logic
3. Tool dispatcher: `query_master_plan`, `query_standard`, ecc. (5-7 tool nuovi)
4. Endpoint `/ai/agile/chat` con auth detection (role admin → admin context)
5. Widget UI in `/admin/agile` (Agile chat panel — coordinamento PRISMA approval gate)
6. Smoke test: 5 query admin + 5 query user
7. Cost tracking conformance MUST
**Dipendenze AGI-3**:
- AGILE AI design tool list + system prompt template (questo doc + estensione)
- MAESTRO co-owner workflow integration (chiamare blocco AWE da Agile)
- PRISMA reviewer UI exposure (`/admin/agile` panel)
- Sub-approval utente prima del deploy LIVE
---
## §12 Stato adoption per prodotto (snapshot 2026-04-23)
### 12.1 Tabella matrice prodotto × elementi standard
Audit DB Hetzner 2026-04-23 (`SELECT product, count, has_card, kb_levels FROM ...`):
| Prodotto | Tot articoli KB | PRODUCT_CARD | KB livelli presenti | ai_agent_profiles | Score conformance | Note |
|---|---|---|---|---|---|---|
| **AGILEHUB** | 52 | **3 (pollution)** ❌ | L0 only | 0 ❌ | NON CONFORME | Fix AGI-4: UPDATE 2 PRODUCT_CARD → HOW_TO; creare AGILE_RUNTIME (AGI-3) |
| **TRPG** | 295 | 1 ✅ | L1 (1) + L2 (294 Tremolada) | 1 (ARIA_SUPPORT_TRPG) ⚠️ short prompt | CONFORME PARZIALE | Fix: estendere `system_prompt` ARIA_SUPPORT_TRPG a template completo; aggiungere ANNA_SALES_TRPG + GAIA_PLANMODE_TRPG in DB |
| **TRPG-PRO** | 0 | 0 ❌ | nessuno | 0 ❌ | NON CONFORME | Fix: scaffolding L1 base + ai_agent_profiles ARIA_SUPPORT_TRPG_PRO |
| **SUSTAINAI** | 27 | 0 ❌ | L1 (1) + L2 (26 cliente 5) | 1 (ARIA_SUPPORT_SUSTAINAI) ⚠️ short prompt | NON CONFORME | Fix: scaffolding PRODUCT_CARD + estendere system_prompt |
| **NIS2** | 2 | 0 ❌ | L1 (1) + L2 (1 cliente 7) | 1 (ARIA_SUPPORT_NIS2) ⚠️ short prompt | NON CONFORME | Fix: scaffolding KB completo + PRODUCT_CARD |
| **AllTax (TAXAI)** | 83 | 0 ❌ | L2 only (cliente 6) | 1 (ARIA_SUPPORT_ALLTAX) ⚠️ short prompt | NON CONFORME | Fix: scaffolding L1 base + PRODUCT_CARD |
| **LG231** | 91 | 0 ❌ | L1 (1) + L2 (90 cliente 8) | 1 (ARIA_SUPPORT_LG231) ⚠️ short prompt | NON CONFORME | Fix: scaffolding PRODUCT_CARD + estendere system_prompt |
| **DFM-PRO** | 0 | 0 ❌ | nessuno | 0 ❌ | NON CONFORME | Scope completo retrofit AGI-4 |
| **ALLRISK** | 0 | 0 ❌ | nessuno | 0 ❌ | NON CONFORME | Scope completo retrofit AGI-4 |
| **WMS** | 0 | 0 ❌ | nessuno | 0 ❌ | NON CONFORME | Scope completo retrofit AGI-4 |
| **MADEBYCLOUD** | 0 | 0 ❌ | nessuno | 0 ❌ | NON CONFORME | Fuori scope AGI-4 master plan v2.1 |
| **CertiSource** | 0 | 0 ❌ | nessuno | 0 ❌ | NON CONFORME | Fuori scope AGI-4 master plan v2.1 |
### 12.2 Drift cross-prodotto da chiudere
| Drift | Affected | Fix |
|---|---|---|
| `system_prompt` ARIA_SUPPORT troppo corto (220-300 char vs template ~3000 char) | TRPG, SUSTAINAI, ALLTAX, NIS2, LG231 | AGI-4: UPDATE `system_prompt` con `buildSupportPrompt(<PRODUCT>)` rendered text |
| `system_prompt_voice` NULL su tutti ARIA_SUPPORT | tutti | AGI-4: popolare `system_prompt_voice` se voice supportato |
| Anna Sales non in `ai_agent_profiles` (hardcoded controller) | tutti i prodotti con landing demo | AGI-4: INSERT `ANNA_SALES_<PRODUCT>` per ogni prodotto con landing |
| Gaia Plan-mode in-memory (no record DB) | TRPG (pilota) | AGI-4 (post AGI-5): persistere config in `ai_agent_profiles` `GAIA_PLANMODE_TRPG` + tabella `gaia_sessions` per metrics |
| 7 Giulia EDUCATION non hanno fsm_config_id valorizzato (`has_fsm=0` query DB) | tutti i 7 Giulia | AGI-4: link FSM config |
| Cost tracking opzionale in `sessionService.js:127` | tutti i caller `runAgentLoop` legacy | AGI-4: passare opts cost-sdk |
### 12.3 Priorità retrofit AGI-4
Settimane 3-4 master plan v2.1 (~7-10gg):
| Settimana | Prodotti | Effort |
|---|---|---|
| 3 (8-14/5) | AGILEHUB (fix pollution + creare Agile AGI-3) | 5gg |
| 3-4 (12-21/5) | TRPG estendere ARIA_SUPPORT + creare ANNA_SALES + GAIA_PLANMODE | 2gg |
| 4 (15-21/5) | SUSTAINAI + NIS2 + ALLTAX + LG231 (fix existing ARIA_SUPPORT + scaffold PRODUCT_CARD) | 3gg |
| 5 (22-28/5) | DFM-PRO + ALLRISK + WMS (scope completo) | 4gg |
| Backlog v1.1 | MADEBYCLOUD + CertiSource | TBD |
---
## §13 Cronologia versioni
| Data | Versione | Modifica | Owner |
|---|---|---|---|
| 2026-04-23 | 1.0 (proposed) | Creazione standard. Sub-template Support + Sales + Plan-mode + Lead qualif. KB multi-livello L0/L1/L2 + L3 PERSONA forward-looking. Cost tracking MUST. Naming convention netto (Agile vs Agile AI vs Anna vs Gaia). Conformance checklist 12 elementi. Tabella adoption snapshot 12 prodotti. | Agile AI (AGI-2 master plan v2.1) |
| _(future)_ | 1.1 | Migration `kb_level` + `knowledge_filter_slug` + `audience` columns. ENUM vertical esteso (`META`, `SUPPORT`, `SALES`, `PLAN_MODE`). L3 PERSONA promosso da `PROPOSED` a STANDARD. Embedding service decision. | TBD |
---
**Documento mantenuto da**: Agile AI (Architetto AI di prodotto + KB governance)
**Distribuzione**: in attesa sub-approval utente per seed `nexus_hub.hub_standards` slug=`ai-prodotto` v1.0 + distribuzione cross-suite (pattern `installer-integration`)
**Riferimento operativo**: [`docs/AGENT_AGILE_AI.md`](AGENT_AGILE_AI.md) v1.0 + [`docs/BRIEFING_AGILE_AI.md`](BRIEFING_AGILE_AI.md) v1.0
**Master Plan link**: [`docs/MASTER_PLAN_GO_LIVE_4_WEEKS.md`](MASTER_PLAN_GO_LIVE_4_WEEKS.md) v2.1 sezione AGI-1...AGI-6

View File

@ -0,0 +1,246 @@
# STANDARD AgileHub — Email Relay centralizzato (canonico)
> **Autorità**: AgileHub (single source of truth per la suite Agile Software).
> **Versione**: 1.0 — 2026-04-21
> **Stato**: ADOTTATO. Vincolante per tutti i prodotti della suite.
> **Destinatari**: team TRPG, SustainAI, NIS2, TAXAI (AllTax), LG231, DFM, MKTG, ALLRISK, WMS, MADEBYCLOUD, CertiSource.
> **Registry DB**: record master in `nexus_hub.hub_standards` (slug=`email-relay`, version=`1.0`).
---
## 1. Principio
**Tutte le email generate dalla suite AgileHub passano da UN SOLO punto**: il microservizio `email-automation-ms` (PM2 id 21, porta 4004).
Nessun prodotto, microservizio, container DevEnv, cron o agent AI può accedere direttamente a:
- Postfix host Hetzner (`172.18.0.1:25`)
- Server Gmail (`smtp-relay.gmail.com`, `smtp.gmail.com`)
- Altri provider SMTP (SendGrid, Mailgun, AWS SES, ecc.)
---
## 2. Architettura
```
┌──────────────────┐ HTTPS + X-Internal-Key
│ Prodotto cliente │──────────────┐
│ (TRPG/SustAI...) │ │
└──────────────────┘ ▼
┌──────────────────────┐
┌──────────────────┐ │ email-automation-ms │ SMTP (internal)
│ Microservizio │────▶│ porta 4004 │──────────┐
│ AgileHub │ │ │ ▼
│ (ticket/lead/..) │ │ Rate limit + Template│ ┌──────────────┐
└──────────────────┘ │ Audit DB + Retry │ │ Postfix host │
└──────────────────────┘ │ 172.18.0.1:25│
└──────┬───────┘
│ SASL auth
┌──────────────┐
│ Gmail │
│ smtp-relay │
│ .gmail.com │
└──────────────┘
```
**Relay chain**:
- Prodotto → `email-automation-ms:4004` (via Apache vhost `agilehub.agile.software/api/emails/*`)
- `email-automation-ms` → Postfix `172.18.0.1:25` (container→host, senza auth perché `mynetworks` include `172.18.0.0/16`)
- Postfix → Gmail SMTP relay `:587` (richiede SASL auth service-account Workspace)
---
## 3. Contratto API
### Endpoint canonico
```
POST https://agilehub.agile.software/api/emails/send
Headers:
X-Internal-Key: <INTERNAL_EMAIL_KEY> (shared via /etc/agilehub/internal-keys.env)
Content-Type: application/json
Body:
{
"to": "user@example.com", // string o array
"from_tenant_id": 5, // int — attribuzione log
"template": "demo-registration-verify", // nome template in email-automation-ms/templates/
"variables": { "name": "Mario", ... }, // placeholder per il template
"product": "TRPG", // slug prodotto (audit)
"reply_to": "support@trpg.agile.software", // opzionale
"priority": "transactional" // "transactional" | "marketing" | "system"
}
```
### Response
```json
{
"success": true,
"message_id": "<agilehub-7c3f-20260421@smtp-relay.gmail.com>",
"queued_at": "2026-04-21T15:30:00.000Z"
}
```
### Log audit
Ogni invio viene registrato in `nexus_email_db.email_log`:
- `id, message_id, to, from, subject, template, product, tenant_id, status, sent_at, smtp_response, retry_count`
### Status delivery
- `queued` — accettato dal service, in coda retry
- `sent` — accettato dal Postfix host (risposta 250 OK)
- `delivered` — accettato dal Gmail relay (webhook o check log mail.log)
- `bounced` — Gmail ha rifiutato (bad recipient, spam, auth failure)
- `failed_permanent` — 5xx, no retry
- `failed_transient` — 4xx, retry backoff (max 3)
---
## 4. VIETATO
Qualsiasi alternativa al percorso `email-automation-ms`. In particolare:
**Connessione SMTP diretta** a `172.18.0.1:25` da container prodotto o DevEnv.
**Provider cloud** (SendGrid, Mailgun, SES, Postmark) — se necessari, integrarli DENTRO `email-automation-ms` come alternative transport.
**Librerie SMTP client** in codice prodotto:
- JS: `nodemailer`, `emailjs`, `smtp-connection`
- PHP: `PHPMailer`, `Swift_Mailer`, `mail()` built-in, `mb_send_mail()`
- Python: `smtplib` usato con credenziali cleartext
- Ruby, Go, ecc.: equivalenti
**Binari di sistema** in container: `sendmail`, `mail`, `mutt`, `msmtp`.
**File .env con `SMTP_*`** in qualsiasi servizio diverso da `email-automation-ms`.
### Deroghe
Solo via ticket AgileHub tag `email-bypass-exception`. Motivazioni accettabili:
- Integrazione legacy in dismissione con data di fine (es. mktg-agile oggi)
- Volume massimo > 100k/mese che giustifica provider dedicato (richiede integrazione in email-automation-ms, non bypass)
- Test E2E isolati (usa container mailpit dedicato, non produzione)
Deroghe hanno scadenza obbligatoria (max 90gg). AgileHub tiene registro in `nexus_hub.hub_standards_adoption.exemption_reason/exemption_expires_at`.
---
## 5. Come integrare
### Lato prodotto (TRPG, SustainAI, ecc.)
1. Rimuovere ogni `SMTP_*` dal `.env` del prodotto
2. Aggiungere `INTERNAL_EMAIL_KEY=<value>` al `.env` (coordinato con team AgileHub)
3. Sostituire chiamate dirette con `curl`/`fetch`/`requests` verso `https://agilehub.agile.software/api/emails/send`
4. Rimuovere dipendenze SMTP (`nodemailer`, `PHPMailer`, ecc.) dal manifest (`package.json`, `composer.json`)
### Lato AgileHub
1. `email-automation-ms` espone `/send` + `/templates/list` + `/webhooks/bounce`
2. Gestisce rate limit per tenant (`nexus_email_db.email_quota`)
3. Tiene in vita la connessione SMTP a Postfix (connection pool)
4. Retry exponential backoff su 4xx (1min, 5min, 30min), drop su 5xx
5. Webhook bounce da Gmail (future) → aggiorna `status=bounced` + notifica tenant admin
---
## 6. Stato attuale (2026-04-22 — FIX APPLICATO)
### ✅ Operativo end-to-end
- Architettura centralizzata implementata e funzionante
- IP allowlist Workspace attiva per `135.181.149.254` (configurata da Massimo Tagliavini)
- `sendmail` locale: funziona (cron watchdog, claude-auth-check, ticket-agent)
- **`email-automation-ms` canonical path**: funziona dopo fix 2026-04-22
- Endpoint protetti da `X-Internal-Key` (auth middleware aggiunto)
- Env vars propagate in `/etc/agilehub/internal-keys.env` + `.env` di 5 prodotti produzione
### Fix applicato al transport Nodemailer
File: `email-automation-ms/src/services/emailService.js:25-40`
```javascript
transporter = nodemailer.createTransport({
host, // da SMTP_HOST=127.0.0.1 (era 172.18.0.1)
port,
secure,
name: process.env.SMTP_HELO_NAME || 'agile.software', // HELO esplicito
...
});
```
File: `email-automation-ms/.env`
```
SMTP_HOST=127.0.0.1 # era 172.18.0.1 (docker bridge) → Postfix vedeva connessione "esterna"
INTERNAL_SERVICE_KEY=nexus-internal-2026 # alias INTERNAL_EMAIL_KEY
```
### Auth endpoint
File: `email-automation-ms/src/index.js:27-55` — middleware `requireInternalKey`:
- Protegge: `/emails/send`, `/emails/send-raw`, `/sequences/*`, `/emails/templates/db` (POST/PUT/DELETE)
- Legge la chiave da `INTERNAL_EMAIL_KEY` (preferita) o `INTERNAL_SERVICE_KEY` (retrocompat cron)
- Senza header → 401 UNAUTHORIZED
- Con header valido → passa al controller
- Se nessuna chiave configurata → warn + pass (fallback legacy)
### Test E2E superati (2026-04-22)
| Test | Risultato |
|------|-----------|
| POST `/emails/send-raw` senza X-Internal-Key | 401 ✅ |
| POST `/emails/send-raw` con X-Internal-Key (locale) | 201 + messageId + SENT in DB ✅ |
| POST HTTPS `https://agilehub.agile.software/api/emails/send-raw` da container TRPG | 201 + SENT in DB, product=TRPG ✅ |
| Consegna su Gmail `250 2.0.0 OK` via `smtp-relay.gmail.com:587` | ✅ confermato in `/var/log/mail.log` |
### ❌ Bypass noti
- `/var/www/mktg-agile/.env` contiene ancora `SMTP_*` — tollerato perché prodotto in dismissione Fase C (spegnimento 2026-04-21+)
---
## 7. Obblighi prodotti della suite
Per essere conforme a `email-relay` v1.0 ogni prodotto **DEVE**:
1. ✅ Rimuovere ogni `SMTP_*`, credenziali Gmail, API key mailer-cloud dal proprio `.env`
2. ✅ Eliminare dipendenze mailer (nodemailer, PHPMailer, ecc.) dal manifest
3. ✅ Sostituire ogni invio con POST verso `https://agilehub.agile.software/api/emails/send`
4. ✅ Ottenere `INTERNAL_EMAIL_KEY` da AgileHub team (via ticket tag `email-key-provision`)
5. ✅ Aggiungere `INTERNAL_EMAIL_KEY` al `.env` istanza con chmod 600
6. ✅ Usare template definiti in `email-automation-ms/templates/` (aprire PR per nuovi template)
7. ✅ Loggare `message_id` ricevuto nella response per debug
Un prodotto non conforme entra in `pending` adoption nel registry. Deroghe formali con scadenza obbligatoria.
---
## 8. Riferimenti
- Microservizio: `/var/www/agile-services/email-automation-ms/` (Hetzner) + `/projects/agile-services/email-automation-ms/` (container)
- Entry point: `email-automation-ms/src/index.js:45` (SMTP transport config)
- Template repo: `email-automation-ms/templates/` (Handlebars-like .hbs)
- Log DB: `nexus_email_db.email_log` (MySQL host Hetzner)
- Postfix config: `/etc/postfix/main.cf` (Hetzner host)
- Gmail Workspace Admin Console: account `admin@agile.software`
---
## 9. Changelog
### v1.2 — 2026-04-22 (fix applicato — operativo)
- Fix transport Nodemailer: `SMTP_HOST=127.0.0.1` (era `172.18.0.1` docker bridge) + `name: 'agile.software'` HELO esplicito
- Aggiunto middleware `requireInternalKey` su `/emails/send*`, `/sequences/*`, `/emails/templates/db` mutazioni — chiave `INTERNAL_SERVICE_KEY=nexus-internal-2026` (alias `INTERNAL_EMAIL_KEY`)
- Registrato `INTERNAL_EMAIL_KEY` in `/etc/agilehub/internal-keys.env`
- Propagato `INTERNAL_EMAIL_KEY` + `EMAIL_MS_URL` al `.env` di 5 prodotti produzione (trpg-agile, sustainai-agile, nis2-agile, mktg-agile, allrisk-agile). Altri 5 prodotti (trpg-pro-agile, taxai-agile, lg231-agile, dfm-pro-agile, wms-agile) non hanno `.env` in produzione — useranno la registry centrale.
- Test E2E passati: invio da container TRPG via HTTPS verso `agilehub.agile.software/api/emails/send-raw` → consegnato Gmail 250 OK.
### v1.1 — 2026-04-22 (correzione narrativa, pre-fix)
- Correzione narrativa sezione 6: SMTP NON è bloccato. Il path `sendmail` funziona (Massimo ha configurato IP allowlist da tempo). Il bug reale era in `email-automation-ms` transport.
- Rimosso "fix A/B Postfix SASL" (errore di analisi del 2026-04-21 — non serviva).
### v1.0 — 2026-04-21
- Versione iniziale — formalizza architettura esistente
- Distribuita a 11 prodotti suite via DB registry `hub_standards`
- Richiesta conformità prodotti: rimuovere SMTP diretti dove presenti
---
**Firma autorità**: AgileHub. Violazioni vanno in `hub_standards_distribution_log` con `result='non_compliant'`.

View File

@ -0,0 +1,298 @@
# STANDARD AgileHub — Installer Integration (canonico)
> **Autorità**: AgileHub (single source of truth per la suite Agile Software).
> **Versione**: 1.0.1 — 2026-04-19 (patch: chiarimento payload tenants/report)
> **Stato**: ADOTTATO. Sostituisce qualsiasi proposta prodotto in caso di conflitto.
> **Destinatari**: team TRPG, SustainAI, NIS2, TAXAI (AllTax), LG231, DFM, MKTG, ALLRISK, WMS, MADEBYCLOUD, CertiSource.
> **Spec origine**: `AGILEHUB_INSTALLER_INTEGRATION_SPEC.md` v1.0 (TRPG, 2026-04-19) — recepita con override sotto.
> **Registry DB**: record master in `nexus_hub.hub_standards` (slug=`installer-integration`, version=`1.0.1`). Questo file è una **copia derivata** — in caso di disallineamento vince il DB.
## Changelog
### v1.0.1 — 2026-04-19 (patch, non breaking)
- **§3.1 `POST /api/tenants/report`**: chiarito che `license_key` è **obbligatorio** nel body (era implicito via "firma HMAC obbligatoria" ma non listato nel payload d'esempio). Serve al middleware HMAC per il lookup della `hub_signing_key`. Origine: smoke test TRPG 2026-04-19 21:15 — 404 LICENSE_NOT_FOUND senza license_key. Decisione AgileHub: consistency > minimal payload (defence in depth anti-tamper).
- TRPG già conforme via commit `5c1aa2e` (2026-04-19).
- Altri prodotti: allineare payload `tenants/report` aggiungendo `license_key` string.
### v1.0 — 2026-04-19
- Versione iniziale adottata. Spec origine TRPG recepita con override AgileHub.
---
## 1. Portata e autorità
Questo documento definisce **il contratto unico** fra AgileHub e i prodotti installati on-premise (o white-label) della suite. Vale per:
- Registrazione prodotti in AgileHub (`hub_products`)
- Provisioning istanze cliente (orchestrator SSH + installer script prodotto)
- Licensing + heartbeat + alert
- Onboarding tenant (consulting_firms) cross-istanza
- Firma HMAC + vault SSH
**Regola di precedenza**: in caso di conflitto fra una proposta prodotto (es. `*_INTEGRATION_SPEC.md` nel loro `docs/`) e questo standard, **vince questo standard**. Deviazioni motivate vanno chieste via ticket AgileHub con tag `standard-exception`.
---
## 2. Decisioni AgileHub (override sulla spec TRPG v1.0)
### 2.1 Hostname ufficiale
| Ruolo | Hostname | Uso |
|-------|----------|-----|
| Hub UI | `https://agilehub.agile.software` | Dashboard admin, `/admin/products/*` |
| Hub API (licensing) | `https://agilehub.agile.software/api/license/*` | endpoint validate/activate/register/report |
| Hub API (installer orchestrator) | `https://agilehub.agile.software/api/installer/*` | wizard, job status, log streaming |
> **NO `hub.agile.software`**. I prodotti che chiamano `hub.agile.software` vanno corretti prima del pilota.
### 2.2 Stack tecnico
| Layer | Scelta |
|-------|--------|
| Backend licensing/installer | **Node.js 20 + Express** (pattern `nexus-*-ms`), microservizio nuovo `nexus-hub-ms` porta **4217** |
| DB | **MySQL host Hetzner** (172.18.0.1:3306), DB dedicato **`nexus_hub`**, user `nexus_user` |
| Frontend | **Next.js 15 + React 19** in `nexus-dashboard`, nuova sezione `/admin/products/*` |
| Job queue (Fase 3) | **BullMQ su Redis** (da aggiungere infra). Fino a Fase 2: cron + status polling |
| Email | Microservizio esistente **`email-automation-ms`** (NO SendGrid/MailHog esterni) |
| Secret vault | **AES-256-GCM env-based**, chiave in `/etc/agilehub/vault.env` (NO HashiCorp Vault/KMS) |
| SSH worker | **ssh2** (npm) invocato da `nexus-hub-ms`, chiave decifrata on-demand |
### 2.3 Prefisso tabelle e naming DB
- DB dedicato: `nexus_hub`
- Tabelle: prefisso `hub_` mantenuto (compat con spec origine). Accesso esclusivo da `nexus-hub-ms`.
- Cross-DB query verso `nexus_ticket_db`, `nexus_tenant_db`, `nexus_lead_db` sono permesse solo via GRANT dedicato a `nexus_user` (pattern già in uso per SSO).
### 2.4 Ruoli
| Ruolo | Permessi |
|-------|----------|
| `hub_admin` | Tutto tranne provisioning SSH |
| `hub_installer` | **Nuovo**. Lettura `hub_*`, esecuzione wizard, decrypt chiavi SSH con audit obbligatorio |
| `hub_support` | Solo lettura `hub_instances`, `hub_tenants`, `hub_license_heartbeats` |
Il ruolo `hub_installer` è **richiesto** per Fase 3. Non riutilizzare `hub_admin`: blast radius troppo ampio.
### 2.5 Versioning
Recepita la regola TRPG: **version mai hardcodata nei seed o nelle migration**. La hub la registra da payload heartbeat. Per `hub_products.version` (ultima release disponibile) leggere on-demand via `GET https://<product>.agile.software/version.json` oppure aggiornare via `POST /api/products/:slug/version` (admin-only).
### 2.6 Integrazione obbligatoria con SSO
`hub_tenants.owner_email` **DEVE** essere foreign-key logica verso `sso_identities.email` (DB `nexus_tenant_db`). Al provisioning di un nuovo tenant:
1. `nexus-hub-ms` POST `/api/auth/admin/tenants/create` sull'istanza
2. L'istanza crea `consulting_firm` + `user` locale
3. `nexus-hub-ms` replica l'identità in `sso_identities` (se non esiste)
4. Prima login dell'owner → SSO flow standard (Fase 2 SSO)
**Prodotti che non hanno ancora SSO dual-mode**: finché non è attivo, `hub_tenants` funziona stand-alone ma `owner_email` deve comunque essere univoco cross-istanza.
### 2.7 Deprecazione `products.json`
`/opt/agent-ai/hub/products.json` è **deprecato** dalla Fase 2. Entro la Fase 2 completa:
- `hub_products` diventa la sorgente di verità
- La UI legge da `hub_products`, non più da `products.json`
- `products.json` resta come fallback statico per landing page esterne, generato da cron da `hub_products`
### 2.8 Multi-prodotto da subito
Fase 1 va implementata **generica** (non TRPG-specific). Lo schema, gli endpoint e le firme HMAC supportano `product_slug` variabile fin dal primo giorno. Il seed iniziale è per TRPG, ma il codice non hardcoda nulla.
---
## 3. Contratto API canonico
### 3.1 Endpoint hub (Istanza → AgileHub)
Tutti su `https://agilehub.agile.software/api/...`, header obbligatorio:
```
X-Product-Signature: hmac_sha256(body, HUB_SIGNING_KEY_<PRODUCT_SLUG_UPPER>)
Content-Type: application/json
```
| Endpoint | Scopo | Chiamato da |
|----------|-------|-------------|
| `POST /api/license/activate` | Prima attivazione dopo install | `install-<slug>.sh` |
| `POST /api/license/validate` | Heartbeat periodico (24h default, 1h se anomalia) | cron istanza |
| `POST /api/instances/register` | Registrazione finale istanza | `install-<slug>.sh` |
| `POST /api/tenants/report` | Sync tenant create/update/archive | istanza, on-change |
| `GET /api/products/:slug/version` | Ultima versione disponibile | `update-<slug>.sh` |
Payload: identici alla spec TRPG §6 (validate/activate/register/report). Campi obbligatori aggiuntivi in **tutti** i body:
- `product_slug` — sempre, anche dove la spec TRPG lo omette implicitamente
- `license_key`**sempre** (v1.0.1 clarification). Serve al middleware HMAC per lookup di `hub_signing_key` prima della verifica firma. Include `tenants/report` che nella spec origine non lo listava esplicitamente.
**Eccezione**: `POST /api/license/activate` non porta `X-Product-Signature` (primo handshake, la chiave non esiste ancora) ma deve comunque includere `license_key` per il match iniziale.
### 3.2 Endpoint istanza (AgileHub → Istanza)
Ogni prodotto della suite **deve** esporre:
| Endpoint | Scopo | Auth |
|----------|-------|------|
| `POST /api/auth/admin/tenants/create` | Onboarding tenant da hub | Bearer JWT service + `X-Hub-Signature` |
| `POST /api/auth/admin/tenants/:id/suspend` | Sospensione tenant | idem |
| `POST /api/auth/admin/tenants/:id/resume` | Riattivazione tenant | idem |
| `GET /api/auth/admin/health` | Health check completo per hub | API key service |
**Shape risposta**: identica alla spec TRPG §5.2. Ogni prodotto implementa il proprio endpoint ma lo shape è vincolante.
### 3.3 Installer script non-interattivo (obbligatorio per prodotti orchestrati)
Ogni prodotto della suite che vuole supportare provisioning orchestrato da AgileHub **deve** fornire uno script:
```
install-<slug>.sh --non-interactive --json-output \
--slug=... --domain=... --client-name=... --admin-email=... --admin-name=... \
--license-key=... --hub-url=https://agilehub.agile.software \
--<provider-key-1>=... --<provider-key-2>=... \
[--smtp-host=... ...] [--force-reinstall]
```
**Exit codes standard** (identici per tutti i prodotti):
| Codice | Significato |
|--------|-------------|
| 0 | Success |
| 10 | Preflight failed (docker, RAM, disk, porte) |
| 20 | Secret generation failed |
| 30 | Hub activation failed (license invalid) |
| 40 | Docker load/up failed |
| 50 | Migration DB failed |
| 60 | Seed/admin creation failed |
| 70 | SSL setup failed |
| 80 | Verify-install failed |
| 90 | Internal error |
**JSON output**: shape identica a spec TRPG §5.1. Campo `product_slug` obbligatorio in entrambi i rami (success/failure).
---
## 4. Schema DB canonico (DB `nexus_hub`)
Recepito integralmente dallo schema TRPG §3.1 (8 tabelle):
- `hub_products`
- `hub_instances`
- `hub_instance_provisioning_logs`
- `hub_licenses`
- `hub_license_heartbeats`
- `hub_license_alerts`
- `hub_tenants`
- `hub_ssh_credentials`
**Modifiche AgileHub**:
1. `hub_products.framework` VARCHAR(32) NULL — aggiunto, indica stack prodotto (`php-vanilla`, `node-express`, `nextjs`, ecc.) utile all'orchestrator
2. `hub_instances.sso_enabled` BOOLEAN DEFAULT FALSE — aggiunto, indica se il prodotto ha SSO attivo su quell'istanza
3. `hub_tenants.sso_identity_id` INT NULL — FK logica a `sso_identities.id` (riempito quando SSO attivo)
4. Tutte le tabelle hanno `tenant_id INT NULL` per compat futuro con multi-tenancy hub (oggi NULL, riservato)
Migration canonical: `nexus-hub-ms/migrations/001-initial-schema.sql` (da creare). **Sub-approval obbligatoria** prima dell'esecuzione su produzione (maintenance mode come da CLAUDE.md).
---
## 5. Sicurezza
### 5.1 Firma HMAC
- **Algoritmo**: HMAC-SHA256
- **Chiave**: `HUB_SIGNING_KEY_<PRODUCT_SLUG_UPPER>` (es. `HUB_SIGNING_KEY_TRPG`, `HUB_SIGNING_KEY_SUSTAINAI`)
- **Generazione**: `openssl rand -hex 32`
- **Storage**: in `/etc/agilehub/vault.env` sull'host AgileHub + `.env` di ogni istanza (propagata da installer al momento di `activate`)
- **Rotazione**: ogni 6 mesi, bidirezionale coordinata
### 5.2 SSH vault
- Cifratura: AES-256-GCM
- Master key: `HUB_VAULT_KEY` in `/etc/agilehub/vault.env` (NON in DB, NON in repo git)
- Ogni decrypt logga in `hub_audit_logs` con `user_id`, `timestamp`, `source_ip`, `purpose`
- Sudo whitelist sul server target: file `/etc/sudoers.d/<slug>-installer` con binari strettamente necessari
### 5.3 JWT service-to-service
- Algoritmo: RS256
- Chiave privata AgileHub + chiave pubblica distribuita ai prodotti al momento dell'attivazione
- Claim obbligatori: `iss=agilehub`, `aud=<product_slug>`, `role=service`, `exp` ≤ 5 minuti
- Ogni prodotto verifica `iss` + `aud` + `exp` + firma
---
## 6. Fasi di rollout (coordinate cross-prodotto)
| Fase | Durata | Chi fa cosa | Bloccante per |
|------|--------|-------------|---------------|
| **Fase 1** — API licensing hub | 2-3 gg | AgileHub: `nexus-hub-ms` + migration `nexus_hub` + 5 endpoint + seed 11 prodotti | Prima installazione cliente qualsiasi prodotto |
| **Fase 1b** — Installer script prodotto | 3-5 gg/prodotto | Ogni prodotto: `install-<slug>.sh` non-interattivo + `update-<slug>.sh` + `/api/auth/admin/tenants/*` endpoint | Pilota white-label di quel prodotto |
| **Fase 2** — UI Prodotti read-only | 3-4 gg | AgileHub: sidebar "Prodotti", elenco, detail read-only, heartbeat dashboard | Visibilità pilota |
| **Fase 3** — Orchestrator + wizard | 6-8 gg | AgileHub: wizard 5-step, SSH worker, log streaming, rollback, BullMQ, encrypt vault | Onboarding cliente self-service |
| **Fase 4** — Multi-prodotto generalizzato | 2-3 gg | AgileHub: template wizard per prodotto, `installer_orchestrator_config` drive-based, billing hook | Scale-up suite |
**Pilota**: TRPG primo. Dopo pilota OK, checklist per onboarding secondo prodotto (SustainAI probabile next).
---
## 7. Obblighi prodotti della suite
Per essere "orchestrabile" da AgileHub, un prodotto **DEVE**:
1. ✅ Fornire `install-<slug>.sh --non-interactive --json-output` con exit codes e shape JSON canonici
2. ✅ Fornire `update-<slug>.sh` (Fase 3, no urgenza) con stesso pattern
3. ✅ Esporre `GET /version.json` con `{version, released_at, changelog_url}`
4. ✅ Esporre `GET /api/auth/admin/health` autenticato
5. ✅ Esporre `POST /api/auth/admin/tenants/create|:id/suspend|:id/resume` con HMAC + JWT service
6. ✅ Chiamare `POST /api/license/activate` alla fine di `install-<slug>.sh`
7. ✅ Avere un cron `license-heartbeat.sh` che chiama `POST /api/license/validate` ogni 24h (1h se anomalia)
8. ✅ Chiamare `POST /api/tenants/report` ad ogni create/update/archive consulting_firm
9. ✅ Gestire stati licenza: `valid → read_only (48h grace) → locked (168h)` con banner UI maintenance
10. ✅ Propagare `HUB_SIGNING_KEY_<SLUG>` nel `.env` istanza al momento dell'attivazione
Un prodotto che **non** rispetta questi 10 punti non può fare parte del catalogo installer AgileHub. Può restare nel menu `products.json` come link esterno (`installer_type=external_link`) finché non si adegua.
---
## 8. Domande aperte — risposte ufficiali
Le 10 domande di §11 della spec TRPG trovano risposta nel capitolo 2 di questo doc. Riepilogo:
1. **Framework hub**: Node.js/Express (non PHP).
2. **DB**: MySQL host Hetzner, DB dedicato `nexus_hub`.
3. **Job queue**: cron fino a Fase 2, BullMQ/Redis da Fase 3.
4. **UI**: Next.js 15 + React 19 (nexus-dashboard).
5. **Vault SSH**: AES-256-GCM env-based.
6. **Monitoring**: `/health` endpoint + cron watchdog esistente. Prometheus opzionale in Fase 4.
7. **Ruolo installer**: nuovo `hub_installer` separato da `hub_admin`.
8. **Email**: `email-automation-ms` esistente.
9. **Multi-prodotto**: generico da Fase 1, seed TRPG ma codice agnostic.
10. **Billing**: nessun Stripe oggi. `hub_customers` placeholder, billing in Fase 4.
---
## 9. Prossimi passi
1. **AgileHub**: scaffold `nexus-hub-ms` + migration `001-initial-schema.sql` (sub-approval richiesta)
2. **AgileHub**: distribuire questo documento a tutti i prodotti della suite via SSH Hetzner
3. **TRPG**: adeguare `install-trpg.sh` al contratto §3.3 (hostname `agilehub.agile.software`, exit codes canonici, JSON shape)
4. **TRPG**: implementare `/api/auth/admin/tenants/*` endpoint con HMAC + JWT (§3.2)
5. **Suite**: ogni team leggere questo doc e aprire ticket AgileHub con tag `installer-readiness` per dichiarare quando saranno pronti
---
## 10. Riferimenti
- Spec origine TRPG: `/var/www/trpg-agile/docs/AGILEHUB_INSTALLER_INTEGRATION_SPEC.md` (v1.0, 2026-04-19)
- Roadmap TRPG: `/var/www/trpg-agile/docs/INSTALLER_ROADMAP.md`
- White-label TRPG: `/var/www/trpg-agile/docs/INSTALLER_WHITELABEL.md`
- Licensing model TRPG: `/var/www/trpg-agile/docs/PROTEZIONE_FASE4_LICENSING.md`
- Multi-studio TRPG: `/var/www/trpg-agile/docs/MULTI_STUDIO_SPEC.md`
- SSO AgileHub: `/projects/agile-services/docs/SPEC_SSO_SINGLE_SIGN_ON.md`
- SSO fasi: `/projects/agile-services/docs/SSO_FASI_IMPLEMENTAZIONE.md`
- CLAUDE.md AgileHub: `/projects/agile-services/CLAUDE.md` (regole maintenance + agent + escalation)
---
**Firma autorità**: questo documento è **canonico** per la suite Agile Software. Modifiche richiedono consenso AgileHub + notifica a tutti i prodotti della suite.
**Versione**: 1.0 — 2026-04-19
**Prossima revisione**: dopo completamento pilota TRPG (stimata 2026-05-31).

View File

@ -0,0 +1,237 @@
# Standard `marketing-tenant-provisioning` v1.4
**Owner**: AgileHub MARKETER agent (governance) + TITAN (esecuzione DKIM) + MAESTRO (orchestratore atomico AC30)
**Stato**: adopted (LIVE 2026-04-26 mattina v1.3 — DKIM per-tenant; v1.4 mattina — AC30 pronto in branch, deploy pending trigger)
**Applies to**: `*` (tutti i prodotti suite — TRPG, SUSTAINAI, NIS2, LG231, TAXAI, DFM, MKTG, ALLRISK, WMS, MADEBYCLOUD, AGILEHUB)
**Versione**: 1.4
**MS implementatore**: `nexus-marketing-ms` (porta 4221) + `email-automation-ms` (porta 4004) + OpenDKIM milter Postfix Helsinki + `agilehub-workflow-engine` (porta 4230, blocco AWE `AC30_MarketingTenantProvision`)
**Endpoint base**: `/api/marketing/*`
---
## 1. Scopo
Lo standard definisce il contratto cross-suite tra prodotti consumer e modulo Marketing AgileHub centralizzato. Ogni prodotto (TRPG come pilot) consuma 28 endpoint stabili `/api/marketing/*` per fornire ai propri clienti (consulting firm) un servizio email marketing white-label multi-tenant.
## 2. Multi-tenancy
| Concetto | Identificativo | Source of truth |
|---|---|---|
| Tenant marketing | `tenant_id` (es. `tnt_agile-technology_b4f9a3141333`) | `nexus_marketing_db.tenants` |
| Consulting firm consumer | `consulting_firm_id` (FK logico) | DB del prodotto consumer (es. `trpg.consulting_firms`) |
| `firm_slug` | derivato da `slugify(consulting_firm.name)` | calcolato server-side al provisioning |
| API key per-tenant | `ah_live_*` (prod) / `ah_test_*` (sandbox) | `nexus_marketing_db.api_keys` |
Vincolo: 1:1 tra `tenant_id` e `consulting_firm_id` nel prodotto consumer (nessuna 1:N).
**Naming convention `firm_slug`** (formalizzato v1.1):
- Algoritmo: `slugify(consulting_firm.name)` — lowercase ASCII, separatore `-`, rimozione caratteri non alfanumerici, max 64 char
- Esempi:
- `"Tremolada Consulting S.r.l."``tremolada-consulting`
- `"Agile Technology s.r.l."``agile-technology`
- `"O'Brien & Co."``obrien-co`
- `tenant_id` auto-generato server-side: `tnt_${firm_slug}_${randomId(6)}` (esempio: `tnt_agile-technology_b4f9a3141333`)
- Sottodominio derivato: `{firm_slug}.agile.software` (es. `agile-technology.agile.software`)
- Reply-to default: `noreply@{firm_slug}.agile.software`
- Il consumer NON costruisce mai il `firm_slug` autonomamente: lo riceve in risposta a `POST /admin/tenants` e lo persiste come dato derivato
## 3. Autenticazione
- Header obbligatorio: `X-AgileHub-Key: <api_key_plaintext>`
- Header obbligatorio: `X-AgileHub-API-Version: 1.0`
- Hash SHA-256 della key in DB (`api_keys.key_hash`); plaintext mai persistito leggibile (cifrato AES-256-GCM in `api_keys.key_encrypted`)
- Provisioning admin via endpoint interno `/api/marketing/admin/tenants/:id/api-keys` (header `X-Internal-Key` solo Esperto Agile/installer)
## 4. API versioning policy
- Header request: `X-AgileHub-API-Version: 1.0` (obbligatorio)
- Header response: `X-AgileHub-API-Version: 1.0` (echo)
- Header response: `X-Request-Id: req_<uuid>` (cross-system tracing)
- Deprecation policy: 6 mesi notice (`X-Deprecation-Date`) + 6 mesi sunset (`X-Sunset-Date`) prima rimozione versione precedente
- Canary opt-in: `X-API-Canary: true` su feature flag tenant
## 5. Rate limiting
| Endpoint family | Default | Override |
|---|---|---|
| Read endpoints | 60 req/min | |
| `/contacts/import` | 10 req/min | `RATE_LIMIT_IMPORT` env |
| `/segments/preview` | 20 req/min | `RATE_LIMIT_PREVIEW` env |
| Tracking pixel/click | nessuno (pubblico) | |
Header response: `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`.
Risposta 429 con `retry_after_sec`.
## 6. Error envelope
```json
{
"error": {
"code": "STRING_CODE",
"message": "Human readable",
"details": { ... opzionale ... }
},
"request_id": "req_<uuid>"
}
```
Codici stabili documentati: `MISSING_API_KEY`, `INVALID_API_KEY`, `API_KEY_REVOKED`, `API_KEY_EXPIRED`, `TENANT_NOT_FOUND`, `TENANT_DELETED`, `TENANT_SUSPENDED`, `UNSUPPORTED_API_VERSION`, `RATE_LIMITED`, `DSL_INVALID`, `DSL_TOO_DEEP`, `DSL_FIELD_NOT_ALLOWED`, `DSL_OP_NOT_ALLOWED`, `CONTACT_SUPPRESSED`, `CONTACT_NOT_FOUND`, `INVALID_EMAIL`, `CAMPAIGN_NOT_FOUND`, `CAMPAIGN_LOCKED`, `CAMPAIGN_FINAL`, `CAMPAIGN_NOT_SCHEDULABLE`, `NO_RECIPIENTS`, `TEMPLATE_NOT_FOUND`, `TEMPLATE_INVALID`, `TEMPLATE_REQUIRED`, `EXPORT_JOB_NOT_FOUND`, `IMPORT_JOB_NOT_FOUND`, `INTERNAL_ERROR`.
## 7. SLA
| Indicatore | Target | Note |
|---|---|---|
| Uptime | 99.5% rolling 30gg | Allineato altri MS suite |
| p95 GET | <500ms | Cache Redis 30s su preview |
| p95 POST | <2000ms | DB persist + audit + idempotency |
| `/segments/preview` 50k contatti | <2s | Cache 30s + indici DB |
| RTO | 30 min | mysqldump nightly + restore documentato |
| RPO | 1h | Replication MySQL slave (futuro) |
## 8. GDPR compliance
- Double opt-in obbligatorio (POST `/contacts` con `consent_pre_confirmed=false` invia email confirm)
- Footer unsubscribe RFC 8058 auto in tutti i template (one-click `List-Unsubscribe` + `List-Unsubscribe-Post`)
- Suppression list permanente per-tenant (`unsubscribe`, `complaint`, `hard_bounce`, `gdpr_request`)
- Hard delete contact (`POST /contacts/:id/hard-delete`) ritorna `audit_trail_id`
- Export ZIP portabilità (`POST /exports`) — contacts.csv + campaigns.json + suppression.csv
- Offboarding tenant: 90gg grace → hard delete (zero retention post-offboard)
- Retention metrics_events: 90gg rolling, cron pulizia notturna
## 9. Branding tenant
- Sottodominio `{firm-slug}.agile.software` per envelope From
- 4 placeholder firm: `firm.name`, `firm.logo_url`, `firm.primary_color`, `firm.accent_color`
- 3 personalization tag contact: `contact.first_name`, `contact.last_name`, `contact.email`
- Reply-to configurabile per tenant
- DKIM selector dedicato per tenant (deliverability isolation)
## 10. 28 endpoint
Vedi `/var/www/agile-services/nexus-marketing-ms/src/routes/*` o ticket `TICKET_AGILEHUB_MARKETING_API.md` per spec completa. Riassunto:
- Sprint 2 (5 read-only): `/tenants/me`, `/contacts` GET, `/campaigns` GET/{id}, `/templates` GET, `/metrics/overview`
- Sprint 3 (12 write): contacts POST, segments POST/GET/preview, campaigns POST/PATCH/schedule/cancel, templates render, test-send, metrics per-campaign
- Sprint 4 (10 GDPR): contacts PATCH/DELETE/hard-delete/import, exports POST/{id}, quota, admin tenants suspend/resume/delete
## 11. Sandbox
- URL primary: `https://agilehub-staging.agile.software/api/marketing` (LIVE 2026-04-25)
- URL secondary (legacy): `https://staging.agilehub.agile.software/api/marketing`
- Tenant test corrente: `tnt_agile-technology_b4f9a3141333` (`plan='enterprise_test'`, `quota_default=5000`)
- Tenant precedente: `tnt_test_tremolada_001` (suspended 2026-04-25 sera, swap-out per testing su entità Agile Technology s.r.l.)
- Key dedicata: `ah_test_*` (mai mescolare con `ah_live_*`)
- DB: `nexus_marketing_db` (clone notturno cron)
- Subdomain tenant: `{firm_slug}.agile.software` (es. `agile-technology.agile.software`) — zone Cloudflare-managed, automazione full
- DNS deliverability sandbox: SPF + DMARC + **DKIM** tutti `active` (LIVE 2026-04-26 mattina)
- **DKIM signing**: OpenDKIM milter su Postfix Helsinki, selector `agile2026`, RSA 2048, rotation annuale Q1
## 12. Deroghe
I prodotti consumer non possono opporsi unilateralmente. Per esenzioni: aprire ticket AgileHub con tag `standard-exception` + motivazione tecnica + termine. MARKETER + REGENT decidono entro 5gg lavorativi.
## 13. Distribuzione
| Prodotto | docs_file | claude_md_section | claude_memory | adoption_status |
|---|---|---|---|---|
| trpg | ✅ TODO | ✅ TODO | ✅ TODO | pending → acknowledged after deploy |
| sustainai | TBD M5 | | | pending |
| nis2 | TBD M5 | | | pending |
| altri 8 | TBD post-Tremolada | | | pending |
## 14. Riferimenti
- Spec produttore TRPG: `/var/www/trpg-agile/docs/PROMPT_MARKETING_FEATURE.md` v1.3
- Spec contratto: `/var/www/trpg-agile/docs/TICKET_AGILEHUB_MARKETING_API.md`
- Doc agente MARKETER: `docs/AGENT_MARKETER.md`
- Reply formale TRPG: `docs/REPLY_TO_TRPG_MARKETING_REQUEST_DRAFT.md`
- Audit gap: `docs/HANDOVER_FROM_TRPG_MARKETING_FEATURE_REQUEST.md`
## 15. Changelog
### v1.4 (2026-04-26 mattina, AC30 blocco AWE pronto in branch — MAESTRO impl + 11/11 test)
- **Nuovo blocco AWE `AC30_MarketingTenantProvision`** implementato + testato (619 righe blocco + 397 righe test, file untracked nel branch `feature/awe-m1-m2-foundation`)
- **Orchestrazione atomica** end-to-end provisioning tenant marketing in 1 nodo workflow: tenant create + 3 DNS Cloudflare (A + SPF + DMARC) + DKIM SSH exec `dkim-provision.js` + DPA opzionale + API key generation
- **Compensation pattern**: hook `compensate(ctx, result)` engine-level + rollback inline su partial-state failure (cancella DNS records, revoca DKIM via `--revoke`, soft-delete tenant 90gg grace)
- **Idempotency H7**: `sha256(runId + nodeId + {firmSlug, firmLegalName})` TTL 7gg
- **Dry-run mode** completo (mock conforme `outputSchema`, no side-effect)
- **Vault integration**: `tier1__nexus-hub-ms__cloudflare/api_token` per CF token, `INTERNAL_SERVICE_KEY` env per X-Internal-Key marketing-ms admin endpoints
- **SSH credential**: pattern `loadAndDecrypt(sshCredentialId)` riusato da AC23
- **Auto-loaded** dal catalog registry (`src/blocks/index.js`) come 13° blocco `action`
- **Test verdi**: 11/11 (6 scenari §9 handover MAESTRO + 3 schema/shape + 2 dryRun) — happy path + rollback DNS + rollback DKIM + skipDkim + firm_slug duplicato + idempotency
- **Trigger originale Q3 2026** (≥3 tenant prod), implementazione **anticipata** disponibile on-demand
- **Decisione GO/NO-GO deploy**: pending — richiede `pm2 reload agilehub-workflow-engine` + bump health.js block count + smoke E2E contro marketing-ms reale
- **Spec autoritativa**: [`HANDOVER_AC30_MARKETING_TENANT_PROVISION_FOR_MAESTRO.md`](HANDOVER_AC30_MARKETING_TENANT_PROVISION_FOR_MAESTRO.md)
- **Coordinamento agenti** (al deploy): MAESTRO (impl) + TITAN (SSH exec coord) + MARKETER (acceptance + bump v1.5 con esempio workflow JSON pubblicato) + PRISMA (form auto-render verify) + VIGILE (security inventory blocco side-effect)
### v1.3 (2026-04-26 mattina, DKIM per-tenant LIVE — TITAN Phase 1-4)
- **DKIM signing OPERATIVO**: OpenDKIM 2.11.0 installato su Postfix Helsinki, milter `inet:localhost:8891`, `milter_default_action=accept` (fail-open per zero downtime)
- **Schema scope chiave**: 1 keypair RSA 2048 per-tenant, selector `agile2026._domainkey.{firm_slug}.agile.software`. Pattern conferma standard §16 (deliverability isolation per-tenant).
- **Tenant Agile Technology**: keypair generata + DNS TXT pubblicato Cloudflare + KeyTable + SigningTable + DB UPDATE → `dkim_status='active'`
- **Automation script**: `scripts/dkim-provision.js` self-contained Node.js (no npm install, mysql CLI + https native + execSync). Idempotente. Eseguito on-host Hetzner.
- **§16 ggiornato**: `Strategia DKIM sandbox vs prod` rimossa "skip sandbox" (DKIM ora attivo anche in sandbox per testing realistico). Mantengono `dkim_status='not_required'` come stato simbolico per tenant esplicitamente esentati.
- **Rotation policy**: annuale Q1 (Q1 2027 prossimo turno), dual-key 30gg overlap durante transizione, coordinata con VIGILE Pillar 1
- **Backward compat**: 99.994% del traffico Postfix Helsinki (cron, www-data, noreply@agile.software) NON matcha SigningTable → continua identica a oggi (15.466 email/7gg invariate, verificato empiricamente)
### v1.2 (2026-04-25 sera, post-allineamento Cristiano TRPG — stesso giorno di v1.1)
- **Convention dominio cambiata**: `*.agilehub.it``*.agile.software` (correzione architetturale: Agile ha un solo dominio commerciale `agile.software`, già zone Cloudflare-managed per altri sottodomini come `trpg.agile.software`, `agilehub.agile.software`)
- **Schema `tenant_branding.dkim_status`**: ALTER additive enum, aggiunto valore `not_required` per sandbox/non-prod (oltre a `pending`/`active`/`failed`)
- **Tenant `tnt_agile-technology_b4f9a3141333`**: UPDATE branding subdomain → `agile-technology.agile.software`, reply_to → `noreply@agile-technology.agile.software`, dkim_status=not_required, spf_status=active, dmarc_status=active
- **§16 riscritto**: sostituito vecchio scenario register.it con scenario reale Cloudflare-managed
- **§11 Sandbox**: aggiornato per nuovo subdomain pattern + DNS deliverability LIVE
- **DKIM strategia**: skipped per sandbox, **mandatory per produzione** (go-live Tremolada 23/6 — vedi §16)
### v1.1 (2026-04-25 sera, superseded by v1.2 stesso giorno)
- **§2 Multi-tenancy**: formalizzato `firm_slug = slugify(consulting_firm.name)` come algoritmo derivato server-side. Aggiunti 3 esempi concreti, regola `tenant_id = tnt_${firm_slug}_${randomId(6)}`, derivazione automatica sottodominio + reply-to.
- **§11 Sandbox**: aggiornato tenant corrente a `tnt_agile-technology_b4f9a3141333` (sostituisce `tnt_test_tremolada_001` suspended)
- v1.1 documentava convention `*.agilehub.it` (corretta in v1.2 stesso giorno)
## 16. Gestione zone DNS `*.agile.software` + deliverability
**Provider DNS authoritative**: Cloudflare (zone id `e3d677355677a6397d2caa77264cbfa2`).
**MX inbound**: separato (Microsoft 365 su zone `agilehub.it`); modulo Marketing fa SOLO outbound, no impatto MX.
**Automazione**: full via Cloudflare API (token in vault `tier1__nexus-hub-ms__cloudflare/api_token`).
**Pattern record per tenant** (provisioning automatico con MARKETER+TITAN):
| Record | Esempio Agile Technology | Scope |
|---|---|---|
| `A` | `agile-technology.agile.software → 135.181.149.254` | Helsinki Postfix outbound |
| `TXT` SPF | `v=spf1 ip4:135.181.149.254 -all` | autorizzazione mittente |
| `TXT` DMARC | `_dmarc.agile-technology.agile.software TXT v=DMARC1; p=none; rua=mailto:dmarc-reports@agile.software; pct=100` | report-only modalità monitoring |
| `TXT` DKIM | `agile2026._domainkey.agile-technology.agile.software TXT v=DKIM1; k=rsa; p=<pubkey>` | **solo PROD**, skip sandbox |
**Strategia DKIM sandbox vs prod (v1.3 LIVE 2026-04-26)**:
- **DKIM mandatory per tutti i tenant produttivi** (no skip). DMARC inizia `p=none` (monitoring) e bumppa `p=quarantine` poi `p=reject` quando deliverability stabile post-3 settimane di metriche pulite.
- **`dkim_status='not_required'`**: stato simbolico per tenant esplicitamente esentati (es. tenant disabilitati o test temporanei senza traffico real-world). Widget consumer lo mappa come "n/a".
- **Sandbox attiva DKIM by default** dal v1.3: `agile-technology.agile.software` ha DKIM `active` per testing realistico pre-prod.
**Effort provisioning per nuovo tenant prod (con script automation v1.3)**:
1. POST `/admin/tenants` (auto-genera firm_slug + subdomain) — 1s
2. POST 3 record Cloudflare base (A + SPF + DMARC) — manuale via curl o futuro workflow AWE — 10s
3. **`node scripts/dkim-provision.js --tenant-id <id> --firm-slug <slug>`** ON HOST HETZNER (root):
- genera keypair RSA 2048 (idempotente)
- append KeyTable + SigningTable
- publish DNS DKIM TXT su Cloudflare (idempotente, PATCH se esiste)
- UPDATE `tenant_branding.dkim_*` + `dkim_status='active'`
- `systemctl reload opendkim` hot
- **~3-5 secondi end-to-end**
**Totale**: ~15s tenant pronto. Future workflow AWE blocco `AC30_MarketingTenantProvision` Q3 2026 può chiamare script via webhook host.
**Rotation policy DKIM** (annuale Q1, coord VIGILE):
1. Q1 ogni anno: VIGILE notifica scadenza
2. TITAN: nuovo selector (es. `agile2027` per Q1 2027), genera keypair nuovo
3. Pubblica DNS DKIM nuovo (entrambi vecchio+nuovo coesistono per 30gg overlap)
4. Aggiorna `KeyTable` + `SigningTable` per usare nuovo selector
5. `systemctl reload opendkim` → email firmate con nuova chiave
6. T+30gg: revoca vecchio selector (DELETE DNS vecchio + cleanup KeyTable/SigningTable)
7. Audit log TITAN+VIGILE entry coordinated
**Comandi rapidi rotation** (futuro Phase 4 enhancement):
```bash
# T0: nuova chiave
node scripts/dkim-provision.js --tenant-id <id> --firm-slug <slug> --selector agile2027
# T+30gg: revoca vecchia
node scripts/dkim-provision.js --revoke --tenant-id <id> --firm-slug <slug> --selector agile2026
```

View File

@ -0,0 +1,160 @@
# STANDARD AgileHub: multitenant-architecture v1.0
> **Codename progetto**: NAVIGAI (Navigare nell'AI)
> **Status**: `proposed`
> **Applies to**: `*` (tutta la suite — 11 prodotti)
> **Owner**: REGENT (coordinamento) + TITAN (backend) + VIGILE (audit) + Agile AI (KB master) + PRISMA (UI master/tenant)
> **Date**: 2026-05-17
> **Public facing**: AgileHub (NAVIGAI è il codename interno, invisibile al cliente)
---
## 1. Scopo
Definire l'architettura multitenant esplicita di AgileHub con un livello master condiviso, tenant client isolati, catalogo cross-tenant condivisibile, billing per-tenant e governance centralizzata.
## 2. Tassonomia tenant
| Tipo | Esempio | id | is_master | tier | parent_tenant_id |
|---|---|---|---|---|---|
| **Master** | AgileHub | 1 | TRUE | `master` | NULL |
| Client enterprise | Work Group s.r.l. | N | FALSE | `enterprise` | 1 |
| Client professional | Studio Tremolada | N | FALSE | `professional` | 1 |
| Client trial | sandbox | N | FALSE | `trial` | 1 |
| Sandbox interno | Agile Technology test | N | FALSE | `trial` | 1 |
## 3. Visibility ENUM cross-tabella
Ogni record sensibile (KB articles, personas, workflow templates, routing rules, RAG repositories) ha colonna `visibility ENUM('global','shared','tenant','team','private')`.
| Valore | Significato | Lettura da |
|---|---|---|
| `global` | Master AgileHub catalog, riusabile read-only da tutti i tenant | tutti |
| `shared` | Visibile a tenant + i suoi sotto-tenant | tenant + figli |
| `tenant` | Solo tenant proprietario | tenant |
| `team` | Team specifico dentro un tenant | team members |
| `private` | Singolo utente | owner_user_id |
## 4. Opt-out granulare
Tabella `nexus_tenant_db.tenant_global_exclusions(tenant_id, resource_type, resource_id, exclusion_reason)` permette al client di dichiarare "non voglio usare quell'articolo KB master/quella persona master/quella regola routing master".
## 5. JWT additivi (no breaking)
Claims aggiunti (additive, retro-compat):
- `tenant_id BIGINT`
- `tenant_slug VARCHAR(64)`
- `is_master TINYINT(1)`
- `permitted_tenant_ids INT[]` (per is_master users che possono switchare contesto)
Vecchi JWT senza nuovi claim restano validi → tenant_id desunto via fallback `tenants.email → user → tenant_membership`.
## 6. Shared library
Pacchetto npm interno `@agile/tenant-auth` (Node) + `agile_tenant_auth` (Python) fornisce:
- `extractTenantContext(req)``{ tenant_id, tenant_slug, is_master, user_id }`
- `requireSameTenant(req, resourceTenantId)` → throws 403 se mismatch
- `requireMaster(req)` → throws 403 se !is_master
- `withTenantFilter(query)` → injects `WHERE (tenant_id = X OR visibility = 'global')`
## 7. Vault namespace tenant-aware
Pattern: `tier1__<ms>__<tenant_slug>__<feature>`
Esempi:
- `tier1__nexus-marketing-ms__work_group_001__mailgun_api_key`
- `tier1__nexus-presenter-ms__agilehub_master__tavus_api_key`
Compatibility: namespace senza `<tenant_slug>` restano validi per master.
## 8. Audit log HMAC chain
JSONL append-only in `docs/multitenant-audit/YYYY-MM-WNN.jsonl` con catena HMAC-SHA256 (ogni entry contiene SHA del precedente). Genesis entry firmata da TITAN al kickoff SG-0.
## 9. Distributed tracing
Header `X-Trace-Id` propagato cross-MS in tutti i call HTTP/Redis Streams. Generato in API gateway Apache se assente. Pino structured log + Loki backend.
## 10. Status page tenant-aware
`/status` Next.js mostra SLA + uptime + latency p99 **per-tenant** (con auth). SLA differenziato per tier:
- master: 99.99%
- enterprise: 99.95%
- professional: 99.9%
- trial: 99%
## 11. Canary deploy
Apache `mod_proxy_balancer` rolling 5% → 50% → 100% con auto-rollback se error rate > 2% per 5 min.
## 12. Billing per-tenant
Tabella `nexus_hub.hub_cost_events(tenant_id, ms, event_type, cost_eur_micros, ts)` con 8 hook nei MS:
- nexus-ai-ms (tokens LLM Anthropic + Voyage embedding)
- nexus-marketing-ms (email sends via Mailgun)
- nexus-call-ms (Twilio minutes)
- nexus-presenter-ms (Tavus session-minutes + LiveKit)
- nexus-voice-ms (Deepgram STT + ElevenLabs TTS)
- nexus-rag-ms (Voyage embed/rerank + storage GB)
- nexus-hub-ms (server time)
- agilehub-workflow-engine (Haiku suggester tokens)
Export mensile CSV/PDF (showback) per cliente.
## 13. Compliance
- **GDPR Art.32**: encryption at rest+transit, access controls, audit trail, business continuity, periodic testing — tutti documentati in `docs/COMPLIANCE_GDPR_ART32.md`
- **ISO A.18.1.5** readiness: gap analysis NON certificazione formale
- **Retention fiscale IT**: 10 anni per `hub_cost_events` (NON 7)
- **GDPR Art.17 erasure**: cascade già implementato in nexus-rag-ms (estendere ai 13 MS)
## 14. Pen test esterno
STRIDE 13×6 matrix (13 MS × 6 categorie) eseguita da vendor EU certificato durante SG-5. Budget €15K una tantum. Vendor RFP da VIGILE.
## 15. Roadmap distribution
| Wave | Prodotti | Trigger |
|---|---|---|
| **Reference** | AGILEHUB | SG-0 in progress |
| **Wave 1 (P1)** | TRPG, SUSTAINAI, NIS2, DFM | SG-4 GREEN |
| **Wave 2 (P2)** | TAXAI, LG231, MKTG, ALLRISK, WMS, MADEBYCLOUD, CERTISOURCE | post smoke Wave 1 |
Distribution via INSTALLATORE pattern: docs_file scp + claude_md append + claude memory write nei 11 container DevEnv prodotto + `hub_standards_adoption` row per ogni prodotto (status `pending → acknowledged → implemented`).
## 16. NESSUN out-of-scope (v1.0)
Documentato esplicitamente FUORI da v1.0:
- Multi-region failover (Hetzner Helsinki + EU secondary)
- Self-service tenant signup pubblico con CC payment
- Multi-currency (solo EUR)
- SAML/OIDC federation
- Tenant white-label dominio custom (solo subdomain `{slug}.agilehub.it`)
- Audit log immutabile blockchain
- ISO 27001 / SOC 2 certification formale
---
## Adoption tracker (status iniziale)
| Prodotto | docs_file | claude_md | claude_memory | adoption_status |
|---|---|---|---|---|
| AGILEHUB | ✓ pending SG-0 | pending | pending | `proposed` |
| TRPG | pending | pending | pending | `pending` |
| SUSTAINAI | pending | pending | pending | `pending` |
| NIS2 | pending | pending | pending | `pending` |
| TAXAI | pending | pending | pending | `pending` |
| LG231 | pending | pending | pending | `pending` |
| DFM | pending | pending | pending | `pending` |
| MKTG | pending | pending | pending | `pending` |
| ALLRISK | pending | pending | pending | `pending` |
| WMS | pending | pending | pending | `pending` |
| MADEBYCLOUD | pending | pending | pending | `pending` |
| CERTISOURCE | pending | pending | pending | `pending` |
## Riferimenti
- [NAVIGAI_EXECUTIVE_BRIEF.md](NAVIGAI_EXECUTIVE_BRIEF.md)
- [NAVIGAI_ARCHITETTURA_TECNICA.md](NAVIGAI_ARCHITETTURA_TECNICA.md)
- [NAVIGAI_ROADMAP_OPERATIVA.md](NAVIGAI_ROADMAP_OPERATIVA.md)
- [PLAN_MULTITENANT_MASTER_REFACTOR_V11.md](PLAN_MULTITENANT_MASTER_REFACTOR_V11.md) (920 righe production-ready)

Binary file not shown.

View File

@ -0,0 +1,233 @@
Agenda di Ricerca e Innovazione
per la Cybersicurezza
Settore
Sottosettore o tipologia di soggetto
Sottosettore o tipologia di Allegato
soggetto I: Settori ad alta criticità
Grandi imprese
(occupano almeno 250 dipendenti oppure hanno un fatturato
di almeno 50M€ oppure hanno un bilancio di almeno 43M€)
Medie imprese
(occupano almeno 50 dipendenti oppure hanno un fatturato
di almeno 10M€ oppure hanno un bilancio di almeno 10M€)
Piccole e micro imprese
Autorità di Settore
1. Energia elettrica
2. Teleriscaldamento e teleraffrescamento
1. Energia
3. Petrolio
4. Gas
5. Idrogeno
1. Trasporto aereo
2. Trasporti
2. Trasporto ferroviario
3. Trasporto per vie d'acqua
4. Trasporto su strada
3. Settore bancario
1. Enti creditizi quali definiti all'articolo 4, punto 1), del regolamento (UE) n. 575/2013 del Parlamento europeo e del Consiglio
(DORA lex specialis)
1. Gestori delle sedi di negoziazione quali definiti all'articolo 4, punto 24), della direttiva 2014/65/UE del Parlamento europeo e del Consiglio
4. Infrastrutture dei mercati finanziari
2. Controparti centrali (CCP) quali definite all'articolo 2, punto 1), del regolamento (UE) n. 648/2012 del Parlamento europeo e del Consiglio
(DORA lex specialis)
Essenziali
Importanti1
Non in ambito2
Essenziali
Importanti1
Non in ambito2
1. Prestatori di assistenza sanitaria quali definiti all'articolo 3, lettera g), della direttiva 2011/24/UE del Parlamento europeo e del Consiglio
2. Laboratori di riferimento dell'UE quali definiti all'articolo 15 del regolamento (UE) 2022/2371 del Parlamento europeo e del Consiglio
5. Settore sanitario
3. Soggetti che svolgono attività di ricerca e sviluppo relative ai medicinali quali definiti all'articolo 1, punto 2), della direttiva 2001/83/CE
del Parlamento europeo e del Consiglio
4. Soggetti che fabbricano prodotti farmaceutici di base e preparati farmaceutici di cui alla sezione C, divisione 21, della NACE Rev. 2
5. Soggetti che fabbricano dispositivi medici considerati critici durante un'emergenza di sanità pubblica (elenco dei dispositivi critici per
l'emergenza di sanità pubblica) di cui all'articolo 22 del regolamento (UE) 2022/123 del Parlamento europeo e del Consiglio
6. Acqua potabile
1. Fornitori e distributori di acque destinate al consumo umano, quali definiti all'articolo 2, punto 1, lettera a), della direttiva (UE) 2020/2184
del Parlamento europeo e del Consiglio, ma esclusi i distributori per i quali la distribuzione di acque destinate al consumo umano è una parte
non essenziale dell'attività generale di distribuzione di altri prodotti e beni
7. Acque reflue
1. Imprese che raccolgono, smaltiscono o trattano acque reflue urbane, domestiche o industriali quali definite all'articolo 2, punti da 1), 2) e 3),
della direttiva 91/271/CEE del Consiglio, escluse le imprese per cui la raccolta, lo smaltimento o il trattamento di acque reflue urbane,
domestiche o industriali è una parte non essenziale della loro attività generale
1. Fornitori di punti di interscambio Internet (Internet exchange point IXP)
2. Fornitori di servizi di sistema dei nomi di dominio (domain name system DNS), esclusi gli operatori dei server dei nomi radice
Essenziali
3. Gestori di registri dei nomi di dominio di primo livello (top level domain TLD)
4. Fornitori di servizi di cloud computing
8. Infrastrutture digitali
Essenziali
5. Fornitori di servizi di data center
Importanti1
Non in ambito2
6. Fornitori di reti di distribuzione dei contenuti (content delivery network CDN)
Essenziali
7. Prestatori di servizi fiduciari qualificati e non qualificati
8. Fornitori di reti pubbliche di comunicazione elettronica
10. Spazio
Importanti1
Essenziali
9. Fornitori di servizi di comunicazione elettronica accessibili al pubblico
9. Gestione dei servizi TIC
(business-to-business)
Essenziali i servizi fiduciari qualificati/Importanti1 quelli non qualificati
1. Fornitori di servizi gestiti
2. Fornitori di servizi di sicurezza gestiti
1. Operatori di infrastrutture terrestri possedute, gestite e operate dagli Stati membri o da privati, che sostengono la fornitura di servizi
spaziali, esclusi i fornitori di reti pubbliche di comunicazione elettronica
Essenziali
Importanti1
Non in ambito2
Essenziali
Importanti1
Non in ambito2
Allegato II: altri settori critici
1. Servizi postali e di corriere
1. Fornitori di servizi postali quali definiti all'articolo 2, punto 1 bis), della direttiva 97/67/CE, tra cui i fornitori di servizi di corriere
2. Gestione dei rifiuti
1. Imprese che si occupano della gestione dei rifiuti quali definite all'articolo 3, punto 9), della direttiva 2008/98/CE del Parlamento europeo
e del Consiglio, escluse quelle per cui la gestione dei rifiuti non è la principale attività economica
3. Fabbricazione, produzione e distribuzione
di sostanze chimiche
1. Imprese che si occupano della fabbricazione di sostanze e della distribuzione di sostanze o miscele di cui all'articolo 3, punti 9) e 14),
del regolamento (CE) n. 1907/2006 del Parlamento europeo e del Consiglio e imprese che si occupano della produzione di articoli quali definite
all'articolo 3, punto 3), del medesimo regolamento, da sostanze o miscele
4. Produzione, trasformazione e distribuzione
di alimenti
1. Imprese alimentari quali definite all'articolo 3, punto 2), del regolamento (CE) n. 178/2002 del Parlamento europeo e del Consiglio che
si occupano della distribuzione all'ingrosso e della produzione industriale e trasformazione
1. Fabbricazione di dispositivi medici e di dispositivi medico-diagnostici in vitro
Importanti1
Non in ambito2
2. Fabbricazione di computer e prodotti di elettronica e ottica
5. Fabbricazione
3. Fabbricazione di apparecchiature elettriche
4. Fabbricazione di macchinari e apparecchiature n.c.a.
5. Fabbricazione di autoveicoli, rimorchi e semirimorchi
6. Fabbricazione di altri mezzi di trasporto
1. Fornitori di mercati online
6. Fornitori di servizi digitali
2. Fornitori di motori di ricerca online
3. Fornitori di piattaforme di social network
Importanti1
4. Fornitori di servizi di registrazione dei nomi di dominio
7. Ricerca
Importanti1
1. Organizzazioni di ricerca
Non in ambito2
Allegato III: Amministrazioni centrali, regionali, locali e di altro tipo
Amministrazioni centrali:
1. Gli Organi costituzionali e di rilievo costituzionale
2. La Presidenza del Consiglio dei ministri e i Ministeri
3. Le Agenzie fiscali
4. Le Autorità amministrative indipendenti
Essenziali
Amministrazioni regionali:
1. Le Regioni e le Province autonome
Pubbliche Amministrazioni
Amministrazioni locali:
1. Le Città metropolitane
2. I Comuni con popolazione superiore a 100.000 abitanti
3. I Comuni capoluoghi di regione
4. Le Aziende sanitarie locali
Importanti1
Altri soggetti pubblici:
1. Gli Enti di regolazione dell'attività economica
2. Gli Enti produttori di servizi economici
3. Gli Enti a struttura associativa
4. Gli Enti produttori di servizi assistenziali, ricreativi e culturali
5. Gli Enti e le Istituzioni di ricerca
6. Gli Istituti zooprofilattici sperimentali
Allegato IV: Ulteriori tipologie di soggetti
1. Soggetti che forniscono servizi di trasporto pubblico locale
2. Istituti di istruzione che svolgono attività di ricerca
Soggetti a eventuale individuazione dellAutorità
Ulteriori tipologie di soggetti
3. Soggetti che svolgono attività di interesse culturale
4. Società in house, società partecipate e società a controllo pubblico, come definite nel decreto legislativo 19 agosto 2016, n.175
Possibile individuazione dellAutorità come essenziali
2
Possibile individuazione dellAutorità come importanti o essenziali
1

Binary file not shown.

View File

@ -0,0 +1,241 @@
Agenzia per la Cybersicurezza Nazionale
Determinazione del Direttore Generale dellAgenzia per la
cybersicurezza nazionale
di cui allarticolo 31, commi 1 e 2, del decreto legislativo 4 settembre 2024, n. 138, adottata
secondo le modalità di cui allarticolo 40, comma 5, lettera l), che, ai sensi dellarticolo 42,
comma 1, lettera c), in fase di prima applicazione, stabilisce le modalità e le specifiche di base
per ladempimento agli obblighi di cui agli articoli 23, 24, 25, 29 e 32 del decreto medesimo.
IL DIRETTORE GENERALE
VISTO il decreto-legge 14 giugno 2021, n. 82, come convertito con modificazioni nella legge 4
agosto 2021, n. 109, recante “Disposizioni urgenti in materia di cybersicurezza, definizione
dellarchitettura nazionale di cybersicurezza e istituzione dellAgenzia per la cybersicurezza
nazionale”;
VISTO il decreto legislativo 4 settembre 2024, n. 138, recante “il recepimento della direttiva (UE)
2022/2555, relativa a misure per un livello comune elevato di cibersicurezza nellUnione, recante
modifica del regolamento (UE) n. 910/2014 e della direttiva (UE) 2018/1972 e che abroga la
direttiva (UE) 2016/1148”, c.d. decreto NIS, ed in particolare larticolo 31, commi 1 e 2, che
prevede che, ai fini di cui agli articoli 23, 24, 25, 27, 28 e 29, l'Autorità nazionale competente NIS
stabilisce obblighi proporzionati tenuto debitamente conto del grado di esposizione dei soggetti ai
rischi, delle dimensioni dei soggetti e della probabilità che si verifichino incidenti, nonché della
loro gravità, compreso il loro impatto sociale ed economico, nonché termini, modalità, specifiche
e tempi graduali di implementazione di tali obblighi;
VISTO larticolo 40, comma 5, lettera l), del decreto NIS che prevede che tali obblighi sono stabiliti
con una o più Determinazioni dellAgenzia per la cybersicurezza nazionale, sentito il Tavolo per
lattuazione della disciplina NIS;
VISTO altresì larticolo 42, comma 1, lettera c), del decreto NIS, che prevede, in fase di prima
applicazione, che lAutorità nazionale competente NIS stabilisce le modalità e le specifiche di base
per ladempimento ai predetti obblighi;
VISTO il decreto del Presidente del Consiglio dei ministri del 10 marzo 2023, recante la nomina
del Prefetto Bruno Frattasi a Direttore generale dellAgenzia per la cybersicurezza nazionale;
VISTO il “Framework Nazionale per la Cybersecurity e la Data Protection”, edizione 2025
(Framework nazionale), realizzato dal Centro di ricerca di cyber intelligence and information
security (CIS) della Sapienza Università di Roma e dal Cybersecurity national lab del Consorzio
interuniversitario nazionale per l'informatica (CINI), in collaborazione con lAgenzia per la
cybersicurezza nazionale (ACN), quale strumento di supporto per le organizzazioni pubbliche e
private in materia di strategie e processi volti alla sicurezza informatica;
CONSIDERATO che gli allegati tecnici recanti le predette specifiche di base, illustrate nella
seconda riunione plenaria del Tavolo per lattuazione della disciplina NIS, tenutasi il 28 gennaio
1 di 6
Agenzia per la Cybersicurezza Nazionale
2025, sono stati condivisi con le Autorità di settore e con le associazioni di categoria anche per
mezzo dei tavoli settoriali di cui allarticolo 11, comma 4, lettera f), del decreto NIS;
PRESSO ATTO dei riscontri pervenuti;
SENTITO il Tavolo per lattuazione della disciplina NIS nella riunione del 10 aprile 2025;
RITENUTO di avviare la procedura di informazione ai sensi della Direttiva (UE) n. 2015/1535 del
Parlamento europeo e del Consiglio del 9 settembre 2015;
CONSIDERATO il grado di esposizione dei soggetti ai rischi, le dimensioni dei soggetti e la
probabilità che si verifichino incidenti, nonché la loro gravità, compreso il loro impatto sociale ed
economico;
ADOTTA LA PRESENTE DETERMINAZIONE
Articolo 1
(Definizioni)
1. Ai fini della presente determinazione si intende per:
a) “decreto NIS”, il decreto legislativo 4 settembre 2024, n. 138;
b) “Agenzia per la cybersicurezza nazionale”, lAgenzia per la cybersicurezza nazionale
di cui allarticolo 5, comma 1, del decreto-legge 14 giugno 2021, n. 82;
c) “Autorità nazionale competente NIS”, lAutorità nazionale competente di cui
allarticolo 10, comma 1, del decreto NIS;
d) “Autorità di settore NIS”, le Amministrazioni di cui allarticolo 11, commi 1 e 2, del
decreto NIS;
e) “soggetto NIS”, un soggetto, di cui allarticolo 2, comma 1, lettera hhh), del decreto
NIS, di natura giuridica pubblica o privata che rientra nellambito di applicazione del
decreto NIS;
f) “soggetti essenziali”, i soggetti NIS considerati essenziali ai sensi del decreto NIS;
g) “soggetti importanti”, i soggetti NIS considerati importanti ai sensi del decreto NIS;
h) “comunicazione di inserimento nellelenco dei soggetti NIS”, la comunicazione di cui
allarticolo 7, comma 3, lettera a), del decreto NIS;
i) “organi di amministrazione e direttivi”, gli organi di amministrazione e direttivi di cui
allarticolo 23 del decreto NIS, ivi incluso, laddove presente, il consiglio di
amministrazione dei soggetti NIS;
j) “misure di sicurezza di base”, specifiche di base per gli obblighi di cui agli articoli 23
e 24 del decreto NIS, sviluppate in accordo al Framework nazionale e organizzate in
funzioni, categorie, sottocategorie e requisiti;
k) “incidenti significativi di base”, le specifiche di base che descrivono gli incidenti
significativi di cui allarticolo 25 del decreto NIS;
2 di 6
Agenzia per la Cybersicurezza Nazionale
l) “sistemi informativi e di rete rilevanti”, sistemi informativi e di rete la cui
compromissione comporterebbe un impatto significativo sulla confidenzialità, integrità
e disponibilità delle attività e servizi per i quali il soggetto NIS rientra nellambito di
applicazione del decreto NIS;
m) “fornitori di servizi di registrazione dei nomi di dominio”, i fornitori di cui allarticolo
2, comma 1, lettera oo), del decreto NIS;
n) “gestori di registri dei nomi di dominio di primo livello”, i gestori di cui allarticolo 2,
comma 1, lettera pp), del decreto NIS;
o) “soggetti PSNC-NIS”, i soggetti di cui all'articolo 1, comma 2-bis, del decreto-legge n.
105 del 2019 che sono soggetti NIS;
p) “sistemi informativi e di rete PSNC”, sistemi informativi e di rete che sono inseriti
nell'elenco di cui all'articolo 1, comma 2, lettera b), del decreto-legge n. 105 del 2019
q) “operatori di servizi essenziali”, c.d. OSE, i soggetti NIS identificati prima della data di
entrata in vigore del decreto NIS come operatori di servizi essenziali ai sensi del decreto
legislativo 18 maggio 2018, n. 65;
r) “sistemi informativi e di rete OSE”, sistemi informativi e di rete delloperatore di servizi
essenziali che abilitano i servizi essenziali per i quali loperatore stesso è stato
identificato ai sensi del decreto legislativo 18 maggio 2018, n. 65;
s) “operatori telco”, i soggetti NIS che forniscono reti pubbliche di comunicazione
elettronica o servizi di comunicazioni elettroniche accessibili al pubblico ai sensi del
decreto legislativo 1° agosto 2003, n. 259, ad un numero di utenti pari o superiore, anche
alternativamente:
1) all1% della base di utenti nazionale, calcolato sulla base dei dati pubblicati
dallOsservatorio trimestrale delle comunicazioni a cura dellAutorità per le
garanzie nelle comunicazioni;
2) a un milione;
t) “sistemi informativi e di rete telco”, sistemi informativi e di rete per laccesso alla rete
fissa o mobile, da postazione o da terminale mobile, individuati come critici
dalloperatore telco in quanto potenzialmente in grado di servire, per ciascun servizio
indicato:
1) una percentuale dellutenza pari o superiore all1% della base di utenti nazionale
per quel servizio, sulla base dei dati pubblicati dallOsservatorio trimestrale delle
comunicazioni a cura dellAutorità per le garanzie nelle comunicazioni;
2) unutenza pari o superiore a un milione.
Articolo 2
(Adozione delle specifiche di base)
1. In fase di prima applicazione del decreto NIS sono adottate le specifiche di base di cui agli
allegati 1, 2, 3 e 4, facenti parte integrante della presente determinazione.
2. Le misure di sicurezza di base, a carico degli organi di amministrazione e direttivi e in
materia di misure di gestione dei rischi per la sicurezza informatica, sono stabiliti:
a) per i soggetti importanti, nellallegato 1;
b) per i soggetti essenziali, nellallegato 2.
3 di 6
Agenzia per la Cybersicurezza Nazionale
3. Gli incidenti significativi di base sono stabiliti:
a) per i soggetti importanti, nellallegato 3;
b) per i soggetti essenziali, nellallegato 4.
Articolo 3
(Termini per ladozione delle specifiche di base)
1. Il termine per ladozione delle misure di sicurezza di base di cui agli allegati 1 e 2 è fissato
in diciotto mesi dalla ricezione, da parte del soggetto NIS della comunicazione di
inserimento nellelenco dei soggetti NIS.
2. Il termine per ladempimento dellobbligo di notifica degli incidenti significativi di base
descritti negli allegati 3 e 4 è fissato in nove mesi dalla ricezione, da parte del soggetto NIS,
della comunicazione di inserimento nellelenco dei soggetti NIS.
Articolo 4
(Sicurezza, stabilità e resilienza dei sistemi di nomi di dominio)
1. Fermo restando quanto previsto dallarticolo 29 del decreto NIS, entro diciotto mesi dalla
ricezione della comunicazione di inserimento nellelenco dei soggetti NIS, i gestori di
registri dei nomi di dominio di primo livello e i fornitori di servizi di registrazione dei nomi
di dominio si adeguano alle previsioni di cui al predetto articolo, commi 1 e 2, nonché
adottano e rendono pubbliche le politiche e le procedure di cui al comma 3 del medesimo
articolo.
2. Le modalità di adeguamento alle previsioni di cui allarticolo 29, commi 1 e 2, nonché le
politiche e le procedure di cui al comma 3 del medesimo articolo sono approvate dagli organi
di amministrazione e direttivi.
3. Ai sensi dellarticolo 32, comma 3, i gestori di registri dei nomi di dominio di primo livello
e i fornitori di servizi di registrazione dei nomi di dominio adottano politiche al fine di
assicurare un livello di sicurezza informatica coerente con le specifiche di cui allallegato 1.
4. Le politiche di sicurezza informatica di cui al comma 3 sono approvate dagli organi di
amministrazione e direttivi.
Articolo 5
(Obblighi di notifica per i soggetti PSNC-NIS)
1. Fermo restando quanto previsto dallarticolo 33 del decreto NIS, i soggetti PSNC-NIS
notificano gli incidenti significativi di base di cui allallegato 4, ai sensi dellarticolo 25 del
decreto NIS, limitatamente ai sistemi informativi e di rete diversi da quelli PSNC.
2. Il termine per lobbligo di cui al comma 1 decorre dalla data di entrata in vigore della
presente determinazione.
4 di 6
Agenzia per la Cybersicurezza Nazionale
Articolo 6
(Regime transitorio per gli operatori di servizi essenziali)
1. Fermo restando quanto previsto dallarticolo 2, comma 2, e dallarticolo 3, comma 1, gli
operatori di servizi essenziali, limitatamente ai sistemi informativi e di rete OSE, per quanto
non in contrasto con la legge e il decreto NIS, assicurano il mantenimento delle misure
tecniche e organizzative già adottate prima dellentrata in vigore del decreto NIS ai sensi del
decreto legislativo 18 maggio 2018, n. 65.
2. Al fine di assicurare la continuità dellobbligo di notifica di incidente di cui allarticolo 12,
comma 5, del decreto legislativo 18 maggio 2018, n. 65, dallentrata in vigore della presente
determinazione, ai sensi dellarticolo 25 del decreto NIS, gli operatori di servizi essenziali,
limitatamente ai sistemi informativi e di rete OSE, notificano gli incidenti significativi di
base di cui:
a) allallegato 3, qualora siano soggetti importanti;
b) allallegato 4, qualora siano soggetti essenziali.
3. Il termine per gli adempimenti di cui al presente articolo decorre dalla data di entrata in
vigore della presente determinazione.
Articolo 7
(Regime transitorio per gli operatori telco)
1. Fermo restando quanto previsto dallarticolo 2, comma 2, e dallarticolo 3, comma 1, gli
operatori telco, limitatamente ai sistemi informativi e di rete telco, per quanto non in
contrasto con la legge e il decreto NIS, assicurano il mantenimento delle misure di sicurezza
e di integrità delle reti e dei servizi già adottate prima dellentrata in vigore del decreto NIS
ai sensi del decreto del Ministro dello sviluppo economico del 12 dicembre 2018.
2. Al fine di assicurare la continuità dellobbligo di notifica di incidente di cui allarticolo 40,
comma 3, lettera b), del decreto legislativo 1° agosto 2003, n. 259, ai sensi dellarticolo 25
del decreto NIS, dallentrata in vigore della presente determinazione, gli operatori telco,
limitatamente ai sistemi informativi e di rete telco, notificano gli incidenti significativi di
base:
a) di cui allallegato 3, qualora siano soggetti importanti;
b) di cui allallegato 4, qualora siano soggetti essenziali.
3. Ai fini del comma 2, nella definizione del livello di servizio atteso di cui agli allegati 3 e 4,
gli operatori telco considerano come incidenti significativi di base i seguenti casi:
a) durata superiore ad unora e percentuale degli utenti colpiti superiore al quindici per
cento del totale degli utenti nazionali del servizio interessato;
b) durata superiore a due ore e percentuale degli utenti colpiti superiore al dieci per
cento del totale degli utenti nazionali del servizio interessato;
c) durata superiore a quattro ore e percentuale degli utenti colpiti superiore al cinque
per cento del totale degli utenti nazionali del servizio interessato;
5 di 6
Agenzia per la Cybersicurezza Nazionale
d) durata superiore a sei ore e percentuale degli utenti colpiti superiore al due per cento
del totale degli utenti nazionali del servizio interessato;
e) durata superiore ad otto ore e percentuale degli utenti colpiti superiore alluno per
cento del totale degli utenti nazionali del servizio interessato.
4.
Il termine per gli adempimenti di cui al presente articolo decorre dalla data di entrata in
vigore della presente determinazione.
Articolo 8
(Disposizioni finanziarie)
1. Dalla presente determinazione non derivano nuovi o maggiori oneri a carico della finanza
pubblica, anche ai sensi dellarticolo 12, comma 6, del decreto NIS.
Articolo 9
(Pubblicità)
1. La presente determinazione è pubblicata sui siti web istituzionali dellAgenzia per la
cybersicurezza nazionale e delle Autorità di settore NIS. Ne sarà data, altresì,
comunicazione tramite pubblicazione nella Gazzetta Ufficiale della Repubblica italiana.
Articolo 10
(Entrata in vigore e disposizioni transitorie)
1. Per quanto non previsto dalla presente determinazione, si applicano le disposizioni del
decreto NIS.
2. La presente determinazione entra in vigore a decorrere dal 30 aprile 2025.
3. Larticolo 2, commi 2 e 3, e larticolo 3 entrano in vigore il giorno successivo
allesperimento della procedura di informazione ai sensi della Direttiva (UE) n. 2015/1535
del Parlamento europeo e del Consiglio del 9 settembre 2015.
Roma, data del protocollo
IL DIRETTORE GENERALE
Bruno Frattasi
Firmato digitalmente da: Bruno
Frattasi
Data: 14/04/2025 10:52:39
6 di 6

Binary file not shown.

View File

@ -0,0 +1,678 @@
Agenzia per la Cybersicurezza Nazionale
Determinazione del Direttore generale dellAgenzia per la
cybersicurezza nazionale
di cui allarticolo 7, comma 6, del decreto legislativo 4 settembre 2024, n. 138, adottata
secondo le modalità di cui allarticolo 40, comma 5, recante termini, modalità e procedimenti
di utilizzo e accesso alla piattaforma digitale nonché ulteriori informazioni che i soggetti
devono fornire allAutorità nazionale competente NIS e termini, modalità e procedimento di
designazione dei rappresentanti NIS sul territorio nazionale.
IL DIRETTORE GENERALE
VISTO il decreto legislativo 12 gennaio 2019, n. 14, recante “Codice della crisi d'impresa e
dell'insolvenza in attuazione della legge 19 ottobre 2017, n. 155”;
VISTO il decreto-legge 14 giugno 2021, n. 82, convertito, con modificazioni, dalla legge 4 agosto
2021, n. 109, recante “Disposizioni urgenti in materia di cybersicurezza, definizione
dellarchitettura nazionale di cybersicurezza e istituzione dellAgenzia per la cybersicurezza
nazionale”;
VISTO il decreto legislativo 4 settembre 2024, n. 138, recante “Recepimento della direttiva (UE)
2022/2555, relativa a misure per un livello comune elevato di cibersicurezza nellUnione, recante
modifica del regolamento (UE) n. 910/2014 e della direttiva (UE) 2018/1972 e che abroga la
direttiva (UE) 2016/1148” e, in particolare, larticolo 7 e larticolo 40, comma 5, lettera b);
VISTO il Regolamento (CEE) n. 2137/85 del Consiglio del 25 luglio 1985 relativo all'istituzione di
un gruppo europeo di interesse economico (GEIE);
VISTO il decreto legislativo 23 luglio 1991, n. 240, recante “Norme per lapplicazione del
regolamento n. 85/2137/CEE relativo allistituzione di un Gruppo europeo di interesse economico GEIE, ai sensi dellart. 17 della legge 29 dicembre 1990, n. 428”;
VISTA la raccomandazione 2003/361/CE della Commissione, del 6 maggio 2003, relativa alla
definizione delle microimprese, piccole e medie imprese;
VISTA la legge 28 giugno 2024, n. 90, recante “Disposizioni in materia di rafforzamento della
cybersicurezza nazionale e di reati informatici”;
VISTO il decreto del Presidente della Repubblica 28 dicembre 2000, n. 445, recante “Testo unico
delle disposizioni legislative e regolamentari in materia di documentazione amministrativa”, e, in
particolare, larticolo 76;
CONSIDERATO che, ai sensi del richiamato articolo 7, comma 1, del decreto legislativo 4
settembre 2024, n. 138, i soggetti di cui all'articolo 3 del medesimo decreto si registrano o
1 di 16
Agenzia per la Cybersicurezza Nazionale
aggiornano la propria registrazione sulla piattaforma digitale resa disponibile dall'Autorità
nazionale competente NIS;
CONSIDERATO, altresì, che, ai sensi dellarticolo 7, comma 6, e dellarticolo 40, comma 5,
lettera b), del decreto legislativo 4 settembre 2024, n. 138, con determinazione dellAgenzia per la
cybersicurezza nazionale, sentito il Tavolo per lattuazione della disciplina NIS, sono stabiliti i
termini, le modalità nonché i procedimenti di utilizzo e accesso alla piattaforma digitale di cui
allarticolo 7 e le eventuali ulteriori informazioni che i soggetti devono fornire ai sensi dello stesso
articolo, nonché i termini, le modalità e i procedimenti di designazione dei rappresentanti di cui
allarticolo 5, comma 3, del medesimo decreto legislativo;
RICHIAMATA la propria determinazione n. 333017 del 22 settembre 2025 che ha aggiornato la
precedente determinazione n.38565 del 26 novembre 2024 integrandovi le modalità di designazione
del referente CSIRT e di aggiornamento annuale delle informazioni, tramite il servizio dedicato
NIS/Aggiornamento annuale disponibile sul Portale ACN;
RITENUTO di aggiornare e sostituire la predetta determinazione in vista della registrazione 2026
dei soggetti NIS sul portale ACN tramite il servizio dedicato NIS/Dichiarazione;
SENTITO il Tavolo per lattuazione della disciplina NIS di cui allarticolo 12 del decreto
legislativo 4 settembre 2024, n. 138, nella riunione del 18 dicembre 2025;
ADOTTA LA PRESENTE DETERMINAZIONE
Capo I
Disposizioni di carattere generale
Articolo 1
(Definizioni)
1. Ai fini della presente determinazione si intende per:
a) “decreto NIS”, il decreto legislativo 4 settembre 2024, n. 138;
b) “Agenzia per la cybersicurezza nazionale”, lAgenzia per la cybersicurezza nazionale
di cui allarticolo 5, comma 1, del decreto-legge 14 giugno 2021, n. 82;
c) “Autorità nazionale competente NIS”, lAutorità nazionale competente di cui
allarticolo 10, comma 1, del decreto NIS;
d) “Autorità di settore NIS”, le Amministrazioni di cui allarticolo 11, commi 1 e 2, del
decreto NIS;
e) “organi di amministrazione e direttivi”, gli organi di amministrazione e direttivi di cui
allarticolo 23 del decreto NIS, ivi incluso, laddove presente, il consiglio di
amministrazione dei soggetti NIS;
f) “Portale ACN”, il Portale dei servizi tramite il quale sono accessibili i servizi che
lAgenzia per la cybersicurezza nazionale mette a disposizione dei suoi interlocutori e
dei soggetti, pubblici e privati, che rientrano nellambito di applicazione della disciplina
cyber o con i quali lAgenzia deve interagire ai sensi della stessa;
2 di 16
Agenzia per la Cybersicurezza Nazionale
g) “SPID”, il Sistema pubblico dellidentità digitale, istituito ai sensi dell'art. 64 del CAD,
modificato dallart. 17-ter del decreto-legge 21 giugno 2013, n. 69, convertito con
modificazioni, dalla legge 9 agosto 2013, n. 98, ai sensi del Decreto del Presidente del
Consiglio dei Ministri 24 ottobre 2014;
h) “CIE”, Carta di Identità Elettronica, è il documento didentità personale garantita dallo
Stato e rilasciata dal Ministero dellInterno che permette laccertamento dellidentità del
possessore e laccesso ai servizi online delle Pubbliche Amministrazioni;
i) “Servizi NIS”, i servizi, accessibili tramite il Portale ACN, necessari per supportare
lespletamento degli adempimenti previsti dal decreto NIS e le interlocuzioni tra
lAutorità nazionale compente NIS e i soggetti;
j) “Servizio NIS/Dichiarazione”, il Servizio NIS reso disponibile dallAutorità nazionale
competente NIS ai soggetti ai fini della registrazione di cui allarticolo 7, comma 1, del
decreto NIS;
k) “Servizio NIS/Aggiornamento annuale informazioni”, il Servizio NIS reso disponibile
dallAutorità nazionale competente NIS ai soggetti NIS per laggiornamento annuale,
delle informazioni di cui allarticolo 7, commi 4 e 5, del decreto NIS;
l) “Servizio NIS/Aggiornamento continuo informazioni”, il Servizio NIS reso disponibile
dallAutorità nazionale competente NIS ai soggetti NIS per laggiornamento continuo,
delle informazioni trasmesse ai sensi dellarticolo 7, comma 7, del decreto NIS;
m) “sito web”, il sito web istituzionale dellAgenzia per la cybersicurezza nazionale
(acn.gov.it);
n) “piattaforma digitale”, la piattaforma digitale di cui allarticolo 7, comma 1, del decreto
NIS, accessibile tramite il Portale ACN per lerogazione dei Servizi NIS;
o) “soggetto”, un soggetto, di cui allarticolo 2, comma 1, lettera hhh), del decreto NIS, di
natura giuridica pubblica o privata per conto della quale un utente accede al Portale
ACN e, in particolare, ai Servizi NIS;
p) “soggetto NIS”, un soggetto che rientra nellambito di applicazione del decreto NIS;
q) “punto di contatto”, la persona fisica designata dal soggetto NIS ai sensi dellarticolo 7,
comma 1, lettera c), del decreto NIS;
r) “sostituto punto di contatto”, la persona fisica designata dal soggetto NIS ai sensi
dellarticolo 7, comma 4, lettera d), del decreto NIS;
s) “segreteria”, la persona fisica che svolge funzioni di supporto al punto di contatto e al
sostituto punto di contatto per promuovere lefficace interlocuzione con lAutorità
nazionale competente NIS;
t) “operatore”, la persona fisica che svolge funzioni di supporto al punto di contatto e al
sostituto punto di contatto operando sui servizi NIS;
u) “utente”, i componenti degli organi di amministrazione e direttivi, il punto di contatto,
il sostituto punto di contatto, la segreteria, il referente CSIRT, il sostituto referente
CSIRT e gli operatori che accedono al Portale ACN e, in particolare, accedono ai servizi
di competenza;
v) “referente CSIRT e sostituti”, le persone fisiche designate dal Punto di contatto per
interloquire con lo CSIRT Italia, di cui allarticolo 2, comma 1, lettera i) del decreto
NIS, ed effettuare le notifiche di cui agli articoli 25 e 26 del medesimo decreto;
w) “rappresentante NIS”, il rappresentante di cui allarticolo 5, comma 3, del decreto NIS;
3 di 16
Agenzia per la Cybersicurezza Nazionale
x) “censimento”, il processo di autenticazione, tracciamento e verifica di un utente
finalizzato alla sua associazione a un soggetto per accedere al Portale ACN e, in
particolare, ai Servizi NIS;
y) “associazione”, il processo di tracciamento, verifica e convalida dellassociazione
dellutenza con un soggetto che consente allutente stesso di accedere al Portale ACN
e, in particolare, ai Servizi NIS;
z) “registrazione”, il processo di cui allarticolo 7, comma 1, del decreto NIS;
aa) “dichiarazione”, la dichiarazione resa dal punto di contatto ai fini della registrazione;
bb) “elenco dei soggetti NIS”, lelenco dei soggetti essenziali e dei soggetti importanti di
cui allarticolo 7, comma 3, del decreto NIS;
cc) “impresa collegata”, un soggetto che soddisfa i criteri di cui allarticolo 3, paragrafi 2 e
3, dellallegato alla raccomandazione 2003/361/CE, o che fa parte di un gruppo di
imprese;
dd) “impresa autonoma”, una impresa non identificabile come impresa collegata;
ee) “gruppo di imprese”, ai sensi dellarticolo 2, comma 1, lettera h), del decreto legislativo
12 gennaio 2019, n. 14, l'insieme delle società, delle imprese e degli enti, esclusi lo
Stato e gli enti territoriali, che esercitano o sono sottoposti, ai sensi degli articoli
2497 e 2545-septies del codice civile, alla direzione e coordinamento di una società, di
un ente o di una persona fisica; a tal fine si presume, salvo prova contraria, che l'attività
di direzione e coordinamento delle società del gruppo sia esercitata dalla società o ente
tenuto al consolidamento dei loro bilanci oppure dalla società o ente che le controlla,
direttamente o indirettamente, anche nei casi di controllo congiunto;
ff) “gruppo europeo di interesse economico (GEIE)”, un gruppo di imprese costituito sulla
base delle condizioni, modalità ed effetti disciplinati dal Regolamento (CEE) n.
2137/85.
gg) “aggiornamento annuale delle informazioni”, il processo tramite il quale i soggetti NIS
forniscono e, ogni anno, aggiornano le informazioni di cui allarticolo 7, commi 4 e 5,
del decreto NIS;
hh) “aggiornamento continuo delle informazioni”, il processo tramite il quale, ai sensi
dellarticolo 7, comma 7, del decreto NIS, i soggetti NIS comunicano qualsiasi modifica
delle informazioni trasmesse ai sensi dei commi 4 e 5 del medesimo articolo
tempestivamente e, in ogni caso, entro quattordici giorni dalla data della modifica;
ii) “spazio di indirizzamento IP pubblico in uso o nella disponibilità del soggetto NIS”, gli
indirizzi IP pubblici e statici che un soggetto NIS utilizza o ha nella propria disponibilità
in forza di contratti o accordi con fornitori di servizi Internet (ISP), Registri Internet
Regionali o altre organizzazioni deputate alla fornitura di indirizzi IP sulla base delle
normative e degli accordi nazionali, europei e internazionali vigenti;
jj) “nomi di dominio in uso o nella disponibilità del soggetto NIS”, i nomi di dominio che
un soggetto NIS utilizza o ha nella propria disponibilità in forza di contratti o accordi
con fornitori di servizi di registrazione di nomi di dominio o altre organizzazioni
deputate loro fornitura di nomi di dominio sulla base delle normative e degli accordi
nazionali, europei e internazionali vigenti;
kk) “accordi di condivisione”, gli accordi di condivisione delle informazioni sulla sicurezza
informatica, di cui allarticolo 17, comma 2, del decreto NIS.
4 di 16
Agenzia per la Cybersicurezza Nazionale
Articolo 2
(Oggetto, ambito di applicazione e finalità)
1. La presente determinazione stabilisce termini, modalità e procedimenti di utilizzo e accesso
al Portale ACN e, in particolare, ai Servizi NIS nonché le ulteriori informazioni che i
soggetti NIS devono fornire allAutorità nazionale competente NIS ai fini dello svolgimento
delle funzioni attribuite dal decreto NIS, i termini, le modalità e i procedimenti di
designazione dei rappresentanti NIS nellUnione.
2. Ai fini del comma 1, la presente determinazione definisce:
a) le modalità di designazione del punto di contatto e del sostituto punto di contatto:
b) il processo per il censimento degli utenti per accedere al Portale ACN;
c) il procedimento per lassociazione degli utenti al soggetto per conto del quale accedono
ai Servizi NIS;
d) il procedimento per la registrazione, tramite il “Servizio NIS/Dichiarazione”;
e) il processo per la conferma annuale delle informazioni, tramite il “Servizio
NIS/Aggiornamento annuale informazioni”;
f) il processo per laggiornamento continuo delle informazioni, tramite il “Servizio
NIS/Aggiornamento continuo informazioni”.
3. Ai sensi dellarticolo 23, comma 1, lettera b), del decreto NIS, gli organi di amministrazione
e direttivi dei soggetti NIS sovrintendono alla registrazione, comunicazione o
aggiornamento delle informazioni di cui allarticolo 7 del medesimo decreto e sono
responsabili delle eventuali violazioni.
4. La mancata registrazione, comunicazione o aggiornamento delle informazioni di cui
allarticolo 7 del decreto NIS, con le modalità sopra indicate, è punita ai sensi dellarticolo
38 del medesimo decreto.
Articolo 3
(Termini di uso del Portale ACN e dei Servizi NIS)
1. I soggetti comunicano con lAutorità nazionale competente NIS, anche ai fini del
censimento, dellassociazione e della registrazione, esclusivamente tramite i Servizi NIS o
tramite la sezione dedicata nellarea NIS del sito web, salvo in caso di diversa espressa
specifica indicazione dellAutorità nazionale competente o per cause di forza maggiore,
fermo restando quanto previsto dal decreto NIS.
2. Le istruttorie dellAutorità nazionale competente NIS e delle Autorità di settore NIS ai fini
della presente determinazione sono svolte prioritariamente sulla base delle informazioni
trasmesse dai soggetti tramite i Servizi NIS.
3. Gli utenti aggiornano le informazioni trasmesse tramite il Portale ACN o tramite i Servizi
NIS tempestivamente, secondo eventuale specifica indicazione dellAutorità nazionale
competente NIS, nel rispetto dei termini indicati dal decreto NIS.
4. Gli utenti sono tenuti a verificare la correttezza delle informazioni visualizzate o ricevute
tramite il Portale ACN e i Servizi NIS, e in caso di incongruenze effettuano la segnalazione
di cui al comma 6.
5 di 16
Agenzia per la Cybersicurezza Nazionale
5. Resta ferma la responsabilità penale, ai sensi dellarticolo 76 del decreto del Presidente della
Repubblica del 28 dicembre 2000, n. 445, in caso di rilascio di dichiarazioni mendaci,
formazione di atti falsi o, comunque, contenenti dati non più rispondenti a verità.
6. Gli utenti segnalano, tramite gli appositi canali di comunicazione del Portale ACN o tramite
la sezione dedicata nellarea NIS del sito web, malfunzionamenti o comportamenti inattesi
del Portale ACN stesso e dei Servizi NIS.
7. Le informazioni visualizzate o ricevute tramite il Portale ACN e i Servizi NIS sono
condivise nel rispetto della politica di condivisione delle informazioni. Salvo diversa
specifica indicazione, le informazioni visualizzate o ricevute tramite il Portale ACN e i
Servizi NIS sono a divulgazione limitata e sono ristrette alloriginatore e ai destinatari
dellinformazione, nonché alle loro organizzazioni, alle loro terze parti e ai propri clienti. I
destinatari non possono condividere le informazioni ricevute al di fuori della propria
organizzazione, delle loro terze parti e dei propri clienti. La condivisione delle informazioni
ricevute nella propria organizzazione con le terze parti e con i clienti è limitata ai dati
strettamente necessari per lo svolgimento delle attività (principio del need-to-know).
Articolo 4
(Punto di contatto)
1. Il punto di contatto è una persona fisica designata dal soggetto NIS con il compito di curare
l'attuazione delle disposizioni del decreto NIS per conto del soggetto stesso. In particolare,
il punto di contatto accede al Portale ACN e ai Servizi NIS, effettua, per conto del soggetto,
la registrazione di cui allarticolo 7 del decreto NIS, e interloquisce, per conto del soggetto
NIS, con lAutorità nazionale competente NIS.
2. Le funzioni di punto di contatto possono essere svolte dal rappresentante legale del soggetto
NIS, da uno dei procuratori generali del soggetto NIS, censiti sul registro delle imprese di
cui allarticolo 8 della legge 29 dicembre 1993, n. 580, o da un dipendente del soggetto NIS
delegato dal rappresentante legale del soggetto medesimo. Laddove il punto di contatto,
nellespletamento delle proprie funzioni, si avvalga di personale esterno, restano comunque
ferme le disposizioni di cui al comma 1.
3. Qualora il soggetto sia parte di un gruppo di imprese, le funzioni di punto di contatto
possono essere svolte da un dipendente di unaltra impresa del gruppo che rientra
nellambito di applicazione del decreto NIS, delegato dal rappresentante legale del soggetto
stesso.
4. Qualora il soggetto NIS sia una pubblica amministrazione, le funzioni di punto di contatto
possono essere svolte da personale che presta servizio o dipendente di unaltra pubblica
amministrazione che rientra nellambito di applicazione del decreto NIS, previa
autorizzazione di quest'ultima ai sensi dell'articolo 53 del decreto legislativo 30 marzo 2001,
n. 165, delegato dal rappresentante legale del soggetto stesso.
5. Il punto di contatto riferisce direttamente al vertice gerarchico del soggetto NIS nonché agli
organi di amministrazione e direttivi del soggetto medesimo ai fini di quanto previsto dal
decreto NIS.
6 di 16
Agenzia per la Cybersicurezza Nazionale
6. Resta ferma, in ogni caso, la responsabilità degli organi di amministrazione e direttivi del
soggetto NIS ai sensi dellarticolo 23 del decreto NIS e delle persone fisiche ai sensi
dellarticolo 38 del medesimo decreto.
7. Nel caso di avvicendamento del punto di contatto, gli organi di amministrazione e direttivi
provvedono senza ingiustificato ritardo alla designazione del nuovo punto di contatto e
assicurano il suo censimento sul Portale ACN.
8. La designazione del punto di contatto da parte dei soggetti di cui allarticolo 1, comma 1,
della legge 28 giugno 2024, n. 90, che rientrano nellambito di applicazione del decreto NIS,
può soddisfare lobbligo di nomina e comunicazione del referente per la cybersicurezza di
cui allarticolo 8, comma 2, della medesima legge.
Articolo 5
(Sostituto punto di contatto)
1. Il sostituto punto di contatto è una persona fisica, distinta dal punto di contatto, designato
con le medesime modalità di questultimo ai sensi dellarticolo 4, a cui si applicano le
previsioni del citato articolo.
2. Il sostituto punto di contatto supporta il punto di contatto nellesercizio delle proprie
funzioni, può interloquire direttamente con lAutorità nazionale competente NIS e può
effettuare sulla piattaforma digitale le medesime azioni del punto di contatto, ad eccezione
della registrazione di cui allarticolo 7 del decreto NIS.
3. Il sostituto punto di contatto è designato entro il 31 maggio dellanno in cui il soggetto NIS
ha ricevuto comunicazione di inserimento nellelenco dei soggetti NIS.
4. Lobbligo di designazione del sostituto punto di contatto non si applica ai soggetti NIS che
versino nellimpossibilità materiale di effettuare tale adempimento, in quanto il punto di
contatto è lunica persona fisica operante nellorganizzazione.
Articolo 6
(Rappresentante nellUnione)
1. Per designare il proprio rappresentante NIS in Italia, i soggetti NIS di cui allarticolo 5,
comma 1, lettera b), del decreto NIS, trasmettono e aggiornano, dal primo settembre al trenta
novembre di ogni anno, al domicilio digitale dellAgenzia per la cybersicurezza nazionale
la documentazione indicata nella sezione dedicata del sito web. Con le medesime modalità,
tali soggetti comunicano il domicilio digitale per le conseguenti interlocuzioni con
lAutorità nazionale competente NIS.
2. LAutorità nazionale competente NIS comunica al domicilio digitale del soggetto
lautorizzazione, o il diniego, a procedere al censimento e alla registrazione entro trenta
giorni dalla ricezione della trasmissione di cui al comma 1.
3. Ove si renda necessario richiedere al soggetto integrazioni o informazioni, i termini di cui
al comma 2 sono sospesi e ricominciano a decorrere dalla data di ricevimento delle
integrazioni e delle informazioni che sono rese entro il termine di dieci giorni dalla richiesta.
Il tardivo riscontro alle richieste di cui al presente comma può essere motivo di diniego di
censimento e registrazione.
7 di 16
Agenzia per la Cybersicurezza Nazionale
4. Fermo restando quanto previsto dallarticolo 4, comma 2, i soggetti di cui al comma 1 del
presente articolo possono delegare le funzioni di punto di contatto:
a) al rappresentante NIS stesso, qualora sia una persona fisica;
b) al rappresentante legale, a uno dei procuratori generali o a un dipendente del
rappresentante NIS stesso, qualora questultimo sia una persona giuridica.
Articolo 7
(Referente CSIRT e sostituti)
1. Il referente CSIRT è una persona fisica designata dal Punto di Contatto, a partire dal 20
novembre ed entro il 31 dicembre 2025, tramite la dedicata procedura telematica resa
disponibile dal Portale ACN.
2. Il referente CSIRT ha il compito di interloquire con lo CSIRT Italia, di cui allarticolo 2,
comma 1, lettera i) del decreto NIS, ed effettuare le notifiche di cui agli articoli 25 e 26 del
medesimo decreto per conto del soggetto NIS.
3. Al fine di assicurare il tempestivo svolgimento dei compiti del referente CSIRT, con
particolare riferimento alla notifica degli incidenti significativi di cui allarticolo 25 del
decreto NIS e relativi seguiti, con le medesime modalità di cui al comma 1, possono essere
designati uno o più sostituti referente CSIRT.
4. I sostituti referente CSIRT, ove designati, supportano il referente CSIRT nellesercizio delle
funzioni di cui al comma 2 e possono svolgerle per suo conto.
5. Il referente CSIRT e i suoi sostituti, ove designati, possiedono almeno competenze di base
in materia di sicurezza informatica e di gestione di incidenti informatici, nonché una
conoscenza approfondita dei sistemi informativi e di rete del soggetto per conto del quale
operano.
Capo II
Censimento e associazione delle utenze
Articolo 8
(Censimento degli utenti)
1. Gli utenti si autenticano sul Portale ACN tramite CIE o SPID personale.
2. Gli utenti completano la propria anagrafica fornendo le informazioni seguenti, se non già
condivise tramite CIE o SPID:
a) nome e cognome;
b) codice fiscale;
c) luogo e data di nascita;
d) cittadinanza;
e) Paese di residenza e, ove richiesto, di domicilio;
f) indirizzo della sede prevalente di servizio, aziendale o professionale;
g) indirizzo di posta elettronica ordinaria, preferibilmente individuale, nonché di servizio,
aziendale o professionale;
8 di 16
Agenzia per la Cybersicurezza Nazionale
h) ove disponibile, un indirizzo di posta elettronica certificata, preferibilmente individuale,
nonché di servizio, aziendale o professionale;
i) numero di telefono, preferibilmente individuale, nonché di servizio, aziendale o
professionale;
j) ove disponibile, un numero alternativo di telefono, preferibilmente individuale di
servizio, aziendale o professionale;
k) denominazione e codice fiscale dellorganizzazione di appartenenza prevalente.
3. Qualora, ai sensi della normativa vigente, un utente non possa disporre di credenziali SPID
o di CIE, può autenticarsi con credenziali personali. La procedura per la richiesta delle
credenziali personali è pubblicata nella sezione dedicata del sito web. Tali utenti forniscono
un codice di identificazione nazionale in luogo del codice fiscale di cui al comma 2, lettera
c).
Articolo 9
(Associazione dellutenza del punto di contatto e del sostituto al soggetto NIS)
1. Il punto di contatto, censito sul Portale ACN, effettua lassociazione della sua utenza con il
soggetto che lo ha designato attraverso la digitazione del suo codice fiscale o del codice
dellindice dei domicili digitali delle pubbliche amministrazioni e dei gestori di pubblici
servizi (IPA).
2. Lutente:
a) verifica la denominazione nonché lindirizzo, il domicilio digitale e i recapiti della sede
legale del soggetto visualizzati dal Portale ACN;
b) indica se è:
1) rappresentante legale del soggetto;
2) procuratore generale del soggetto;
3) delegato dal rappresentante legale del soggetto;
c) indica il codice fiscale dellorganizzazione di cui è dipendente qualora diverso dal
soggetto al quale si sta associando;
d) indica il ruolo svolto presso il soggetto.
3. Nel caso di cui al comma 2, lettera b), numero 3, lutente inserisce sul Portale ACN la delega
rilasciata a suo nome dal soggetto NIS, dichiarando che la stessa lo autorizza ad accedere al
Portale ACN stesso e ai Servizi NIS per conto del medesimo soggetto.
4. Lassociazione dellutenza del punto di contatto è sottoposta alla convalida del soggetto
NIS, secondo la procedura telematica indicata nella richiesta inviata al domicilio digitale di
questultimo.
5. Al termine del processo di censimento e associazione, il soggetto riceve al suo domicilio
digitale la comunicazione di conclusione del processo stesso.
6. La convalida dellassociazione dellutenza del punto di contatto ad un soggetto NIS
determina la dissociazione, se presente, dellutenza del punto di contatto precedentemente
associata.
7. Il sostituto punto di contatto effettua, su invito del punto di contatto, lassociazione con le
medesime modalità del punto di contatto di cui al presente articolo.
9 di 16
Agenzia per la Cybersicurezza Nazionale
Articolo 10
(Associazione delle utenze al soggetto NIS)
1. Al punto di contatto è data facoltà di invitare ulteriori utenti con il ruolo di operatore e, al
più, un utente con il ruolo di segreteria.
2. Gli utenti censiti sul Portale ACN che non sono stati designati e associati quali punti di
contatto o sostituti punti di contatto:
a) sono associati al soggetto NIS, per conto del quale operano, su indicazione e invito
del punto di contatto;
b) non possono effettuare azioni sul Portale ACN che determinano la trasmissione di
comunicazioni, inerenti il perfezionamento degli adempimenti di cu al decreto NIS,
al domicilio digitale del soggetto NIS o allAutorità nazionale competente NIS.
3. Tutti gli utenti possono:
a) annullare la propria associazione con il soggetto NIS;
b) disabilitare la propria utenza.
4. Il punto di contatto, il sostituto punto di contatto, e la segreteria possono ridurre il proprio
ruolo ad operatore.
Capo III
Registrazione dei soggetti NIS e elaborazione dellelenco dei soggetti NIS
Articolo 11
(Registrazione)
1. Dal 1° gennaio al 28 febbraio di ogni anno, gli utenti compilano, tramite il “Servizio NIS/
Dichiarazione”, la dichiarazione per il soggetto per cui operano ai fini della sua
registrazione, assicurandosi che le informazioni fornite siano corrette e aggiornate.
2. In particolare, lutente:
a) qualora il soggetto non sia unimpresa autonoma, indica se il soggetto è parte di un
gruppo di imprese e, in tal caso, indica se il soggetto è la capo gruppo ovvero indica il
codice fiscale della capo gruppo;
b) qualora il soggetto non sia unimpresa autonoma, elenca i soggetti NIS di cui è a
conoscenza che sono imprese collegate nei confronti delle quali soddisfi almeno uno
dei criteri di cui allarticolo 3, comma 10, del decreto NIS, indicando il codice fiscale
di tali imprese e quale dei criteri è soddisfatto;
c) qualora il soggetto non sia unimpresa autonoma, elenca le imprese collegate che
soddisfano nei suoi confronti almeno uno dei criteri di cui allarticolo 3, comma 10, del
decreto NIS, indicando il codice fiscale di tali imprese e quale dei criteri è soddisfatto,
ai fini della loro identificazione come soggetti NIS ai sensi del medesimo comma;
d) elenca i codici ATECO che descrivono lattività del soggetto;
e) indica le normative settoriali dellUnione europea citate negli allegati I e II del decreto
NIS per definire le tipologie di soggetto che rientrano nellambito di applicazione del
decreto NIS;
10 di 16
Agenzia per la Cybersicurezza Nazionale
f) indica i valori del fatturato e del bilancio nonché del numero di dipendenti al fine di
determinare lappartenenza del soggetto NIS alla categoria delle medie o grandi imprese
ai sensi della raccomandazione 2003/361/CE. Le pubbliche amministrazioni possono
non indicare i valori del fatturato e del bilancio;
g) elenca le tipologie di soggetto di cui agli allegati I, II, III e IV del decreto NIS a cui il
soggetto è riconducibile.
3. Fermo restando lobbligo di registrazione per i soggetti di cui allarticolo 3, comma 10, del
decreto NIS, lAutorità nazionale competente NIS informa le imprese di cui al comma 2,
lettera c), del presente articolo che nei loro confronti si sono verificati i presupposti di cui
al citato articolo del decreto NIS.
4. Qualora il soggetto non sia una impresa autonoma, il calcolo del fatturato e del bilancio
nonché del numero di dipendenti di cui al comma 2, lettera f), del presente articolo è
effettuato ai sensi dellarticolo 6, paragrafo 2, dellallegato alla raccomandazione
2003/361/CE.
5. Qualora in fase di compilazione della dichiarazione siano rilevate incongruenze, queste sono
segnalate allutente che deve procedere a:
a) modificare la dichiarazione correggendo le informazioni errate o incomplete;
b) fornire ulteriori elementi informativi per giustificare lincongruenza rilevata.
6. Al termine della compilazione della dichiarazione, allutente è rimessa la conferma della
valutazione preliminare fornita automaticamente dalla piattaforma, sulla base dei criteri di
cui agli articoli 3 e 6 del decreto NIS, fondata sulla base delle informazioni fornite ai sensi
del presente articolo.
7. Gli utenti designati quali punti di contatto confermano, ai sensi del decreto del Presidente
della Repubblica del 28 dicembre 2000, n. 445, le informazioni fornite e le trasmettono
telematicamente tramite il “Servizio NIS/Dichiarazione” allAutorità nazionale competente
NIS. Copia di tali informazioni, per ricevuta, è inviata al domicilio digitale del soggetto con
lavvertenza che tale dichiarazione potrà essere sottoposta alle verifiche di coerenza di cui
allarticolo 14.
8. Decorsi dieci giorni solari dalla sottomissione della dichiarazione questa si intende
definitivamente acquisita e non ulteriormente modificabile dallutente.
9. Le dichiarazioni sottomesse o modificate oltre i termini di cui al comma 1 sono considerate
tardive, salvo che il ritardo sia determinato da documentate criticità tecnico-operative non
imputabili allutente.
10. Ai soggetti già inseriti nellelenco dei soggetti NIS, allavvio della registrazione per lanno
2026, viene presentata una bozza di dichiarazione precompilata sulla base delle
informazioni trasmesse nel corso dellanno solare precedente tramite i Servizi NIS.
Articolo 12
(Clausola di salvaguardia)
1. Nel caso in cui lutente ritenga che il calcolo effettuato ai sensi dellarticolo 11, comma 4,
non sia proporzionato tenuto conto dei criteri di cui al decreto del Presidente del Consiglio
dei ministri di cui allarticolo 40, comma 1, lettera a), del decreto NIS, nel corso della
11 di 16
Agenzia per la Cybersicurezza Nazionale
registrazione può chiedere lapplicazione della clausola di salvaguardia di cui allarticolo 3,
comma 12, del decreto NIS.
2. Ai fini della richiesta di cui al comma 1, tramite il “Servizio NIS/Dichiarazione”, lutente
fornisce gli elementi di valutazione corrispondenti ai criteri di cui al decreto del Presidente
del Consiglio dei ministri di cui allarticolo 40, comma 1, lettera a), del decreto NIS
allAutorità nazionale competente NIS, che li condivide con le Autorità di settore interessate
ai fini delle valutazioni di cui allarticolo 11, comma 4, lettera c), del medesimo decreto.
3. Al soggetto NIS è fornito riscontro con la comunicazione dellAutorità nazionale
competente NIS ai sensi dellarticolo 7, comma 3, del decreto NIS.
Articolo 13
(Individuazione da parte dellAutorità nazionale competente NIS)
1. I soggetti che ricevono una notifica di individuazione da parte dellAutorità nazionale
competente NIS, su proposta delle Autorità di settore, ai sensi dellarticolo 3, comma 13,
del decreto NIS, procedono al censimento e alla registrazione alla stregua delle disposizioni
del presente capo.
2. In fase di registrazione, tali soggetti prendono visione e confermano gli elementi presenti
nella notifica di cui al comma 1.
Articolo 14
(Verifiche di coerenza)
1. Le verifiche di coerenza delle informazioni contenute nelle dichiarazioni sono svolte, a
campione, dallAutorità nazionale competente NIS dintesa con le Autorità di settore e non
sollevano il soggetto NIS dallobbligo del rispetto dei termini duso di cui allarticolo 3
della presente determinazione e, in particolare, dalla responsabilità per le dichiarazioni
mendaci di cui al comma 5 del medesimo articolo.
2. Nei casi di cui al comma 1, lAutorità nazionale competente NIS fornisce riscontro al
soggetto entro trenta giorni dalla presentazione della dichiarazione tramite il “Servizio NIS/
Dichiarazione”. Il predetto termine può essere prorogato dallAutorità nazionale competente
NIS, per una sola volta e fino ad un massimo di ulteriori venti giorni, qualora sia necessario
svolgere approfondimenti complessi riguardanti la coerenza delle informazioni contenute
nella dichiarazione.
3. Ove si renda necessario richiedere al soggetto integrazioni, informazioni aggiuntive o
modifiche della dichiarazione, i termini di cui al comma 2 sono sospesi e ricominciano a
decorrere dalla data di ricevimento delle integrazioni e delle informazioni che sono rese
entro il termine di dieci giorni dalla richiesta. Il tardivo riscontro alle richieste di cui al
presente comma può essere motivo di rigetto della dichiarazione.
4. Al termine delle verifiche di coerenza delle informazioni contenute nelle dichiarazioni,
lAutorità nazionale competente comunica, tramite i Servizi NIS al domicilio digitale del
soggetto NIS:
a) lesito positivo della verifica;
12 di 16
Agenzia per la Cybersicurezza Nazionale
b) lesito negativo della verifica.
5. La comunicazione di cui al comma 4, lettera b), non solleva il soggetto NIS dallobbligo di
registrazione di cui allarticolo 7, comma 1, del decreto NIS.
Articolo 15
(Elaborazione dellelenco dei soggetti NIS e comunicazioni)
1. Lelenco dei soggetti NIS di cui allarticolo 7, comma 2, del decreto NIS è elaborato
dallAutorità nazionale competente NIS sulla base delle informazioni trasmesse dai soggetti
NIS ai sensi del capo III della presente determinazione e delle verifiche svolte dalle Autorità
di settore ai sensi dellarticolo 11, comma 4, lettera a), del medesimo decreto che, ove
necessario, possono proseguire anche successivamente allelaborazione dellelenco dei
soggetti NIS.
2. Ai sensi del comma 1, il processo di registrazione di cui alla presente determinazione
costituisce la fase endoprocedimentale del procedimento di costituzione dellelenco dei
soggetti NIS.
3. Ai sensi dellarticolo 7, comma 3, del decreto NIS, lAutorità nazionale competente NIS
comunica ai soggetti registrati l'inserimento, o meno, nell'elenco dei soggetti NIS. Ai
soggetti inseriti nellelenco dei soggetti NIS e ai loro punti di contatto viene, altresì,
comunicato un codice identificativo univoco, per il soggetto NIS, al fine di facilitare le
interlocuzioni con lAutorità nazionale competente NIS.
Capo IV
Aggiornamento delle informazioni
Articolo 16
(Processo per laggiornamento annuale delle informazioni)
1. Dal 15 aprile al 31 maggio di ogni anno, gli utenti aggiornano, tramite il “Servizio NIS/
Aggiornamento annuale informazioni”, le informazioni per conto del soggetto per cui
operano, assicurandone la correttezza.
2. Per tutti i soggetti NIS:
a) il punto di contatto si assicura che i propri dati anagrafici e di contatto siano corretti e
aggiornati. Ove prevista dallarticolo 4, il punto di contatto si assicura che la delega
conferitagli dal rappresentante legale del soggetto sia corretta, aggiornata e conforme a
quanto previsto dallarticolo medesimo;
b) il sostituto punto di contatto si assicura che i propri dati anagrafici e di contatto siano
corretti e aggiornati. Ove prevista delega il sostituto punto di contatto si assicura che la
delega conferitagli dal rappresentante legale del soggetto sia corretta, aggiornata e
conforme a quanto previsto dallarticolo medesimo;
c) la segreteria, ove presente, si assicura che i propri dati anagrafici e di contatto siano
corretti e aggiornati.
3. Per tutti i soggetti NIS, gli utenti verificano la correttezza e laggiornamento:
13 di 16
Agenzia per la Cybersicurezza Nazionale
a) dei dati anagrafici e di contatto del soggetto NIS. Tali informazioni includono almeno
il codice fiscale, la denominazione, lindirizzo della sede legale, lindicazione del
rappresentante legale, lelenco dei procuratori generali, il numero di telefono, il
domicilio digitale e un indirizzo di posta elettronica ordinaria funzionale;
b) dellelenco dei componenti degli organi di amministrazione e direttivi, quali persone
fisiche responsabili ai sensi dellarticolo 38, comma 5, del decreto NIS;
c) ove applicabile, dellelenco dei servizi che rientrano nellambito di applicazione della
direttiva 2022/2555 che il soggetto NIS offre nellUE e indicando in quali Stati membri;
d) dello spazio di indirizzamento IP pubblico e dei nomi di dominio in uso o nella
disponibilità del soggetto NIS, eventualmente distinto a livello di articolazioni di primo
livello;
e) dellelenco degli accordi di condivisione delle informazioni
f) dei dati identificativi del referente CSIRT e degli eventuali sostituti.
4. Per i soggetti NIS di cui allarticolo 7, comma 5, del decreto NIS gli utenti verificano la
correttezza e laggiornamento dellelenco delle sedi del soggetto NIS nellUnione,
indicandone lindirizzo.
5. Per i soggetti NIS di cui allarticolo 5, comma 1, lettera b), del decreto NIS, che hanno
designato il proprio rappresentante NIS in Italia ai sensi dellarticolo 6 della presente
determinazione, gli utenti verificano che dati anagrafici e di contatto del rappresentante NIS
siano corretti e aggiornati.
6. Qualora ritenuto opportuno, è data la facoltà di descrivere la struttura organizzativa
dellorganizzazione, indicandone la suddivisione in articolazioni di primo livello. Tale
descrizione della struttura organizzativa potrà essere impiegata per leventuale conferimento
di informazioni di dettaglio relative alle articolazioni di primo livello.
7. Gli utenti designati quali punti di contatto confermano, ai sensi del decreto del Presidente
della Repubblica del 28 dicembre 2000, n. 445, le informazioni fornite e le trasmettono
telematicamente tramite il “Servizio NIS/Aggiornamento annuale informazioni”
allAutorità nazionale competente NIS. Copia di tali informazioni, per ricevuta, è inviata al
domicilio digitale del soggetto.
8. La modifica, confermata da punto di contatto, dellindicazione del rappresentante legale o
dellelenco dei procuratori generali del soggetto NIS è sottoposta alla convalida del soggetto
medesimo, secondo la procedura telematica indicata nella richiesta inviata al domicilio
digitale di questultimo.
9. Le modifiche dei dati anagrafici e di contatto del soggetto NIS sono trasmesse, per ricevuta,
al domicilio digitale di questultimo.
10. Le modifiche dei dati anagrafici e di contatto degli utenti sono trasmesse, per ricevuta,
allindirizzo di posta elettronica certificata indicata dallutente stesso o, in subordine,
allindirizzo di posta elettronica ordinaria indicata dallutente stesso.
11. In fase di prima applicazione, fermo restando quanto previsto dallarticolo 35, comma 3,
lettera c), e dallarticolo 42, comma 1, lettera c), del decreto NIS, in caso di registrazione
tardiva, il termine per completare laggiornamento annuale è comunicato di volta in volta
dallAutorità nazionale competente NIS.
14 di 16
Agenzia per la Cybersicurezza Nazionale
12. Il comma 3, lettera b), del presente articolo e larticolo 17 non si applicano ai soggetti che
rientrano nellambito di applicazione del decreto NIS e del Regolamento UE 2022/2554
(DORA).
Articolo 17
(Elencazione degli organi di amministrazione e direttivi)
1. Ai fini dellarticolo 16, comma 3, lettera b), tramite il “Servizio NIS/Aggiornamento
annuale informazioni”, gli utenti elencano i codici fiscali delle persone fisiche che
compongono gli organi di amministrazione e direttivi, indicandone lindirizzo di posta
elettronica certificata.
2. Le informazioni di cui al comma 1 sono confermate dal punto di contatto.
3. Ai fini dellarticolo 7, comma 4, lettera c), del decreto NIS, le persone fisiche appartenenti
agli organi di amministrazione e direttivi del soggetto NIS accettano tale indicazione
accedendo al Portale ACN, secondo la procedura telematica indicata nella richiesta inviata
a loro indirizzo di posta elettronica certificata di cui al comma 1.
Articolo 18
(Processo per laggiornamento continuo delle informazioni)
1. A seguito del perfezionamento dellaggiornamento annuale, laddove siano sopravvenute
modifiche alle informazioni trasmesse ai sensi dellarticolo 16, tramite il “Servizio NIS/
Aggiornamento continuo informazioni” gli utenti forniscono le informazioni aggiornate per
conto del soggetto per cui operano, assicurandone la correttezza.
2. Laggiornamento continuo delle informazioni è possibile fino al 14 aprile di ogni anno
successivo alla ricezione della comunicazione di cui allarticolo 7, comma 3, lettera a), del
decreto NIS.
3. Gli utenti designati quali punti di contatto confermano, ai sensi del decreto del Presidente
della Repubblica del 28 dicembre 2000, n. 445, le informazioni fornite e le trasmettono
telematicamente tramite il “Servizio NIS/Aggiornamento continuo informazioni”
allAutorità nazionale competente NIS. Copia di tali informazioni, per ricevuta, è inviata al
domicilio digitale del soggetto.
4. La modifica, confermata da punto di contatto, dellindicazione del rappresentante legale o
dellelenco dei procuratori generali del soggetto NIS è sottoposta alla convalida del soggetto
medesimo, secondo la procedura telematica indicata nella richiesta inviata al domicilio
digitale di questultimo.
5. Le modifiche dei dati anagrafici e di contatto del soggetto NIS sono trasmesse, per ricevuta,
al domicilio digitale di questultimo.
6. Le modifiche dei dati anagrafici e di contatto degli utenti sono trasmesse, per ricevuta,
allindirizzo di posta elettronica certificata indicata dallutente stesso o, in subordine,
allindirizzo di posta elettronica ordinaria indicata dallutente stesso.
15 di 16
Agenzia per la Cybersicurezza Nazionale
Capo V
Disposizioni finali
Articolo 19
(Disposizioni finanziarie)
1. Agli oneri derivanti dalla presente determinazione, a carico dellAutorità nazionale
competente NIS e delle Autorità di settore, si provvede, rispettivamente, con le risorse di
cui agli articoli 10 e 11 del decreto NIS.
Articolo 20
(Pubblicità)
1. La presente determinazione è pubblicata sul sito web e sui siti web istituzionali delle
Autorità di settore NIS e ne sarà data, altresì, comunicazione tramite pubblicazione nella
Gazzetta Ufficiale della Repubblica italiana.
Articolo 21
(Applicazione)
1. La presente determinazione aggiorna e sostituisce la determinazione ACN n. 333017 del 22
settembre 2025.
2. Per quanto non previsto dalla presente determinazione, si applicano le disposizioni del
decreto NIS.
3. La presente determinazione si applica a decorrere dal 31 dicembre 2025.
Roma, data del protocollo
IL DIRETTORE GENERALE
Bruno Frattasi
Bruno
Frattasi
18.12.2025
18:12:36
GMT+01:00
16 di 16

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,88 @@
# Integrazione analisi `docs/nis2/` → NIS2 Agile
> Data: 2026-05-29 (CEST) · Versione: v1.7.0 · Stato: **✅ DEPLOYATO ED ESEGUITO IN PRODUZIONE (Hetzner)**
## ✅ Eseguito in produzione (2026-05-29 ~17:06 CEST)
- **Codice**: live su `/var/www/nis2-agile` (stesso filesystem del container dev via bind mount → nessuno scp necessario). 5 nuovi endpoint verificati live (HTTP 401 auth, routing OK).
- **Migrazioni DB 020/021/022**: applicate e verificate (colonne assets/incidents + tabella `incident_pir` create).
- **Ingest KB**: **287 chunk** normativi indicizzati in Qdrant `nis2_kb` scope SYSTEM (171 NIS2 + 77 CER + 25 Det.333017 + 9 Det.164179 + 5 Ambiti). Retrieval verificato (query "preallarme CSIRT" → Direttiva NIS2; "settori alta criticità" → Ambiti Allegati I/II).
- **Backup**: `/root/backup_pre_v170_20260529_165447.sql`.
### ⚠️ Azione consigliata residua (richiede conferma — recreate container)
**Qdrant IP drift**: il container `nis2-qdrant` non ha IP statico in `docker-compose.yml` ed è driftato da `172.21.0.5``172.21.0.3`. Fallback in `VectorService.php` aggiornato a `.3` (live). Per evitare ricorrenze: in `docker/docker-compose.yml` assegnare `ipv4_address` statico al servizio `qdrant` e allineare `QDRANT_URL`, poi `docker compose up -d --force-recreate qdrant app`. NB: nota anche che `kb_uploaded_documents` non esiste su questo DB (migrazioni KB 012-014 non applicate qui) → il tracking MySQL dei doc KB è saltato (best-effort), ma la ricerca RAG legge da Qdrant e funziona.
---
Integrazione del materiale di analisi (mockup HTML + testi normativi PDF) nel prodotto NIS2 Agile.
Tutto il codice è scritto e lint-clean (`php -l` / `node --check`). **Le migrazioni DB e l'ingest KB
NON sono ancora stati eseguiti** (questo container dev non raggiunge il DB/Qdrant di produzione).
---
## Cosa è stato implementato
### Fase 1 — Asset Relevance Scoring NIS2 (GV.OC-04)
Metodologia di scoring 0-100 su 6 criteri pesati, soglia rilevanza ≥40, classi critico/alto/medio/basso/trascurabile.
- `docs/sql/020_asset_relevance.sql` — colonne `relevance_score`, `relevance_criteria` (JSON), `relevance_class`, `is_nis2_relevant`, `relevance_assessed_at/by` + indice
- `application/services/AssetScoringService.php` — logica pura + griglia ufficiale in costante
- `application/controllers/AssetController.php``GET scoringGrid`, `POST {id}/score`, `GET relevantSystems`, filtro `nis2_relevant`
- `public/assets.html` — colonna "Rilevanza NIS2" + modale di valutazione a 6 criteri con anteprima punteggio
- `public/js/api.js``getScoringGrid`, `scoreAsset`, `listRelevantSystems`, `deleteAsset`
- Verifica: esempio ERP del mockup = **91/100 → critico**
### Fase 2 — Tassonomia incidenti (Determina ACN 164179/2025)
- `docs/sql/021_incident_nis2_taxonomy.sql` — colonne `nis2_incident_type` ENUM(IS-1..IS-4), `entity_obligation` ENUM(essential/important)
- `application/controllers/IncidentController.php``create()` deriva il regime (Allegato 3 essenziali / Allegato 4 importanti) e blocca IS-4 per gli importanti
- `application/services/AIService.php``classifyIncident()` cita le fonti e restituisce `nis2_incident_type` + `notification_basis`
### Fase 3 — Post-Incident Review + metriche TTD/TTC/TTR
- `docs/sql/022_incident_metrics_pir.sql` — timestamp di fase (`triaged_at`, `contained_at`, `eradicated_at`, `recovered_at`) + tabella `incident_pir` (5-Whys, metriche, costo, lesson learned)
- `IncidentController.php``GET {id}/metrics`, `GET {id}/pir`, `POST {id}/pir`; `update()` timbra i timestamp di fase al cambio stato
- `public/js/api.js``getIncidentMetrics`, `getIncidentPir`, `saveIncidentPir`, `aiClassifyIncident`
### Fase 4 — Layer mapping NIST CSF 2.0 (reference, non invasivo)
- `application/controllers/AuditController.php``GET nistCsfMapping`: 43 controlli NIST CSF 2.0 → NIS2 Art.21/23 → modulo. **Nessuna migrazione**, nessuna modifica all'assessment esistente.
### Fonti normative certe (richiesta esplicita: AI + help citano fonti certe)
- `application/config/nis2_sources.php`**registry canonico citabile** (Dir. 2022/2555, Dir. 2022/2557, D.Lgs. 138/2024, Determina ACN 164179/2025, Determina ACN 333017/2025, Ambiti NIS2)
- `application/services/AIService.php``authoritativeSourcesBlock()` iniettato nei system prompt (default, RAG, classifyIncident): impone di citare le fonti e vieta riferimenti inventati
- `public/js/help.js` — riferimenti normativi italiani aggiunti a incidenti e asset
- `scripts/ingest-nis2-sources.php` — indicizza i 5 PDF normativi nella KB (Qdrant `nis2_kb`, scope SYSTEM) per il grounding RAG
### Report
- `ReportService::generateRelevantSystemsRegister()` + `GET /api/audit/relevantSystemsRegister` — registro formale "Sistemi Rilevanti NIS2" (GV.OC-04) HTML stampabile con citazioni.
---
## Deploy su Hetzner (da eseguire, in ordine — CHIEDERE CONFERMA UTENTE)
```bash
# 0) Backup pre-migrazione
ssh -i docs/credentials/hetzner_key root@135.181.149.254
mysqldump nis2_agile_db assets incidents > /root/backup_pre_v170_$(date +%F).sql
# 1) Deploy codice (bind mount: PHP live; verificare path reale di produzione)
cd /var/www/nis2-agile && git pull origin main # dopo push su Gitea
# 2) Migrazioni DB (additive, idempotenti). NB: usare host MySQL, non docker exec nis2-db
mysql -h localhost nis2_agile_db -e "source /var/www/nis2-agile/docs/sql/020_asset_relevance.sql"
mysql -h localhost nis2_agile_db -e "source /var/www/nis2-agile/docs/sql/021_incident_nis2_taxonomy.sql"
mysql -h localhost nis2_agile_db -e "source /var/www/nis2-agile/docs/sql/022_incident_metrics_pir.sql"
# 3) Ingest fonti normative nella KB (richiede Qdrant + Voyage attivi)
docker exec -i nis2-app php /var/www/nis2-agile/scripts/ingest-nis2-sources.php --dry-run # verifica
docker exec -i nis2-app php /var/www/nis2-agile/scripts/ingest-nis2-sources.php # esegui
# 4) Smoke test
curl -s https://nis2.agile.software/api/assets/scoringGrid -H "Authorization: Bearer <jwt>" | head
```
### Rollback
Ogni `.sql` contiene la sezione ROLLBACK in coda. Per la KB: cancellare i chunk scope=SYSTEM/source=normativa o ripristinare la collection.
---
## Note
- **Phase 4** è volutamente reference-only (nessuna tabella/migrazione) per non toccare l'assessment Art.21 consolidato.
- I PDF normativi restano in `docs/nis2/*.pdf` come libreria sorgente referenziata da `nis2_sources.php`.
- I file `*copy.html` e `incidente_r00/` dei mockup non sono stati usati (duplicati/superati).

2015
docs/nis2/assets.html Normal file

File diff suppressed because it is too large Load Diff

1481
docs/nis2/dashboard.html Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,450 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Diagramma Topologia Rete - NIS2</title>
<style>
:root {
--bg-primary: #ffffff;
--text-primary: #1a1a1a;
--text-secondary: #666666;
--border-color: #cccccc;
--accent-primary: #0066cc;
--zone-dmz: #fff3cd;
--zone-internal: #d1ecf1;
--zone-secure: #d4edda;
--zone-external: #f8d7da;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background-color: #f5f5f5;
color: var(--text-primary);
padding: 20px;
}
.document-container {
max-width: 297mm;
margin: 0 auto;
background-color: var(--bg-primary);
padding: 30px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
}
.header {
text-align: center;
border-bottom: 3px solid var(--text-primary);
padding-bottom: 20px;
margin-bottom: 30px;
}
.logo {
font-size: 24px;
font-weight: bold;
color: var(--accent-primary);
margin-bottom: 10px;
}
.doc-title {
font-size: 20px;
font-weight: bold;
margin: 15px 0;
}
.doc-subtitle {
font-size: 12px;
color: var(--text-secondary);
}
.classification {
text-align: center;
font-size: 11px;
font-weight: bold;
color: #d32f2f;
background-color: #ffebee;
padding: 10px;
border: 2px solid #d32f2f;
margin-bottom: 20px;
}
.diagram-container {
width: 100%;
background-color: #fafafa;
border: 2px solid var(--border-color);
padding: 30px;
margin: 20px 0;
position: relative;
}
.zone {
border: 2px dashed #333;
padding: 20px;
margin: 15px 0;
border-radius: 8px;
position: relative;
}
.zone-label {
position: absolute;
top: -12px;
left: 20px;
background-color: var(--bg-primary);
padding: 0 10px;
font-weight: bold;
font-size: 14px;
}
.zone-external {
background-color: var(--zone-external);
border-color: #721c24;
}
.zone-dmz {
background-color: var(--zone-dmz);
border-color: #856404;
}
.zone-internal {
background-color: var(--zone-internal);
border-color: #004085;
}
.zone-secure {
background-color: var(--zone-secure);
border-color: #155724;
}
.device {
background-color: white;
border: 2px solid #333;
border-radius: 6px;
padding: 15px;
margin: 10px;
display: inline-block;
min-width: 150px;
text-align: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.device-icon {
font-size: 32px;
margin-bottom: 8px;
}
.device-name {
font-weight: bold;
font-size: 12px;
margin-bottom: 4px;
}
.device-ip {
font-size: 10px;
color: var(--text-secondary);
}
.device-code {
font-size: 9px;
color: var(--accent-primary);
font-family: monospace;
}
.connection {
text-align: center;
font-size: 24px;
color: #333;
margin: 10px 0;
}
.legend {
margin-top: 30px;
padding: 20px;
background-color: #f9f9f9;
border: 1px solid var(--border-color);
border-radius: 6px;
}
.legend-title {
font-weight: bold;
font-size: 14px;
margin-bottom: 15px;
color: var(--accent-primary);
}
.legend-item {
display: inline-block;
margin: 5px 15px 5px 0;
font-size: 11px;
}
.legend-color {
display: inline-block;
width: 20px;
height: 20px;
border: 1px solid #333;
margin-right: 8px;
vertical-align: middle;
}
.info-box {
background-color: #e7f3ff;
border-left: 4px solid var(--accent-primary);
padding: 15px;
margin: 20px 0;
font-size: 11px;
}
.info-box-title {
font-weight: bold;
margin-bottom: 8px;
}
.no-print {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
}
.btn-print {
padding: 12px 24px;
background-color: var(--accent-primary);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.btn-print:hover {
background-color: #0052a3;
}
.grid-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 15px;
justify-items: center;
}
@media print {
body {
background-color: white;
padding: 0;
}
.document-container {
box-shadow: none;
}
.no-print {
display: none;
}
}
</style>
</head>
<body>
<div class="no-print">
<button class="btn-print" onclick="window.print()">🖨️ Stampa PDF</button>
</div>
<div class="document-container">
<div class="classification">
⚠️ DOCUMENTO RISERVATO - SOLO PERSONALE AUTORIZZATO ⚠️
</div>
<div class="header">
<div class="logo">ACME CORPORATION S.p.A.</div>
<div class="doc-title">Diagramma Topologia Rete Fisica</div>
<div class="doc-subtitle">Versione 3.2 | Data: 15 Febbraio 2024 | Approvato da: CISO</div>
<div class="doc-subtitle">Documento ID.AM-03 - Requisito NIS2 (Solo Soggetti Essenziali)</div>
</div>
<div class="info-box">
<div class="info-box-title">📋 Informazioni Documento</div>
<strong>Codice:</strong> NET-TOPO-PHY-v3.2 |
<strong>Ultima Modifica:</strong> 15/02/2024 |
<strong>Prossima Revisione:</strong> 15/05/2024 |
<strong>Classificazione:</strong> RISERVATO
</div>
<div class="diagram-container">
<!-- ZONA INTERNET -->
<div class="zone zone-external">
<div class="zone-label">🌐 INTERNET / ZONA ESTERNA</div>
<div style="text-align: center;">
<div class="device">
<div class="device-icon">🌍</div>
<div class="device-name">Internet</div>
<div class="device-ip">Pubblico</div>
</div>
</div>
</div>
<div class="connection">⬇️ Connessione Fibra 1Gbps</div>
<!-- ZONA DMZ -->
<div class="zone zone-dmz">
<div class="zone-label">🛡️ DMZ (DeMilitarized Zone)</div>
<div class="grid-container">
<div class="device">
<div class="device-icon">🔥</div>
<div class="device-name">Firewall Perimetrale</div>
<div class="device-ip">10.10.0.1</div>
<div class="device-code">HW-NET-015</div>
</div>
<div class="device">
<div class="device-icon">🌐</div>
<div class="device-name">Web Server Pubblico</div>
<div class="device-ip">10.10.2.45</div>
<div class="device-code">HW-SRV-023</div>
</div>
<div class="device">
<div class="device-icon">📧</div>
<div class="device-name">Mail Gateway</div>
<div class="device-ip">10.10.2.50</div>
<div class="device-code">HW-SRV-028</div>
</div>
<div class="device">
<div class="device-icon">🔐</div>
<div class="device-name">VPN Gateway</div>
<div class="device-ip">10.10.2.60</div>
<div class="device-code">HW-NET-018</div>
</div>
</div>
</div>
<div class="connection">⬇️ VLAN Segmentation</div>
<!-- ZONA INTERNA -->
<div class="zone zone-internal">
<div class="zone-label">🏢 RETE INTERNA</div>
<div class="grid-container">
<div class="device">
<div class="device-icon">🔀</div>
<div class="device-name">Core Switch</div>
<div class="device-ip">10.10.1.1</div>
<div class="device-code">HW-NET-022</div>
</div>
<div class="device">
<div class="device-icon">💻</div>
<div class="device-name">Workstation (x150)</div>
<div class="device-ip">10.20.x.x</div>
<div class="device-code">VLAN 20</div>
</div>
<div class="device">
<div class="device-icon">🖨️</div>
<div class="device-name">Stampanti</div>
<div class="device-ip">10.30.x.x</div>
<div class="device-code">VLAN 30</div>
</div>
<div class="device">
<div class="device-icon">📱</div>
<div class="device-name">WiFi Guest</div>
<div class="device-ip">10.40.x.x</div>
<div class="device-code">VLAN 40</div>
</div>
</div>
</div>
<div class="connection">⬇️ Firewall Interno</div>
<!-- ZONA SICURA -->
<div class="zone zone-secure">
<div class="zone-label">🔒 DATACENTER / ZONA SICURA</div>
<div class="grid-container">
<div class="device">
<div class="device-icon">🖥️</div>
<div class="device-name">ERP Server</div>
<div class="device-ip">10.10.1.10</div>
<div class="device-code">HW-SRV-001</div>
</div>
<div class="device">
<div class="device-icon">💾</div>
<div class="device-name">Database Server</div>
<div class="device-ip">10.10.1.20</div>
<div class="device-code">HW-SRV-012</div>
</div>
<div class="device">
<div class="device-icon">📊</div>
<div class="device-name">SIEM Splunk</div>
<div class="device-ip">10.10.1.30</div>
<div class="device-code">HW-SRV-045</div>
</div>
<div class="device">
<div class="device-icon">💿</div>
<div class="device-name">Backup Server</div>
<div class="device-ip">10.10.1.40</div>
<div class="device-code">HW-SRV-050</div>
</div>
<div class="device">
<div class="device-icon">☁️</div>
<div class="device-name">VMware Cluster</div>
<div class="device-ip">10.10.1.50-55</div>
<div class="device-code">HW-SRV-060</div>
</div>
<div class="device">
<div class="device-icon">🔐</div>
<div class="device-name">Active Directory</div>
<div class="device-ip">10.10.1.60</div>
<div class="device-code">HW-SRV-065</div>
</div>
</div>
</div>
</div>
<div class="legend">
<div class="legend-title">📖 LEGENDA</div>
<div class="legend-item">
<span class="legend-color" style="background-color: var(--zone-external);"></span>
Zona Esterna (Internet)
</div>
<div class="legend-item">
<span class="legend-color" style="background-color: var(--zone-dmz);"></span>
DMZ (Servizi Esposti)
</div>
<div class="legend-item">
<span class="legend-color" style="background-color: var(--zone-internal);"></span>
Rete Interna (Utenti)
</div>
<div class="legend-item">
<span class="legend-color" style="background-color: var(--zone-secure);"></span>
Datacenter (Sistemi Critici)
</div>
</div>
<div class="info-box">
<div class="info-box-title">🔐 Note di Sicurezza</div>
• Tutti i flussi tra zone sono controllati da firewall con regole whitelist<br>
• Monitoraggio 24/7 tramite SIEM centralizzato (Splunk)<br>
• Segmentazione VLAN per separazione logica dei servizi<br>
• Backup giornaliero con replica off-site<br>
• Accesso datacenter con autenticazione biometrica e logging<br>
• Aggiornamento obbligatorio entro 5 giorni da modifiche infrastrutturali
</div>
<div style="margin-top: 40px; padding-top: 20px; border-top: 2px solid var(--border-color); text-align: center; font-size: 10px; color: var(--text-secondary);">
Documento NET-TOPO-PHY-v3.2 - RISERVATO - Pagina 1 di 1
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,482 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documento Formale - Sistemi Rilevanti NIS2</title>
<style>
:root {
--bg-primary: #ffffff;
--text-primary: #1a1a1a;
--text-secondary: #666666;
--border-color: #cccccc;
--accent-primary: #0066cc;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Times New Roman', Times, serif;
background-color: #f5f5f5;
color: var(--text-primary);
line-height: 1.6;
padding: 20px;
}
.document-container {
max-width: 210mm;
margin: 0 auto;
background-color: var(--bg-primary);
padding: 40mm 25mm;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
min-height: 297mm;
}
.header {
text-align: center;
border-bottom: 3px solid var(--text-primary);
padding-bottom: 20px;
margin-bottom: 30px;
}
.logo {
font-size: 24px;
font-weight: bold;
color: var(--accent-primary);
margin-bottom: 10px;
}
.doc-title {
font-size: 20px;
font-weight: bold;
margin: 20px 0 10px 0;
text-transform: uppercase;
}
.doc-subtitle {
font-size: 14px;
color: var(--text-secondary);
margin-bottom: 5px;
}
.doc-info {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin: 30px 0;
padding: 15px;
background-color: #f9f9f9;
border-left: 4px solid var(--accent-primary);
}
.doc-info-item {
font-size: 12px;
}
.doc-info-label {
font-weight: bold;
color: var(--text-secondary);
}
.section {
margin: 30px 0;
}
.section-title {
font-size: 16px;
font-weight: bold;
color: var(--accent-primary);
border-bottom: 2px solid var(--border-color);
padding-bottom: 5px;
margin-bottom: 15px;
}
.section-content {
font-size: 12px;
text-align: justify;
margin-bottom: 15px;
}
table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
font-size: 11px;
}
th {
background-color: #e8e8e8;
padding: 10px 8px;
text-align: left;
border: 1px solid var(--border-color);
font-weight: bold;
font-size: 10px;
}
td {
padding: 8px;
border: 1px solid var(--border-color);
}
tr:nth-child(even) {
background-color: #f9f9f9;
}
.critical {
color: #d32f2f;
font-weight: bold;
}
.high {
color: #f57c00;
font-weight: bold;
}
.medium {
color: #388e3c;
font-weight: bold;
}
.footer {
margin-top: 50px;
padding-top: 20px;
border-top: 2px solid var(--border-color);
}
.signature-block {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 40px;
margin-top: 40px;
}
.signature {
text-align: center;
}
.signature-line {
border-top: 1px solid var(--text-primary);
margin-top: 60px;
padding-top: 10px;
font-size: 11px;
}
.signature-role {
font-size: 10px;
color: var(--text-secondary);
}
.page-number {
text-align: center;
font-size: 10px;
color: var(--text-secondary);
margin-top: 30px;
}
.classification {
text-align: center;
font-size: 11px;
font-weight: bold;
color: #d32f2f;
background-color: #ffebee;
padding: 10px;
border: 2px solid #d32f2f;
margin-bottom: 20px;
}
@media print {
body {
background-color: white;
padding: 0;
}
.document-container {
box-shadow: none;
padding: 20mm 15mm;
}
}
.no-print {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
}
.btn-print {
padding: 12px 24px;
background-color: var(--accent-primary);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.btn-print:hover {
background-color: #0052a3;
}
</style>
</head>
<body>
<div class="no-print">
<button class="btn-print" onclick="window.print()">🖨️ Stampa PDF</button>
</div>
<div class="document-container">
<div class="classification">
⚠️ DOCUMENTO RISERVATO - DISTRIBUZIONE LIMITATA ⚠️
</div>
<div class="header">
<div class="logo">ACME CORPORATION S.p.A.</div>
<div class="doc-title">Elenco Sistemi Rilevanti NIS2</div>
<div class="doc-subtitle">Documento Formale ai sensi della Direttiva (UE) 2022/2555</div>
<div class="doc-subtitle">Requisito GV.OC-04</div>
</div>
<div class="doc-info">
<div class="doc-info-item">
<span class="doc-info-label">Codice Documento:</span> NIS2-GV-OC-04-v2.3
</div>
<div class="doc-info-item">
<span class="doc-info-label">Data Emissione:</span> 15 Febbraio 2024
</div>
<div class="doc-info-item">
<span class="doc-info-label">Versione:</span> 2.3
</div>
<div class="doc-info-item">
<span class="doc-info-label">Prossima Revisione:</span> 15 Agosto 2024
</div>
<div class="doc-info-item">
<span class="doc-info-label">Redatto da:</span> CISO - Marco Bianchi
</div>
<div class="doc-info-item">
<span class="doc-info-label">Approvato da:</span> CdA - Delibera 05/2024
</div>
</div>
<div class="section">
<div class="section-title">1. PREMESSA</div>
<div class="section-content">
Il presente documento costituisce l'elenco formale dei sistemi classificati come rilevanti ai fini della conformità alla Direttiva NIS2 (UE) 2022/2555 e al relativo decreto di recepimento nazionale. La classificazione è stata effettuata secondo la metodologia di scoring approvata dal Consiglio di Amministrazione in data 10 Gennaio 2024, basata su sei criteri di valutazione con punteggio massimo di 100 punti.
</div>
<div class="section-content">
<strong>Soglia di Rilevanza:</strong> Sono considerati rilevanti tutti i sistemi con punteggio ≥ 40 punti.<br>
<strong>Sistemi Critici:</strong> Sistemi con punteggio ≥ 80 punti richiedono misure di sicurezza massime e monitoraggio continuo 24/7.
</div>
</div>
<div class="section">
<div class="section-title">2. RIEPILOGO STATISTICO</div>
<div class="section-content">
<table>
<tr>
<th>Categoria</th>
<th>Range Punteggio</th>
<th>Numero Sistemi</th>
<th>Percentuale</th>
</tr>
<tr>
<td><span class="critical">CRITICO</span></td>
<td>80-100</td>
<td>12</td>
<td>31.6%</td>
</tr>
<tr>
<td><span class="high">ALTO</span></td>
<td>60-79</td>
<td>15</td>
<td>39.5%</td>
</tr>
<tr>
<td><span class="medium">MEDIO</span></td>
<td>40-59</td>
<td>11</td>
<td>28.9%</td>
</tr>
<tr>
<td><strong>TOTALE RILEVANTI</strong></td>
<td>≥40</td>
<td><strong>38</strong></td>
<td><strong>100%</strong></td>
</tr>
</table>
</div>
</div>
<div class="section">
<div class="section-title">3. ELENCO SISTEMI CRITICI (≥80 PUNTI)</div>
<div class="section-content">
<table>
<tr>
<th>Codice</th>
<th>Nome Sistema</th>
<th>Tipo</th>
<th>Punteggio</th>
<th>RTO/RPO</th>
<th>Monitoraggio</th>
</tr>
<tr>
<td>SW-ERP-001</td>
<td>SAP ERP</td>
<td>Software</td>
<td class="critical">95</td>
<td>4h / 1h</td>
<td>24/7</td>
</tr>
<tr>
<td>SVC-001</td>
<td>Piattaforma ERP Cloud</td>
<td>Servizio</td>
<td class="critical">94</td>
<td>4h / 1h</td>
<td>24/7</td>
</tr>
<tr>
<td>SW-SEC-008</td>
<td>Splunk SIEM</td>
<td>Software</td>
<td class="critical">92</td>
<td>4h / 4h</td>
<td>24/7</td>
</tr>
<tr>
<td>HW-SRV-001</td>
<td>ERP-PROD-01</td>
<td>Hardware</td>
<td class="critical">91</td>
<td>4h / 1h</td>
<td>24/7</td>
</tr>
<tr>
<td>SW-DB-003</td>
<td>Oracle Database</td>
<td>Software</td>
<td class="critical">89</td>
<td>4h / 1h</td>
<td>24/7</td>
</tr>
<tr>
<td>HW-NET-015</td>
<td>FW-PERIMETRALE-01</td>
<td>Hardware</td>
<td class="critical">88</td>
<td>1h / N/A</td>
<td>24/7</td>
</tr>
<tr>
<td>SVC-002</td>
<td>Hosting Applicazioni</td>
<td>Servizio</td>
<td class="critical">88</td>
<td>8h / 4h</td>
<td>24/7</td>
</tr>
<tr>
<td>CLD-IAAS-001</td>
<td>VM Production AWS</td>
<td>Cloud</td>
<td class="critical">87</td>
<td>4h / 1h</td>
<td>24/7</td>
</tr>
<tr>
<td>CLD-PAAS-003</td>
<td>Azure SQL Database</td>
<td>Cloud</td>
<td class="critical">85</td>
<td>4h / 1h</td>
<td>24/7</td>
</tr>
<tr>
<td>HW-NET-022</td>
<td>Core Switch Datacenter</td>
<td>Hardware</td>
<td class="critical">84</td>
<td>2h / N/A</td>
<td>24/7</td>
</tr>
<tr>
<td>SW-BACKUP-001</td>
<td>Veeam Backup System</td>
<td>Software</td>
<td class="critical">82</td>
<td>24h / 24h</td>
<td>24/7</td>
</tr>
<tr>
<td>HW-SRV-012</td>
<td>DB-PROD-01</td>
<td>Hardware</td>
<td class="critical">80</td>
<td>4h / 1h</td>
<td>24/7</td>
</tr>
</table>
</div>
</div>
<div class="section">
<div class="section-title">4. MISURE DI SICUREZZA OBBLIGATORIE</div>
<div class="section-content">
Per tutti i sistemi rilevanti sono implementate le seguenti misure minime di sicurezza:
<ul style="margin-left: 20px; margin-top: 10px;">
<li>Backup giornaliero con retention minima 30 giorni</li>
<li>Patch management con SLA massimo 30 giorni per vulnerabilità critiche</li>
<li>Logging centralizzato su SIEM con retention 12 mesi</li>
<li>Controllo accessi con autenticazione multi-fattore (MFA)</li>
<li>Monitoraggio continuo con alerting automatico</li>
<li>Business Continuity Plan e Disaster Recovery Plan documentati e testati</li>
</ul>
</div>
<div class="section-content">
<strong>Sistemi Critici (≥80):</strong> Oltre alle misure sopra elencate, richiedono ridondanza hardware/software, monitoraggio 24/7 con reperibilità H24, test DR semestrali e revisione trimestrale delle configurazioni di sicurezza.
</div>
</div>
<div class="section">
<div class="section-title">5. REVISIONE E AGGIORNAMENTO</div>
<div class="section-content">
Il presente documento è soggetto a revisione semestrale obbligatoria. Revisioni straordinarie sono richieste in caso di:
<ul style="margin-left: 20px; margin-top: 10px;">
<li>Introduzione di nuovi sistemi con punteggio ≥40</li>
<li>Modifiche sostanziali ai sistemi esistenti che ne alterano il punteggio</li>
<li>Incidenti di sicurezza significativi</li>
<li>Cambiamenti normativi rilevanti</li>
</ul>
</div>
</div>
<div class="footer">
<div class="section-title">APPROVAZIONE</div>
<div class="section-content">
Il presente documento è stato approvato dal Consiglio di Amministrazione in data 15 Febbraio 2024 con Delibera n. 05/2024 e costituisce documento ufficiale ai fini della conformità NIS2.
</div>
<div class="signature-block">
<div class="signature">
<div class="signature-line">
<strong>Marco Bianchi</strong><br>
<span class="signature-role">Chief Information Security Officer (CISO)</span>
</div>
</div>
<div class="signature">
<div class="signature-line">
<strong>Giovanni Rossi</strong><br>
<span class="signature-role">Amministratore Delegato</span>
</div>
</div>
</div>
</div>
<div class="page-number">
Pagina 1 di 1 - Documento NIS2-GV-OC-04-v2.3 - RISERVATO
</div>
</div>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,353 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Accesso Sistema Gestione Incidenti - NIS2</title>
<style>
:root {
--bg-primary: #0d1117;
--bg-secondary: #161b22;
--bg-tertiary: #1c2128;
--border-color: #30363d;
--text-primary: #c9d1d9;
--text-secondary: #8b949e;
--accent-primary: #58a6ff;
--accent-secondary: #1f6feb;
--success: #3fb950;
--warning: #d29922;
--danger: #f85149;
--essential-bg: #fef3c7;
--essential-text: #92400e;
--essential-border: #f59e0b;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
background-color: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 20px;
}
.gate-container {
max-width: 900px;
width: 100%;
}
.gate-header {
text-align: center;
margin-bottom: 48px;
}
.gate-header h1 {
font-size: 32px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 16px;
}
.gate-header p {
font-size: 16px;
color: var(--text-secondary);
line-height: 1.6;
}
.warning-box {
background-color: rgba(210, 153, 34, 0.1);
border: 2px solid var(--warning);
border-radius: 8px;
padding: 24px;
margin-bottom: 48px;
}
.warning-box-title {
display: flex;
align-items: center;
gap: 12px;
font-size: 18px;
font-weight: 600;
color: var(--warning);
margin-bottom: 16px;
}
.warning-icon {
font-size: 24px;
}
.warning-box-content {
font-size: 14px;
color: var(--text-primary);
line-height: 1.8;
}
.selection-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 24px;
margin-bottom: 32px;
}
.selection-card {
background-color: var(--bg-secondary);
border: 2px solid var(--border-color);
border-radius: 8px;
padding: 32px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.selection-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
transform: scaleX(0);
transition: transform 0.3s ease;
}
.selection-card:hover {
border-color: var(--accent-primary);
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(88, 166, 255, 0.2);
}
.selection-card:hover::before {
transform: scaleX(1);
}
.selection-card.essential {
border-color: var(--essential-border);
}
.selection-card.essential:hover {
border-color: var(--essential-border);
box-shadow: 0 8px 24px rgba(245, 158, 11, 0.2);
}
.selection-card.essential::before {
background: linear-gradient(90deg, var(--essential-border), var(--warning));
}
.card-badge {
display: inline-block;
padding: 6px 12px;
border-radius: 4px;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 16px;
}
.badge-important {
background-color: rgba(88, 166, 255, 0.2);
color: var(--accent-primary);
border: 1px solid var(--accent-primary);
}
.badge-essential {
background-color: var(--essential-bg);
color: var(--essential-text);
border: 1px solid var(--essential-border);
}
.card-title {
font-size: 24px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 16px;
}
.card-description {
font-size: 14px;
color: var(--text-secondary);
line-height: 1.6;
margin-bottom: 24px;
}
.card-features {
list-style: none;
margin-bottom: 24px;
}
.card-features li {
font-size: 13px;
color: var(--text-primary);
padding: 8px 0;
padding-left: 24px;
position: relative;
}
.card-features li::before {
content: '✓';
position: absolute;
left: 0;
color: var(--success);
font-weight: 700;
}
.card-button {
width: 100%;
padding: 14px 24px;
background-color: var(--bg-tertiary);
border: 2px solid var(--accent-primary);
border-radius: 6px;
color: var(--accent-primary);
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.card-button:hover {
background-color: var(--accent-primary);
color: var(--bg-primary);
}
.selection-card.essential .card-button {
border-color: var(--essential-border);
color: var(--essential-border);
}
.selection-card.essential .card-button:hover {
background-color: var(--essential-border);
color: var(--bg-primary);
}
.footer-note {
text-align: center;
font-size: 13px;
color: var(--text-secondary);
padding: 24px;
background-color: var(--bg-secondary);
border-radius: 6px;
border: 1px solid var(--border-color);
}
.footer-note strong {
color: var(--text-primary);
}
@media (max-width: 768px) {
.selection-grid {
grid-template-columns: 1fr;
}
.gate-header h1 {
font-size: 24px;
}
}
</style>
</head>
<body>
<div class="gate-container">
<div class="gate-header">
<h1>Sistema Gestione Incidenti NIS2</h1>
<p>Seleziona la tipologia del tuo soggetto per accedere alle funzionalità appropriate</p>
</div>
<div class="warning-box">
<div class="warning-box-title">
<span class="warning-icon">⚠️</span>
Accesso Obbligatorio
</div>
<div class="warning-box-content">
<strong>La conformità alla gestione incidenti è obbligatoria per TUTTI i soggetti NIS2</strong> (essenziali e importanti). La differenza sta nei tipi di incidenti da segnalare e nelle tempistiche, come definito negli Allegati 3 (soggetti essenziali) e 4 (soggetti importanti) della Determina 164179/2025.
<br><br>
Seleziona la categoria corretta per visualizzare solo i requisiti applicabili alla tua organizzazione.
</div>
</div>
<div class="selection-grid">
<div class="selection-card" onclick="selectType('important')">
<span class="card-badge badge-important">Soggetto Importante</span>
<h2 class="card-title">Soggetto Importante</h2>
<p class="card-description">
Organizzazioni che rientrano nella categoria "importanti" secondo il D.Lgs. 138/2024, con obblighi di notifica per incidenti significativi secondo Allegato 4.
</p>
<ul class="card-features">
<li>Notifica incidenti IS-1, IS-2, IS-3</li>
<li>Preallarme entro 24 ore</li>
<li>Notifica completa entro 72 ore</li>
<li>Relazione finale entro 1 mese</li>
<li>Gestione incidenti ricorrenti (IS-4) non obbligatoria</li>
</ul>
<button class="card-button">Accedi come Soggetto Importante</button>
</div>
<div class="selection-card essential" onclick="selectType('essential')">
<span class="card-badge badge-essential">Soggetto Essenziale</span>
<h2 class="card-title">Soggetto Essenziale</h2>
<p class="card-description">
Organizzazioni che rientrano nella categoria "essenziali" secondo il D.Lgs. 138/2024, con obblighi estesi di notifica per incidenti significativi secondo Allegato 3.
</p>
<ul class="card-features">
<li>Notifica incidenti IS-1, IS-2, IS-3, IS-4</li>
<li>Preallarme entro 24 ore</li>
<li>Notifica completa entro 72 ore</li>
<li>Relazione finale entro 1 mese</li>
<li>Gestione incidenti ricorrenti (IS-4) obbligatoria</li>
<li>Monitoraggio 24/7 richiesto</li>
</ul>
<button class="card-button">Accedi come Soggetto Essenziale</button>
</div>
</div>
<div class="footer-note">
<strong>Riferimenti normativi:</strong> D.Lgs. 138/2024 (Direttiva NIS2), Determina ACN 164179/2025 (Allegati 3 e 4)<br>
<strong>Requisiti NIS2 coperti:</strong> RS.MA-01 (Gestione Incidenti), RS.CO-02 (Notifiche), RC.RP-01 (Ripristino), RC.CO-03 (Comunicazioni)
</div>
</div>
<script>
function selectType(type) {
// Salva la selezione in sessionStorage
sessionStorage.setItem('nis2_subject_type', type);
// Reindirizza alla dashboard
window.location.href = 'incident-dashboard.html';
}
// Verifica se c'è già una selezione
window.addEventListener('DOMContentLoaded', function() {
const savedType = sessionStorage.getItem('nis2_subject_type');
if (savedType) {
// Mostra un messaggio che sta caricando
const container = document.querySelector('.gate-container');
container.innerHTML = `
<div style="text-align: center; padding: 60px 20px;">
<h2 style="font-size: 24px; margin-bottom: 16px;">Accesso già configurato</h2>
<p style="color: var(--text-secondary); margin-bottom: 24px;">
Sei configurato come: <strong style="color: var(--text-primary);">${savedType === 'essential' ? 'Soggetto Essenziale' : 'Soggetto Importante'}</strong>
</p>
<button onclick="window.location.href='incident-dashboard.html'"
style="padding: 12px 24px; background-color: var(--accent-primary); border: none; border-radius: 6px; color: white; font-weight: 600; cursor: pointer; margin-right: 12px;">
Vai alla Dashboard
</button>
<button onclick="sessionStorage.removeItem('nis2_subject_type'); location.reload();"
style="padding: 12px 24px; background-color: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 6px; color: var(--text-primary); font-weight: 600; cursor: pointer;">
Cambia Selezione
</button>
</div>
`;
}
});
</script>
</body>
</html>

View File

@ -0,0 +1,956 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nuovo Incidente - NIS2 Management System</title>
<style>
:root {
--bg-primary: #0d1117;
--bg-secondary: #161b22;
--bg-tertiary: #1c2128;
--border-color: #30363d;
--text-primary: #c9d1d9;
--text-secondary: #8b949e;
--accent-primary: #58a6ff;
--accent-secondary: #1f6feb;
--success: #3fb950;
--warning: #d29922;
--danger: #f85149;
--essential-bg: #fef3c7;
--essential-text: #92400e;
--essential-border: #f59e0b;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
background-color: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
overflow-y: auto;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.header {
background-color: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
padding: 24px 0;
margin-bottom: 32px;
position: sticky;
top: 0;
z-index: 100;
}
.header-content {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
font-size: 24px;
font-weight: 600;
color: var(--text-primary);
}
.breadcrumb {
font-size: 13px;
color: var(--text-secondary);
margin-top: 4px;
}
.breadcrumb a {
color: var(--accent-primary);
text-decoration: none;
}
.btn {
padding: 8px 16px;
background-color: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-primary);
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
text-decoration: none;
}
.btn:hover {
border-color: var(--accent-primary);
color: var(--accent-primary);
}
.form-section {
background-color: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 24px;
margin-bottom: 24px;
}
.form-section-title {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
gap: 8px;
}
.help-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
background-color: rgba(167, 139, 250, 0.2);
border: 2px solid #a78bfa;
border-radius: 50%;
font-size: 13px;
font-weight: 700;
color: #a78bfa;
cursor: help;
position: relative;
}
.help-icon:hover {
background-color: rgba(167, 139, 250, 0.3);
transform: scale(1.1);
}
.tooltip {
visibility: hidden;
position: absolute;
z-index: 1000;
background-color: var(--bg-tertiary);
color: var(--text-primary);
padding: 12px;
border-radius: 6px;
border: 1px solid var(--border-color);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
width: 320px;
top: 28px;
left: 50%;
transform: translateX(-50%);
font-size: 12px;
line-height: 1.5;
opacity: 0;
transition: opacity 0.2s;
white-space: normal;
}
.tooltip::before {
content: '';
position: absolute;
top: -6px;
left: 50%;
transform: translateX(-50%);
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid var(--border-color);
}
.tooltip-title {
color: var(--accent-primary);
font-weight: 600;
margin-bottom: 8px;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.help-icon:hover .tooltip {
visibility: visible;
opacity: 1;
}
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.form-group {
margin-bottom: 20px;
}
.form-group.full-width {
grid-column: 1 / -1;
}
.form-label {
display: block;
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 8px;
}
.form-label .required {
color: var(--danger);
margin-left: 4px;
}
.form-input, .form-select, .form-textarea {
width: 100%;
padding: 10px 12px;
background-color: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-primary);
font-size: 14px;
font-family: inherit;
}
.form-input:focus, .form-select:focus, .form-textarea:focus {
outline: none;
border-color: var(--accent-primary);
box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.1);
}
.form-textarea {
resize: vertical;
min-height: 100px;
}
.form-help {
font-size: 12px;
color: var(--text-secondary);
margin-top: 6px;
line-height: 1.4;
}
.severity-selector {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 12px;
}
.severity-option {
padding: 16px;
background-color: var(--bg-tertiary);
border: 2px solid var(--border-color);
border-radius: 6px;
cursor: pointer;
text-align: center;
transition: all 0.2s;
}
.severity-option:hover {
border-color: var(--accent-primary);
}
.severity-option.selected {
border-color: var(--accent-primary);
background-color: rgba(88, 166, 255, 0.1);
}
.severity-option.sev-1 {
border-color: var(--danger);
}
.severity-option.sev-1.selected {
background-color: rgba(248, 81, 73, 0.1);
}
.severity-option.sev-2 {
border-color: var(--warning);
}
.severity-option.sev-2.selected {
background-color: rgba(210, 153, 34, 0.1);
}
.severity-label {
font-weight: 700;
font-size: 14px;
margin-bottom: 4px;
}
.severity-desc {
font-size: 11px;
color: var(--text-secondary);
}
.alert-box {
background-color: rgba(88, 166, 255, 0.1);
border: 1px solid var(--accent-primary);
border-left: 4px solid var(--accent-primary);
border-radius: 6px;
padding: 16px;
margin-bottom: 20px;
}
.alert-box.warning {
background-color: rgba(210, 153, 34, 0.1);
border-color: var(--warning);
border-left-color: var(--warning);
}
.alert-box.danger {
background-color: rgba(248, 81, 73, 0.1);
border-color: var(--danger);
border-left-color: var(--danger);
}
.alert-title {
font-weight: 600;
margin-bottom: 8px;
font-size: 14px;
}
.alert-content {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.5;
}
.form-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
padding-top: 24px;
border-top: 1px solid var(--border-color);
}
.btn-primary {
background-color: var(--accent-primary);
border-color: var(--accent-primary);
color: white;
}
.btn-primary:hover {
background-color: var(--accent-secondary);
border-color: var(--accent-secondary);
color: white;
}
.btn-danger {
background-color: var(--danger);
border-color: var(--danger);
color: white;
}
.btn-danger:hover {
background-color: #dc2626;
border-color: #dc2626;
}
.checklist {
list-style: none;
}
.checklist li {
padding: 10px;
background-color: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 4px;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 12px;
}
.checklist input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
}
.essential-badge {
display: inline-block;
background-color: var(--essential-bg);
color: var(--essential-text);
padding: 4px 8px;
border-radius: 3px;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
border: 1px solid var(--essential-border);
margin-left: 8px;
}
@media (max-width: 768px) {
.form-grid {
grid-template-columns: 1fr;
}
.severity-selector {
grid-template-columns: 1fr 1fr;
}
}
</style>
</head>
<body>
<div class="header">
<div class="header-content">
<div>
<h1>🚨 Segnalazione Nuovo Incidente</h1>
<div class="breadcrumb">
<a href="dashboard.html">Dashboard NIS2</a> /
<a href="incident-dashboard.html">Gestione Incidenti</a> /
Nuovo Incidente
</div>
</div>
<a href="incident-dashboard.html" class="btn">← Annulla</a>
</div>
</div>
<div class="container">
<div class="alert-box danger">
<div class="alert-title">⚠️ Procedura di Emergenza</div>
<div class="alert-content">
Per incidenti <strong>SEV-1 (Critici)</strong>: attivare immediatamente il Crisis Team e il CISO.
La notifica al CSIRT Italia (preallarme) deve essere effettuata entro <strong>24 ore</strong> dalla conoscenza dell'incidente significativo.
</div>
</div>
<form id="incidentForm">
<!-- Sezione 1: Rilevazione -->
<div class="form-section">
<div class="form-section-title">
Fase 1: Rilevazione e Segnalazione
<span class="help-icon">?
<div class="tooltip">
<div class="tooltip-title">HELP DELLA SEZIONE</div>
Registrazione iniziale dell'incidente. Compilare entro 15 minuti dalla rilevazione per incidenti critici. Tutte le informazioni possono essere aggiornate durante la gestione.
</div>
</span>
</div>
<div class="form-grid">
<div class="form-group">
<label class="form-label">
Data/Ora Rilevazione <span class="required">*</span>
</label>
<input type="datetime-local" class="form-input" id="detectionTime" required>
<div class="form-help">Momento in cui l'incidente è stato rilevato</div>
</div>
<div class="form-group">
<label class="form-label">
Fonte di Rilevazione <span class="required">*</span>
</label>
<select class="form-select" id="detectionSource" required>
<option value="">Seleziona...</option>
<option value="siem">Alert SIEM/SOC</option>
<option value="edr">Alert EDR/Antimalware</option>
<option value="user">Segnalazione Utente</option>
<option value="supplier">Notifica Fornitore/Partner</option>
<option value="csirt">Notifica CSIRT Italia</option>
<option value="threat-intel">Threat Intelligence</option>
<option value="external">Segnalazione Esterna</option>
<option value="monitoring">Monitoraggio Infrastruttura</option>
</select>
</div>
<div class="form-group">
<label class="form-label">
Analista Assegnato <span class="required">*</span>
</label>
<select class="form-select" id="analyst" required>
<option value="">Seleziona...</option>
<option value="ciso">CISO</option>
<option value="soc-lead">SOC Lead</option>
<option value="analyst1">Analista Sicurezza 1</option>
<option value="analyst2">Analista Sicurezza 2</option>
<option value="sysadmin">System Administrator</option>
<option value="netadmin">Network Administrator</option>
</select>
</div>
<div class="form-group full-width">
<label class="form-label">
Descrizione Preliminare <span class="required">*</span>
</label>
<textarea class="form-textarea" id="description" required placeholder="Descrizione dettagliata dell'incidente rilevato..."></textarea>
<div class="form-help">Fornire tutti i dettagli disponibili: cosa è stato rilevato, su quali sistemi, sintomi osservati</div>
</div>
<div class="form-group full-width">
<label class="form-label">
Sistemi Potenzialmente Impattati
</label>
<input type="text" class="form-input" id="affectedSystems" placeholder="es. HW-SRV-001, SW-ERP-001, CLD-IAAS-001">
<div class="form-help">Codici asset da inventario (Org.01). Separare con virgola se multipli</div>
</div>
</div>
</div>
<!-- Sezione 2: Triage e Classificazione -->
<div class="form-section">
<div class="form-section-title">
Fase 2: Triage e Classificazione
<span class="help-icon">?
<div class="tooltip">
<div class="tooltip-title">HELP DELLA SEZIONE</div>
Determinazione della severità e classificazione secondo NIS2. Completare entro 1 ora per SEV-1/SEV-2. La classificazione determina gli obblighi di notifica al CSIRT.
</div>
</span>
</div>
<div class="form-group full-width">
<label class="form-label">
Severità Incidente <span class="required">*</span>
</label>
<div class="severity-selector">
<div class="severity-option sev-1" onclick="selectSeverity('sev-1')">
<div class="severity-label" style="color: var(--danger);">SEV-1</div>
<div class="severity-desc">CRITICO</div>
</div>
<div class="severity-option sev-2" onclick="selectSeverity('sev-2')">
<div class="severity-label" style="color: var(--warning);">SEV-2</div>
<div class="severity-desc">ALTO</div>
</div>
<div class="severity-option" onclick="selectSeverity('sev-3')">
<div class="severity-label" style="color: var(--accent-primary);">SEV-3</div>
<div class="severity-desc">MEDIO</div>
</div>
<div class="severity-option" onclick="selectSeverity('sev-4')">
<div class="severity-label" style="color: var(--text-secondary);">SEV-4</div>
<div class="severity-desc">BASSO</div>
</div>
</div>
<input type="hidden" id="severity" required>
<div class="form-help" id="severityHelp" style="margin-top: 12px;"></div>
</div>
<div class="alert-box warning" id="sev1Alert" style="display: none;">
<div class="alert-title">🚨 Incidente SEV-1 - Azioni Immediate Richieste</div>
<div class="alert-content">
<ul style="margin-left: 20px; margin-top: 8px;">
<li>Attivare immediatamente Crisis Team</li>
<li>Notificare CISO e Direzione</li>
<li>Contenimento entro 30 minuti</li>
<li>Preallarme CSIRT entro 24h (se significativo)</li>
</ul>
</div>
</div>
<div class="form-grid">
<div class="form-group">
<label class="form-label">
Classificazione NIS2 <span class="required">*</span>
</label>
<select class="form-select" id="classification" required onchange="updateClassificationHelp()">
<option value="">Seleziona...</option>
<option value="is-1">IS-1 - Impatto su servizi rilevanti</option>
<option value="is-2">IS-2 - Impatto su integrità/riservatezza dati</option>
<option value="is-3">IS-3 - Impatto su altri soggetti/supply chain</option>
<option value="is-4" id="is4ClassOption">IS-4 - Incidente ricorrente (SOLO ESSENZIALI)</option>
<option value="not-significant">Non Significativo</option>
</select>
<div class="form-help" id="classificationHelp"></div>
</div>
<div class="form-group">
<label class="form-label">
Impatto
</label>
<select class="form-select" id="impact" multiple size="4">
<option value="availability">Disponibilità</option>
<option value="integrity">Integrità</option>
<option value="confidentiality">Riservatezza</option>
<option value="authenticity">Autenticità</option>
</select>
<div class="form-help">Tenere premuto Ctrl/Cmd per selezione multipla</div>
</div>
<div class="form-group">
<label class="form-label">
Categoria MITRE ATT&CK
</label>
<select class="form-select" id="mitreCategory">
<option value="">Non determinata</option>
<option value="initial-access">Initial Access</option>
<option value="execution">Execution</option>
<option value="persistence">Persistence</option>
<option value="privilege-escalation">Privilege Escalation</option>
<option value="defense-evasion">Defense Evasion</option>
<option value="credential-access">Credential Access</option>
<option value="discovery">Discovery</option>
<option value="lateral-movement">Lateral Movement</option>
<option value="collection">Collection</option>
<option value="exfiltration">Exfiltration</option>
<option value="impact">Impact</option>
</select>
</div>
<div class="form-group">
<label class="form-label">
Vettore di Attacco
</label>
<select class="form-select" id="attackVector">
<option value="">Non determinato</option>
<option value="phishing">Phishing/Spear Phishing</option>
<option value="malware">Malware</option>
<option value="ransomware">Ransomware</option>
<option value="vulnerability">Exploit Vulnerabilità</option>
<option value="brute-force">Brute Force</option>
<option value="credential-stuffing">Credential Stuffing</option>
<option value="supply-chain">Supply Chain Attack</option>
<option value="ddos">DDoS</option>
<option value="insider">Insider Threat</option>
<option value="physical">Accesso Fisico</option>
<option value="social-engineering">Social Engineering</option>
</select>
</div>
</div>
<div class="alert-box" id="csirtAlert" style="display: none;">
<div class="alert-title">📡 Notifica CSIRT Obbligatoria</div>
<div class="alert-content">
Questo incidente rientra nella classificazione di incidente significativo secondo NIS2.
È obbligatorio inviare il <strong>preallarme al CSIRT Italia entro 24 ore</strong> dalla conoscenza dell'incidente.
<br><br>
Dopo la creazione dell'incidente, procedere alla sezione "Notifiche CSIRT" per gestire le comunicazioni obbligatorie.
</div>
</div>
</div>
<!-- Sezione 3: Dati Impattati -->
<div class="form-section">
<div class="form-section-title">
Dati e Servizi Impattati
<span class="help-icon">?
<div class="tooltip">
<div class="tooltip-title">HELP DELLA SEZIONE</div>
Identificazione dei dati e servizi coinvolti nell'incidente. Fondamentale per valutare obblighi GDPR e impatto operativo.
</div>
</span>
</div>
<div class="form-grid">
<div class="form-group">
<label class="form-label">
Dati Personali Coinvolti?
</label>
<select class="form-select" id="personalData" onchange="toggleGDPR()">
<option value="no">No</option>
<option value="yes"></option>
<option value="unknown">Da verificare</option>
</select>
</div>
<div class="form-group" id="gdprGroup" style="display: none;">
<label class="form-label">
Notifica Garante Privacy
</label>
<select class="form-select" id="gdprNotification">
<option value="not-required">Non richiesta</option>
<option value="required">Richiesta (entro 72h)</option>
<option value="sent">Già inviata</option>
</select>
<div class="form-help">Art. 33 GDPR - coordinare con DPO</div>
</div>
<div class="form-group full-width">
<label class="form-label">
Servizi Rilevanti NIS2 Impattati
</label>
<input type="text" class="form-input" id="affectedServices" placeholder="Codici servizi da catalogo (ID.AM-04)">
<div class="form-help">Elencare i servizi rilevanti NIS2 che hanno subito interruzione o degrado</div>
</div>
<div class="form-group">
<label class="form-label">
Numero Utenti Impattati (stima)
</label>
<input type="number" class="form-input" id="usersImpacted" min="0">
</div>
<div class="form-group">
<label class="form-label">
Impatto Economico Stimato (€)
</label>
<input type="number" class="form-input" id="financialImpact" min="0" step="100">
<div class="form-help">Stima preliminare (diretto + indiretto)</div>
</div>
</div>
</div>
<!-- Sezione 4: Azioni Immediate -->
<div class="form-section">
<div class="form-section-title">
Azioni Immediate di Contenimento
<span class="help-icon">?
<div class="tooltip">
<div class="tooltip-title">HELP DELLA SEZIONE</div>
Checklist delle azioni di contenimento immediato. Per SEV-1: contenimento entro 30 minuti. Documentare ogni azione con timestamp.
</div>
</span>
</div>
<ul class="checklist">
<li>
<input type="checkbox" id="action1">
<label for="action1">Sistema compromesso isolato dalla rete</label>
</li>
<li>
<input type="checkbox" id="action2">
<label for="action2">Account compromessi bloccati</label>
</li>
<li>
<input type="checkbox" id="action3">
<label for="action3">IP/domini malevoli bloccati su firewall</label>
</li>
<li>
<input type="checkbox" id="action4">
<label for="action4">Evidenze preservate (snapshot, log, memoria)</label>
</li>
<li>
<input type="checkbox" id="action5">
<label for="action5">Monitoraggio intensivo attivato su sistemi correlati</label>
</li>
<li>
<input type="checkbox" id="action6">
<label for="action6">Crisis Team / IRT notificato</label>
</li>
<li>
<input type="checkbox" id="action7">
<label for="action7">Direzione informata (per SEV-1/SEV-2)</label>
</li>
</ul>
<div class="form-group" style="margin-top: 20px;">
<label class="form-label">
Note Azioni di Contenimento
</label>
<textarea class="form-textarea" id="containmentNotes" placeholder="Descrivere in dettaglio le azioni di contenimento effettuate con timestamp..."></textarea>
</div>
</div>
<!-- Sezione 5: Collegamento Risk -->
<div class="form-section">
<div class="form-section-title">
Collegamento Risk Assessment
<span class="help-icon">?
<div class="tooltip">
<div class="tooltip-title">HELP DELLA SEZIONE</div>
Collegare l'incidente a rischi già identificati nel risk assessment (Org.05). Se l'incidente evidenzia un nuovo rischio, verrà creato automaticamente.
</div>
</span>
</div>
<div class="form-group">
<label class="form-label">
Collegamento N°RISK
</label>
<input type="text" class="form-input" id="riskId" placeholder="es. RISK-2024-015">
<div class="form-help">Codice rischio da Org.05 - Risk Assessment. Lasciare vuoto se nuovo rischio</div>
</div>
<div class="form-group">
<label class="form-label">
Root Cause Preliminare
</label>
<select class="form-select" id="rootCause">
<option value="">Da determinare</option>
<option value="vulnerability">Vulnerabilità non patchata</option>
<option value="misconfiguration">Errore di configurazione</option>
<option value="weak-credentials">Credenziali deboli/compromesse</option>
<option value="missing-controls">Controlli di sicurezza mancanti</option>
<option value="human-error">Errore umano</option>
<option value="third-party">Compromissione terza parte</option>
<option value="zero-day">Zero-day exploit</option>
<option value="insider">Insider threat</option>
</select>
</div>
</div>
<!-- Form Actions -->
<div class="form-section">
<div class="form-actions">
<a href="incident-dashboard.html" class="btn">Annulla</a>
<button type="button" class="btn" onclick="saveDraft()">Salva Bozza</button>
<button type="submit" class="btn btn-danger">🚨 Crea Incidente e Attiva Gestione</button>
</div>
</div>
</form>
</div>
<script>
// Verifica tipo soggetto
function checkSubjectType() {
const subjectType = sessionStorage.getItem('nis2_subject_type');
if (!subjectType) {
window.location.href = 'incident-gate.html';
return;
}
// Nascondi IS-4 per soggetti importanti
if (subjectType === 'important') {
const is4Option = document.getElementById('is4ClassOption');
if (is4Option) is4Option.style.display = 'none';
}
}
// Imposta data/ora corrente
function setCurrentDateTime() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
document.getElementById('detectionTime').value = `${year}-${month}-${day}T${hours}:${minutes}`;
}
// Selezione severità
function selectSeverity(severity) {
// Rimuovi selezione precedente
document.querySelectorAll('.severity-option').forEach(opt => {
opt.classList.remove('selected');
});
// Seleziona nuovo
event.target.closest('.severity-option').classList.add('selected');
document.getElementById('severity').value = severity;
// Mostra help specifico
const helpTexts = {
'sev-1': '🚨 <strong>CRITICO:</strong> Interruzione totale servizio critico, ransomware attivo, esfiltrazione massiva dati, compromissione infrastruttura core. <strong>Attivazione immediata Crisis Team.</strong>',
'sev-2': '⚠️ <strong>ALTO:</strong> Interruzione parziale servizio rilevante, compromissione account privilegiato, malware attivo non contenuto, accesso non autorizzato a dati riservati.',
'sev-3': ' <strong>MEDIO:</strong> Degrado prestazioni servizio rilevante, malware contenuto automaticamente, tentativo intrusione bloccato ma significativo.',
'sev-4': '📝 <strong>BASSO:</strong> Anomalia senza impatto operativo, tentativo attacco bloccato (routine), violazione policy minore.'
};
document.getElementById('severityHelp').innerHTML = helpTexts[severity];
// Mostra alert per SEV-1
const sev1Alert = document.getElementById('sev1Alert');
if (severity === 'sev-1') {
sev1Alert.style.display = 'block';
} else {
sev1Alert.style.display = 'none';
}
}
// Aggiorna help classificazione
function updateClassificationHelp() {
const classification = document.getElementById('classification').value;
const helpTexts = {
'is-1': 'Interruzione operativa o degrado significativo di un servizio rilevante NIS2. <strong>Notifica CSIRT obbligatoria.</strong>',
'is-2': 'Accesso non autorizzato, modifica, esfiltrazione o violazione riservatezza dati. <strong>Notifica CSIRT obbligatoria.</strong> Verificare anche obbligo GDPR.',
'is-3': 'Incidente che si propaga a soggetti terzi o impatta la catena di fornitura. <strong>Notifica CSIRT obbligatoria.</strong>',
'is-4': 'Serie di incidenti correlati con impatto cumulativo significativo. <strong>Notifica CSIRT obbligatoria SOLO per soggetti ESSENZIALI.</strong>',
'not-significant': 'Incidente gestito internamente, senza obbligo di notifica CSIRT. Documentare comunque nel registro incidenti.'
};
const helpElement = document.getElementById('classificationHelp');
helpElement.innerHTML = helpTexts[classification] || '';
// Mostra/nascondi alert CSIRT
const csirtAlert = document.getElementById('csirtAlert');
if (['is-1', 'is-2', 'is-3', 'is-4'].includes(classification)) {
csirtAlert.style.display = 'block';
} else {
csirtAlert.style.display = 'none';
}
}
// Toggle GDPR
function toggleGDPR() {
const personalData = document.getElementById('personalData').value;
const gdprGroup = document.getElementById('gdprGroup');
if (personalData === 'yes') {
gdprGroup.style.display = 'block';
} else {
gdprGroup.style.display = 'none';
}
}
// Salva bozza
function saveDraft() {
alert('Bozza salvata con successo. Puoi continuare la compilazione in seguito.');
}
// Submit form
document.getElementById('incidentForm').addEventListener('submit', function(e) {
e.preventDefault();
// Validazione
const severity = document.getElementById('severity').value;
if (!severity) {
alert('Seleziona la severità dell\'incidente');
return;
}
// Genera codice incidente
const year = new Date().getFullYear();
const progressive = Math.floor(Math.random() * 900) + 100; // Simulato
const incidentCode = `INC-${year}-${progressive}`;
// Simula creazione incidente
const tooltip = document.createElement('div');
tooltip.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--bg-secondary);
border: 2px solid var(--success);
border-radius: 8px;
padding: 32px;
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
z-index: 10000;
max-width: 500px;
text-align: center;
`;
tooltip.innerHTML = `
<div style="font-size: 48px; margin-bottom: 16px;"></div>
<h2 style="color: var(--success); margin-bottom: 16px;">Incidente Creato con Successo</h2>
<p style="color: var(--text-secondary); margin-bottom: 8px;">Codice incidente:</p>
<p style="font-size: 24px; font-weight: 700; color: var(--text-primary); margin-bottom: 24px; font-family: monospace;">${incidentCode}</p>
<div style="background: var(--bg-tertiary); padding: 16px; border-radius: 6px; margin-bottom: 24px; text-align: left;">
<p style="font-size: 13px; color: var(--text-secondary); margin-bottom: 12px;"><strong>Prossime azioni:</strong></p>
<ul style="font-size: 13px; color: var(--text-secondary); margin-left: 20px;">
<li>Incident Response Team notificato</li>
<li>Timeline di gestione avviata</li>
${severity === 'sev-1' ? '<li style="color: var(--danger); font-weight: 600;">Crisis Team attivato</li>' : ''}
${['is-1', 'is-2', 'is-3'].includes(document.getElementById('classification').value) ? '<li style="color: var(--warning); font-weight: 600;">Preallarme CSIRT da inviare entro 24h</li>' : ''}
</ul>
</div>
<button onclick="window.location.href='incident-detail.html?id=${incidentCode}'"
style="padding: 12px 24px; background: var(--accent-primary); border: none; border-radius: 6px; color: white; font-weight: 600; cursor: pointer; margin-right: 12px;">
Vai all'Incidente
</button>
<button onclick="window.location.href='incident-dashboard.html'"
style="padding: 12px 24px; background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 6px; color: var(--text-primary); font-weight: 600; cursor: pointer;">
Torna alla Dashboard
</button>
`;
document.body.appendChild(tooltip);
});
// Inizializza
window.addEventListener('DOMContentLoaded', function() {
checkSubjectType();
setCurrentDateTime();
});
</script>
</body>
</html>

View File

@ -0,0 +1,758 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Notifiche CSIRT - INC-2024-047</title>
<style>
:root {
--bg-primary: #0d1117;
--bg-secondary: #161b22;
--bg-tertiary: #1c2128;
--border-color: #30363d;
--text-primary: #c9d1d9;
--text-secondary: #8b949e;
--accent-primary: #58a6ff;
--accent-secondary: #1f6feb;
--success: #3fb950;
--warning: #d29922;
--danger: #f85149;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
background-color: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
overflow-y: auto;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.header {
background-color: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
padding: 24px 0;
margin-bottom: 32px;
position: sticky;
top: 0;
z-index: 100;
}
.header-content {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
font-size: 24px;
font-weight: 600;
color: var(--text-primary);
}
.breadcrumb {
font-size: 13px;
color: var(--text-secondary);
margin-top: 4px;
}
.breadcrumb a {
color: var(--accent-primary);
text-decoration: none;
}
.btn {
padding: 8px 16px;
background-color: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-primary);
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
text-decoration: none;
}
.btn:hover {
border-color: var(--accent-primary);
color: var(--accent-primary);
}
.btn-primary {
background-color: var(--accent-primary);
border-color: var(--accent-primary);
color: white;
}
.btn-primary:hover {
background-color: var(--accent-secondary);
color: white;
}
.btn-danger {
background-color: var(--danger);
border-color: var(--danger);
color: white;
}
.btn-danger:hover {
background-color: #dc2626;
}
.alert-box {
background-color: rgba(210, 153, 34, 0.1);
border: 1px solid var(--warning);
border-left: 4px solid var(--warning);
border-radius: 6px;
padding: 20px;
margin-bottom: 24px;
}
.alert-box.danger {
background-color: rgba(248, 81, 73, 0.1);
border-color: var(--danger);
border-left-color: var(--danger);
}
.alert-box.success {
background-color: rgba(63, 185, 80, 0.1);
border-color: var(--success);
border-left-color: var(--success);
}
.alert-title {
font-weight: 600;
margin-bottom: 8px;
font-size: 15px;
display: flex;
align-items: center;
gap: 8px;
}
.alert-content {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.6;
}
.section {
background-color: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 24px;
margin-bottom: 24px;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border-color);
}
.section-title {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
}
.timeline-horizontal {
display: flex;
justify-content: space-between;
margin-bottom: 32px;
position: relative;
}
.timeline-horizontal::before {
content: '';
position: absolute;
top: 20px;
left: 0;
right: 0;
height: 2px;
background: var(--border-color);
z-index: 0;
}
.timeline-step {
flex: 1;
text-align: center;
position: relative;
z-index: 1;
}
.timeline-circle {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--bg-tertiary);
border: 2px solid var(--border-color);
margin: 0 auto 12px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 14px;
}
.timeline-step.completed .timeline-circle {
background: var(--success);
border-color: var(--success);
color: white;
}
.timeline-step.active .timeline-circle {
background: var(--warning);
border-color: var(--warning);
color: white;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(210, 153, 34, 0.7); }
50% { box-shadow: 0 0 0 10px rgba(210, 153, 34, 0); }
}
.timeline-label {
font-size: 12px;
color: var(--text-secondary);
font-weight: 600;
margin-bottom: 4px;
}
.timeline-deadline {
font-size: 11px;
color: var(--text-secondary);
}
.timeline-step.completed .timeline-label,
.timeline-step.active .timeline-label {
color: var(--text-primary);
}
.form-section {
background-color: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 20px;
margin-bottom: 20px;
}
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 8px;
}
.form-input, .form-select, .form-textarea {
width: 100%;
padding: 10px 12px;
background-color: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-primary);
font-size: 14px;
font-family: inherit;
}
.form-input:focus, .form-select:focus, .form-textarea:focus {
outline: none;
border-color: var(--accent-primary);
}
.form-textarea {
resize: vertical;
min-height: 120px;
}
.form-help {
font-size: 12px;
color: var(--text-secondary);
margin-top: 6px;
}
.info-box {
background-color: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 16px;
margin-bottom: 16px;
}
.info-box-title {
font-weight: 600;
color: var(--accent-primary);
margin-bottom: 8px;
font-size: 13px;
}
.info-box-content {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.6;
}
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.data-table th {
background-color: var(--bg-primary);
color: var(--text-secondary);
font-weight: 600;
text-align: left;
padding: 10px 12px;
border: 1px solid var(--border-color);
font-size: 11px;
text-transform: uppercase;
}
.data-table td {
padding: 10px 12px;
border: 1px solid var(--border-color);
color: var(--text-primary);
}
@media (max-width: 768px) {
.timeline-horizontal {
flex-direction: column;
}
.timeline-horizontal::before {
display: none;
}
.timeline-step {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
text-align: left;
}
.timeline-circle {
margin: 0;
}
}
</style>
</head>
<body>
<div class="header">
<div class="header-content">
<div>
<h1>📡 Notifiche CSIRT Italia - INC-2024-047</h1>
<div class="breadcrumb">
<a href="dashboard.html">Dashboard NIS2</a> /
<a href="incident-dashboard.html">Gestione Incidenti</a> /
<a href="incident-detail.html?id=INC-2024-047">INC-2024-047</a> /
Notifiche CSIRT
</div>
</div>
<a href="incident-detail.html?id=INC-2024-047" class="btn">← Torna all'Incidente</a>
</div>
</div>
<div class="container">
<!-- Alert Scadenza -->
<div class="alert-box danger">
<div class="alert-title">
⏰ Scadenza Imminente - Preallarme CSIRT
</div>
<div class="alert-content">
Il <strong>preallarme</strong> deve essere inviato al CSIRT Italia entro <strong>24 ore</strong> dalla conoscenza dell'incidente significativo.<br>
<strong>Scadenza:</strong> 2024-03-07 08:15 (rimangono <span style="color: var(--danger); font-weight: 700;">18 ore</span>)<br>
<strong>Canale:</strong> Portale CSIRT Italia (https://www.csirt.gov.it) o PEC dedicata
</div>
</div>
<!-- Timeline Notifiche -->
<div class="timeline-horizontal">
<div class="timeline-step active">
<div class="timeline-circle">1</div>
<div class="timeline-label">Preallarme</div>
<div class="timeline-deadline">Entro 24h</div>
</div>
<div class="timeline-step">
<div class="timeline-circle">2</div>
<div class="timeline-label">Notifica Completa</div>
<div class="timeline-deadline">Entro 72h</div>
</div>
<div class="timeline-step">
<div class="timeline-circle">3</div>
<div class="timeline-label">Relazioni Intermedie</div>
<div class="timeline-deadline">Su richiesta</div>
</div>
<div class="timeline-step">
<div class="timeline-circle">4</div>
<div class="timeline-label">Relazione Finale</div>
<div class="timeline-deadline">Entro 1 mese</div>
</div>
</div>
<!-- Sezione Preallarme -->
<div class="section">
<div class="section-header">
<div class="section-title">1. Preallarme (Entro 24 ore)</div>
</div>
<div class="info-box">
<div class="info-box-title">📋 Contenuto Minimo Preallarme</div>
<div class="info-box-content">
<ul style="margin-left: 20px; margin-top: 8px;">
<li>Se l'incidente è presumibilmente causato da atti illegittimi o malevoli</li>
<li>Se può avere impatto transfrontaliero</li>
<li>Descrizione preliminare dell'incidente</li>
</ul>
</div>
</div>
<form id="prealertForm">
<div class="form-section">
<h3 style="font-size: 16px; font-weight: 600; margin-bottom: 16px;">Dati Incidente</h3>
<div class="form-group">
<label class="form-label">Codice Incidente</label>
<input type="text" class="form-input" value="INC-2024-047" readonly>
</div>
<div class="form-group">
<label class="form-label">Data/Ora Rilevazione</label>
<input type="text" class="form-input" value="2024-03-06 08:15" readonly>
</div>
<div class="form-group">
<label class="form-label">Classificazione NIS2</label>
<input type="text" class="form-input" value="IS-2 - Impatto su integrità/riservatezza dati" readonly>
</div>
</div>
<div class="form-section">
<h3 style="font-size: 16px; font-weight: 600; margin-bottom: 16px;">Informazioni Preallarme</h3>
<div class="form-group">
<label class="form-label">Atti Illegittimi o Malevoli?</label>
<select class="form-select" required>
<option value="yes" selected>Sì - Attacco ransomware</option>
<option value="no">No</option>
<option value="unknown">Da determinare</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Impatto Transfrontaliero?</label>
<select class="form-select" required>
<option value="no" selected>No</option>
<option value="yes"></option>
<option value="possible">Possibile</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Descrizione Preliminare Incidente</label>
<textarea class="form-textarea" required>Tentativo di attacco ransomware (famiglia BlackCat/ALPHV) rilevato su server ERP di produzione. L'EDR ha bloccato l'esecuzione del malware in fase iniziale. Il vettore di attacco è stato identificato come email phishing con allegato malevolo, sfruttando credenziali utente precedentemente compromesse. Il sistema è stato immediatamente isolato. Nessuna crittografia dati avvenuta. Nessuna esfiltrazione rilevata. Incidente contenuto con successo.</textarea>
<div class="form-help">Fornire una descrizione sintetica ma completa dell'incidente</div>
</div>
<div class="form-group">
<label class="form-label">Sistemi/Servizi Impattati</label>
<textarea class="form-textarea">- Server ERP-PROD-01 (HW-SRV-001)
- Applicazione SAP ERP (SW-ERP-001)
- Servizio gestionale aziendale (SRV-ERP-001)
- 45 utenti interni impattati da interruzione servizio</textarea>
</div>
<div class="form-group">
<label class="form-label">Azioni di Contenimento Adottate</label>
<textarea class="form-textarea">- Isolamento immediato server dalla rete (08:22)
- Blocco account utente compromesso (09:30)
- Blocco IP C2 su firewall (09:45)
- Preservazione evidenze forensi (08:35)
- Attivazione Crisis Team e CISO (08:25)
- Contenimento confermato entro 2.2 ore dalla rilevazione</textarea>
</div>
</div>
<div class="form-section">
<h3 style="font-size: 16px; font-weight: 600; margin-bottom: 16px;">Dati Organizzazione</h3>
<div class="form-group">
<label class="form-label">Nome Organizzazione</label>
<input type="text" class="form-input" value="[Nome Azienda]" required>
</div>
<div class="form-group">
<label class="form-label">Settore NIS2</label>
<select class="form-select" required>
<option value="">Seleziona settore...</option>
<option value="energia">Energia</option>
<option value="trasporti">Trasporti</option>
<option value="bancario">Bancario</option>
<option value="sanitario">Sanitario</option>
<option value="digitale" selected>Infrastrutture Digitali</option>
<option value="acqua">Acqua Potabile</option>
<option value="altro">Altro</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Referente Tecnico</label>
<input type="text" class="form-input" value="CISO - P. Lombardi" required>
</div>
<div class="form-group">
<label class="form-label">Email Contatto</label>
<input type="email" class="form-input" value="ciso@azienda.it" required>
</div>
<div class="form-group">
<label class="form-label">Telefono Contatto</label>
<input type="tel" class="form-input" value="+39 02 1234567" required>
</div>
</div>
<div style="display: flex; gap: 12px; justify-content: flex-end; padding-top: 24px; border-top: 1px solid var(--border-color);">
<button type="button" class="btn" onclick="savePrealertDraft()">Salva Bozza</button>
<button type="button" class="btn btn-primary" onclick="previewPrealert()">Anteprima</button>
<button type="submit" class="btn btn-danger">📡 Invia Preallarme CSIRT</button>
</div>
</form>
</div>
<!-- Sezione Notifica Completa -->
<div class="section">
<div class="section-header">
<div class="section-title">2. Notifica Completa (Entro 72 ore)</div>
</div>
<div class="alert-box">
<div class="alert-title">⏸️ In Attesa Invio Preallarme</div>
<div class="alert-content">
La notifica completa può essere compilata dopo l'invio del preallarme.<br>
<strong>Scadenza:</strong> 2024-03-09 08:15 (72 ore dalla rilevazione)
</div>
</div>
<div class="info-box">
<div class="info-box-title">📋 Contenuto Notifica Completa</div>
<div class="info-box-content">
<ul style="margin-left: 20px; margin-top: 8px;">
<li>Aggiornamento informazioni del preallarme</li>
<li>Valutazione iniziale dell'incidente</li>
<li>Gravità e impatto dettagliato</li>
<li>Indicatori di compromissione (IoC) se disponibili</li>
<li>Misure di contenimento adottate</li>
<li>Stato corrente della gestione</li>
</ul>
</div>
</div>
<button class="btn" disabled style="opacity: 0.5;">Compila Notifica Completa (disponibile dopo preallarme)</button>
</div>
<!-- Sezione Relazione Finale -->
<div class="section">
<div class="section-header">
<div class="section-title">4. Relazione Finale (Entro 1 mese)</div>
</div>
<div class="alert-box">
<div class="alert-title">⏸️ In Attesa Chiusura Incidente</div>
<div class="alert-content">
La relazione finale deve essere inviata entro 1 mese dalla notifica completa,
o entro 1 mese dalla gestione completa dell'incidente se ancora in corso.<br>
<strong>Scadenza stimata:</strong> 2024-04-09
</div>
</div>
<div class="info-box">
<div class="info-box-title">📋 Contenuto Relazione Finale</div>
<div class="info-box-content">
<ul style="margin-left: 20px; margin-top: 8px;">
<li>Descrizione dettagliata dell'incidente</li>
<li>Causa radice (root cause) identificata o probabile</li>
<li>Misure di mitigazione applicate</li>
<li>Impatto transfrontaliero (se applicabile)</li>
<li>Tipo di minaccia o causa radice</li>
<li>Misure correttive in corso e pianificate</li>
<li>Lesson learned e raccomandazioni</li>
</ul>
</div>
</div>
<button class="btn" disabled style="opacity: 0.5;">Compila Relazione Finale (disponibile dopo chiusura incidente)</button>
</div>
<!-- Storico Comunicazioni -->
<div class="section">
<div class="section-header">
<div class="section-title">Storico Comunicazioni CSIRT</div>
</div>
<table class="data-table">
<thead>
<tr>
<th>Data/Ora</th>
<th>Tipo Comunicazione</th>
<th>Canale</th>
<th>Protocollo/Riferimento</th>
<th>Stato</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="6" style="text-align: center; color: var(--text-secondary); padding: 24px;">
Nessuna comunicazione inviata ancora
</td>
</tr>
</tbody>
</table>
</div>
</div>
<script>
function savePrealertDraft() {
alert('Bozza preallarme salvata con successo. Puoi continuare la compilazione in seguito.');
}
function previewPrealert() {
const tooltip = document.createElement('div');
tooltip.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 24px;
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
z-index: 10000;
max-width: 700px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
`;
tooltip.innerHTML = `
<h3 style="margin-bottom: 16px; color: var(--text-primary);">📄 Anteprima Preallarme CSIRT Italia</h3>
<div style="background: var(--bg-tertiary); padding: 20px; border-radius: 6px; margin-bottom: 16px; font-size: 13px; line-height: 1.8; font-family: monospace;">
<strong>PREALLARME INCIDENTE SIGNIFICATIVO NIS2</strong><br>
<strong>Protocollo:</strong> [Generato automaticamente]<br>
<strong>Data invio:</strong> ${new Date().toLocaleString('it-IT')}<br>
<br>
<strong>ORGANIZZAZIONE:</strong> [Nome Azienda]<br>
<strong>Settore:</strong> Infrastrutture Digitali<br>
<strong>Tipologia soggetto:</strong> ${sessionStorage.getItem('nis2_subject_type') === 'essential' ? 'Essenziale' : 'Importante'}<br>
<br>
<strong>CODICE INCIDENTE:</strong> INC-2024-047<br>
<strong>Data/Ora rilevazione:</strong> 2024-03-06 08:15<br>
<strong>Classificazione:</strong> IS-2 (Impatto integrità/riservatezza dati)<br>
<br>
<strong>NATURA INCIDENTE:</strong><br>
☑ Atti illegittimi o malevoli: SÌ (Attacco ransomware)<br>
☐ Impatto transfrontaliero: NO<br>
<br>
<strong>DESCRIZIONE PRELIMINARE:</strong><br>
Tentativo di attacco ransomware (famiglia BlackCat/ALPHV) rilevato su server ERP di produzione.
L'EDR ha bloccato l'esecuzione del malware in fase iniziale. Il vettore di attacco è stato identificato
come email phishing con allegato malevolo, sfruttando credenziali utente precedentemente compromesse.
Il sistema è stato immediatamente isolato. Nessuna crittografia dati avvenuta. Nessuna esfiltrazione rilevata.
Incidente contenuto con successo.<br>
<br>
<strong>SISTEMI IMPATTATI:</strong><br>
- Server ERP-PROD-01<br>
- Applicazione SAP ERP<br>
- 45 utenti interni<br>
<br>
<strong>AZIONI CONTENIMENTO:</strong><br>
- Isolamento server (2.2h)<br>
- Blocco account compromesso<br>
- Blocco IP C2<br>
- Evidenze preservate<br>
<br>
<strong>REFERENTE:</strong> CISO - P. Lombardi<br>
<strong>Contatto:</strong> ciso@azienda.it / +39 02 1234567<br>
<br>
<em>Notifica completa seguirà entro 72 ore.</em>
</div>
<div style="display: flex; gap: 12px;">
<button onclick="this.parentElement.parentElement.remove()" style="flex: 1; padding: 10px; background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 6px; color: var(--text-primary); font-weight: 600; cursor: pointer;">
Chiudi
</button>
<button onclick="this.parentElement.parentElement.remove(); document.getElementById('prealertForm').dispatchEvent(new Event('submit'));" style="flex: 1; padding: 10px; background: var(--danger); border: none; border-radius: 6px; color: white; font-weight: 600; cursor: pointer;">
Conferma e Invia
</button>
</div>
`;
document.body.appendChild(tooltip);
}
document.getElementById('prealertForm').addEventListener('submit', function(e) {
e.preventDefault();
const tooltip = document.createElement('div');
tooltip.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--bg-secondary);
border: 2px solid var(--success);
border-radius: 8px;
padding: 32px;
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
z-index: 10000;
max-width: 500px;
text-align: center;
`;
tooltip.innerHTML = `
<div style="font-size: 48px; margin-bottom: 16px;"></div>
<h2 style="color: var(--success); margin-bottom: 16px;">Preallarme Inviato con Successo</h2>
<p style="color: var(--text-secondary); margin-bottom: 8px;">Protocollo CSIRT:</p>
<p style="font-size: 20px; font-weight: 700; color: var(--text-primary); margin-bottom: 24px; font-family: monospace;">CSIRT-2024-03-06-${Math.floor(Math.random() * 10000)}</p>
<div style="background: var(--bg-tertiary); padding: 16px; border-radius: 6px; margin-bottom: 24px; text-align: left;">
<p style="font-size: 13px; color: var(--text-secondary); margin-bottom: 12px;"><strong>Dettagli invio:</strong></p>
<ul style="font-size: 13px; color: var(--text-secondary); margin-left: 20px;">
<li>Data/Ora invio: ${new Date().toLocaleString('it-IT')}</li>
<li>Canale: Portale CSIRT Italia</li>
<li>Ricevuta PEC: Confermata</li>
<li>Scadenza rispettata: ✅ SÌ (entro 24h)</li>
</ul>
<p style="font-size: 13px; color: var(--text-secondary); margin-top: 12px;">
<strong>Prossimo step:</strong> Notifica completa entro 2024-03-09 08:15 (72h)
</p>
</div>
<button onclick="window.location.href='incident-detail.html?id=INC-2024-047'" style="padding: 12px 24px; background: var(--accent-primary); border: none; border-radius: 6px; color: white; font-weight: 600; cursor: pointer;">
Torna all'Incidente
</button>
`;
document.body.appendChild(tooltip);
});
</script>
</body>
</html>

View File

@ -0,0 +1,803 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Post-Incident Review - INC-2024-047</title>
<style>
:root {
--bg-primary: #0d1117;
--bg-secondary: #161b22;
--bg-tertiary: #1c2128;
--border-color: #30363d;
--text-primary: #c9d1d9;
--text-secondary: #8b949e;
--accent-primary: #58a6ff;
--success: #3fb950;
--warning: #d29922;
--danger: #f85149;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
background-color: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
overflow-y: auto;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
.header {
background-color: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
padding: 24px 0;
margin-bottom: 32px;
position: sticky;
top: 0;
z-index: 100;
}
.header-content {
max-width: 1400px;
margin: 0 auto;
padding: 0 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
font-size: 24px;
font-weight: 600;
color: var(--text-primary);
}
.breadcrumb {
font-size: 13px;
color: var(--text-secondary);
margin-top: 4px;
}
.breadcrumb a {
color: var(--accent-primary);
text-decoration: none;
}
.btn {
padding: 8px 16px;
background-color: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-primary);
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
text-decoration: none;
}
.btn:hover {
border-color: var(--accent-primary);
}
.btn-primary {
background-color: var(--accent-primary);
border-color: var(--accent-primary);
color: white;
}
.btn-primary:hover {
background-color: #1f6feb;
}
.alert-box {
background-color: rgba(88, 166, 255, 0.1);
border: 1px solid var(--accent-primary);
border-left: 4px solid var(--accent-primary);
border-radius: 6px;
padding: 20px;
margin-bottom: 24px;
}
.alert-title {
font-weight: 600;
margin-bottom: 8px;
font-size: 15px;
}
.alert-content {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.6;
}
.section {
background-color: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 24px;
margin-bottom: 24px;
}
.section-title {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border-color);
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.metric-card {
background-color: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 16px;
text-align: center;
}
.metric-label {
font-size: 11px;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 8px;
}
.metric-value {
font-size: 28px;
font-weight: 700;
color: var(--text-primary);
}
.metric-value.success {
color: var(--success);
}
.metric-value.warning {
color: var(--warning);
}
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 8px;
}
.form-textarea {
width: 100%;
padding: 12px;
background-color: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-primary);
font-size: 14px;
font-family: inherit;
resize: vertical;
min-height: 100px;
}
.form-textarea:focus {
outline: none;
border-color: var(--accent-primary);
}
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.data-table th {
background-color: var(--bg-tertiary);
color: var(--text-secondary);
font-weight: 600;
text-align: left;
padding: 10px 12px;
border: 1px solid var(--border-color);
font-size: 11px;
text-transform: uppercase;
}
.data-table td {
padding: 10px 12px;
border: 1px solid var(--border-color);
color: var(--text-primary);
}
.priority-high {
color: var(--danger);
font-weight: 700;
}
.priority-medium {
color: var(--warning);
font-weight: 600;
}
.priority-low {
color: var(--text-secondary);
}
.status-badge {
display: inline-block;
padding: 4px 8px;
border-radius: 3px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
}
.status-planned {
background-color: rgba(88, 166, 255, 0.2);
color: var(--accent-primary);
border: 1px solid var(--accent-primary);
}
.status-progress {
background-color: rgba(210, 153, 34, 0.2);
color: var(--warning);
border: 1px solid var(--warning);
}
.status-completed {
background-color: rgba(63, 185, 80, 0.2);
color: var(--success);
border: 1px solid var(--success);
}
</style>
</head>
<body>
<div class="header">
<div class="header-content">
<div>
<h1>📊 Post-Incident Review - INC-2024-047</h1>
<div class="breadcrumb">
<a href="dashboard.html">Dashboard NIS2</a> /
<a href="incident-dashboard.html">Gestione Incidenti</a> /
<a href="incident-detail.html?id=INC-2024-047">INC-2024-047</a> /
Post-Incident Review
</div>
</div>
<div style="display: flex; gap: 8px;">
<button class="btn" onclick="exportPIR()">📄 Esporta Report</button>
<a href="incident-detail.html?id=INC-2024-047" class="btn">← Torna all'Incidente</a>
</div>
</div>
</div>
<div class="container">
<div class="alert-box">
<div class="alert-title"> Post-Incident Review (RC.CO-03)</div>
<div class="alert-content">
Analisi completa dell'incidente per identificare lesson learned e azioni di miglioramento.
Da completare entro <strong>2 settimane dalla chiusura</strong> per incidenti SEV-1/SEV-2.
<br><br>
<strong>Partecipanti richiesti:</strong> Incident Response Team, CISO, Responsabili Divisione impattate, Direzione (per SEV-1)
</div>
</div>
<!-- Metriche Incidente -->
<div class="section">
<div class="section-title">Metriche Incidente</div>
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-label">TTD (Time to Detect)</div>
<div class="metric-value success">0.5h</div>
<div style="font-size: 11px; color: var(--text-secondary); margin-top: 4px;">Target: <2h </div>
</div>
<div class="metric-card">
<div class="metric-label">TTC (Time to Contain)</div>
<div class="metric-value success">2.2h</div>
<div style="font-size: 11px; color: var(--text-secondary); margin-top: 4px;">Target SEV-1: <1h </div>
</div>
<div class="metric-card">
<div class="metric-label">TTR (Time to Recover)</div>
<div class="metric-value success">30.25h</div>
<div style="font-size: 11px; color: var(--text-secondary); margin-top: 4px;">RTO: ≤48h ✅</div>
</div>
<div class="metric-card">
<div class="metric-label">Downtime Totale</div>
<div class="metric-value warning">30h 15m</div>
<div style="font-size: 11px; color: var(--text-secondary); margin-top: 4px;">45 utenti impattati</div>
</div>
<div class="metric-card">
<div class="metric-label">Costo Stimato</div>
<div class="metric-value">€12.5K</div>
<div style="font-size: 11px; color: var(--text-secondary); margin-top: 4px;">Diretto + indiretto</div>
</div>
<div class="metric-card">
<div class="metric-label">Conformità Notifiche</div>
<div class="metric-value success">100%</div>
<div style="font-size: 11px; color: var(--text-secondary); margin-top: 4px;">CSIRT entro 24h ✅</div>
</div>
</div>
</div>
<!-- Root Cause Analysis -->
<div class="section">
<div class="section-title">Root Cause Analysis (5 Whys)</div>
<div style="background: var(--bg-tertiary); padding: 20px; border-radius: 6px; margin-bottom: 20px;">
<div style="margin-bottom: 16px;">
<strong style="color: var(--accent-primary);">Problema:</strong> Tentativo di attacco ransomware su server ERP
</div>
<div style="margin-bottom: 12px;">
<strong style="color: var(--text-primary);">1. Perché è successo?</strong><br>
<span style="color: var(--text-secondary);">→ Perché un utente ha aperto un allegato malevolo da email phishing</span>
</div>
<div style="margin-bottom: 12px;">
<strong style="color: var(--text-primary);">2. Perché l'utente ha aperto l'allegato?</strong><br>
<span style="color: var(--text-secondary);">→ Perché l'email sembrava legittima (spoofing fornitore) e l'utente non ha riconosciuto i segnali di phishing</span>
</div>
<div style="margin-bottom: 12px;">
<strong style="color: var(--text-primary);">3. Perché l'utente non ha riconosciuto il phishing?</strong><br>
<span style="color: var(--text-secondary);">→ Perché la formazione awareness sulla sicurezza non era sufficientemente frequente e pratica</span>
</div>
<div style="margin-bottom: 12px;">
<strong style="color: var(--text-primary);">4. Perché le credenziali dell'utente erano già compromesse?</strong><br>
<span style="color: var(--text-secondary);">→ Perché l'utente utilizzava la stessa password su servizi esterni (credential stuffing attack non rilevato)</span>
</div>
<div>
<strong style="color: var(--text-primary);">5. Perché non c'era MFA obbligatoria su account amministrativi?</strong><br>
<span style="color: var(--text-secondary);">→ Perché la policy MFA era in fase di rollout graduale e non ancora applicata a tutti gli account admin</span>
</div>
</div>
<div style="background: var(--bg-tertiary); padding: 16px; border-radius: 6px; border-left: 4px solid var(--danger);">
<strong style="color: var(--danger);">ROOT CAUSE IDENTIFICATA:</strong><br>
<span style="color: var(--text-secondary); font-size: 13px;">
1. MFA non obbligatoria su tutti gli account amministrativi<br>
2. Formazione awareness non sufficientemente efficace<br>
3. Monitoraggio credential stuffing non attivo<br>
4. Policy password debole (riutilizzo su servizi esterni)
</span>
</div>
</div>
<!-- Valutazione Efficacia -->
<div class="section">
<div class="section-title">Valutazione Efficacia Risposta</div>
<table class="data-table">
<thead>
<tr>
<th>Fase</th>
<th>Valutazione</th>
<th>Punti di Forza</th>
<th>Aree di Miglioramento</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Rilevazione</strong></td>
<td><span style="color: var(--success); font-weight: 600;">✅ Eccellente</span></td>
<td>EDR ha bloccato immediatamente il malware. TTD: 0.5h</td>
<td>Nessuna - detection efficace</td>
</tr>
<tr>
<td><strong>Triage</strong></td>
<td><span style="color: var(--success); font-weight: 600;">✅ Buono</span></td>
<td>Classificazione corretta entro 1h. Crisis Team attivato tempestivamente</td>
<td>Processo decisionale può essere ulteriormente accelerato</td>
</tr>
<tr>
<td><strong>Contenimento</strong></td>
<td><span style="color: var(--warning); font-weight: 600;">⚠️ Sufficiente</span></td>
<td>Isolamento rapido. Nessuna propagazione</td>
<td>TTC 2.2h > target 1h per SEV-1. Procedure di isolamento da ottimizzare</td>
</tr>
<tr>
<td><strong>Eradicazione</strong></td>
<td><span style="color: var(--success); font-weight: 600;">✅ Buono</span></td>
<td>Rimozione completa minaccia. Hardening applicato</td>
<td>Analisi forense richiede più tempo del previsto</td>
</tr>
<tr>
<td><strong>Ripristino</strong></td>
<td><span style="color: var(--success); font-weight: 600;">✅ Buono</span></td>
<td>TTR 30.25h < RTO 48h. Backup immutabile efficace</td>
<td>Processo di verifica post-ripristino può essere standardizzato</td>
</tr>
<tr>
<td><strong>Comunicazioni</strong></td>
<td><span style="color: var(--success); font-weight: 600;">✅ Eccellente</span></td>
<td>Preallarme CSIRT entro 24h. Comunicazioni interne tempestive</td>
<td>Template comunicazioni possono essere pre-compilati</td>
</tr>
</tbody>
</table>
</div>
<!-- Gap Identificati -->
<div class="section">
<div class="section-title">Gap Identificati</div>
<div class="form-group">
<label class="form-label">1. GAP TECNICI</label>
<textarea class="form-textarea" readonly>• MFA non obbligatoria su tutti gli account amministrativi
• Monitoraggio credential stuffing assente
• Filtri email anti-phishing non sufficientemente efficaci
• Procedure di isolamento automatico non implementate
• Tempo di contenimento superiore al target per SEV-1</textarea>
</div>
<div class="form-group">
<label class="form-label">2. GAP PROCEDURALI</label>
<textarea class="form-textarea" readonly>• Policy MFA in rollout graduale (non completata)
• Procedura di isolamento rapido non documentata
• Checklist contenimento SEV-1 non disponibile in formato rapido
• Processo di escalation Crisis Team può essere ottimizzato
• Template comunicazioni non pre-compilati</textarea>
</div>
<div class="form-group">
<label class="form-label">3. GAP ORGANIZZATIVI</label>
<textarea class="form-textarea" readonly>• Formazione awareness non sufficientemente frequente
• Simulazioni phishing non regolari
• Policy password debole (riutilizzo su servizi esterni non vietato esplicitamente)
• Competenze forensics interne limitate (dipendenza da consulenti esterni)</textarea>
</div>
<div class="form-group">
<label class="form-label">4. GAP FORMATIVI</label>
<textarea class="form-textarea" readonly>• Utenti non addestrati a riconoscere phishing avanzato
• Account amministrativi senza formazione specifica su minacce mirate
• Awareness su riutilizzo password insufficiente
• Esercitazioni tabletop incident response non regolari</textarea>
</div>
</div>
<!-- Azioni Correttive -->
<div class="section">
<div class="section-title">Piano Azioni Correttive</div>
<table class="data-table">
<thead>
<tr>
<th>ID</th>
<th>Azione Correttiva</th>
<th>Categoria</th>
<th>Priorità</th>
<th>Responsabile</th>
<th>Scadenza</th>
<th>Org. Rif.</th>
<th>Stato</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>AC-001</strong></td>
<td>Implementare MFA obbligatoria su TUTTI gli account amministrativi</td>
<td>Tecnica</td>
<td><span class="priority-high">ALTA</span></td>
<td>IT Manager</td>
<td>2024-03-20</td>
<td>Org.03</td>
<td><span class="status-badge status-progress">In corso</span></td>
</tr>
<tr>
<td><strong>AC-002</strong></td>
<td>Implementare monitoraggio credential stuffing su SIEM</td>
<td>Tecnica</td>
<td><span class="priority-high">ALTA</span></td>
<td>SOC Lead</td>
<td>2024-03-25</td>
<td>Org.09</td>
<td><span class="status-badge status-planned">Pianificata</span></td>
</tr>
<tr>
<td><strong>AC-003</strong></td>
<td>Rafforzare filtri anti-phishing email gateway</td>
<td>Tecnica</td>
<td><span class="priority-high">ALTA</span></td>
<td>IT Security</td>
<td>2024-03-15</td>
<td>Org.08</td>
<td><span class="status-badge status-progress">In corso</span></td>
</tr>
<tr>
<td><strong>AC-004</strong></td>
<td>Implementare isolamento automatico endpoint compromessi</td>
<td>Tecnica</td>
<td><span class="priority-medium">MEDIA</span></td>
<td>IT Security</td>
<td>2024-04-15</td>
<td>Org.08</td>
<td><span class="status-badge status-planned">Pianificata</span></td>
</tr>
<tr>
<td><strong>AC-005</strong></td>
<td>Aggiornare policy password (vietare riutilizzo su servizi esterni)</td>
<td>Procedurale</td>
<td><span class="priority-high">ALTA</span></td>
<td>CISO</td>
<td>2024-03-10</td>
<td>Org.03</td>
<td><span class="status-badge status-progress">In corso</span></td>
</tr>
<tr>
<td><strong>AC-006</strong></td>
<td>Creare checklist rapida contenimento SEV-1</td>
<td>Procedurale</td>
<td><span class="priority-medium">MEDIA</span></td>
<td>CISO</td>
<td>2024-03-15</td>
<td>Org.10</td>
<td><span class="status-badge status-planned">Pianificata</span></td>
</tr>
<tr>
<td><strong>AC-007</strong></td>
<td>Preparare template comunicazioni pre-compilati</td>
<td>Procedurale</td>
<td><span class="priority-low">BASSA</span></td>
<td>CISO</td>
<td>2024-04-30</td>
<td>Org.10</td>
<td><span class="status-badge status-planned">Pianificata</span></td>
</tr>
<tr>
<td><strong>AC-008</strong></td>
<td>Aumentare frequenza formazione awareness (mensile)</td>
<td>Formativa</td>
<td><span class="priority-high">ALTA</span></td>
<td>HR + CISO</td>
<td>2024-04-01</td>
<td>Org.04</td>
<td><span class="status-badge status-planned">Pianificata</span></td>
</tr>
<tr>
<td><strong>AC-009</strong></td>
<td>Implementare simulazioni phishing trimestrali</td>
<td>Formativa</td>
<td><span class="priority-medium">MEDIA</span></td>
<td>CISO</td>
<td>2024-04-15</td>
<td>Org.04</td>
<td><span class="status-badge status-planned">Pianificata</span></td>
</tr>
<tr>
<td><strong>AC-010</strong></td>
<td>Formazione specifica account amministrativi su minacce mirate</td>
<td>Formativa</td>
<td><span class="priority-medium">MEDIA</span></td>
<td>CISO</td>
<td>2024-03-30</td>
<td>Org.04</td>
<td><span class="status-badge status-planned">Pianificata</span></td>
</tr>
<tr>
<td><strong>AC-011</strong></td>
<td>Aggiornare risk assessment con nuovo scenario ransomware</td>
<td>Organizzativa</td>
<td><span class="priority-high">ALTA</span></td>
<td>CISO</td>
<td>2024-03-20</td>
<td>Org.05</td>
<td><span class="status-badge status-planned">Pianificata</span></td>
</tr>
<tr>
<td><strong>AC-012</strong></td>
<td>Pianificare esercitazione tabletop ransomware</td>
<td>Organizzativa</td>
<td><span class="priority-medium">MEDIA</span></td>
<td>CISO</td>
<td>2024-05-31</td>
<td>Org.08</td>
<td><span class="status-badge status-planned">Pianificata</span></td>
</tr>
</tbody>
</table>
<div style="margin-top: 24px; padding: 16px; background: var(--bg-tertiary); border-radius: 6px;">
<strong style="color: var(--text-primary);">Riepilogo Azioni:</strong><br>
<div style="display: flex; gap: 24px; margin-top: 12px; font-size: 13px;">
<div>
<span style="color: var(--text-secondary);">Totali:</span>
<strong style="color: var(--text-primary);"> 12</strong>
</div>
<div>
<span style="color: var(--text-secondary);">Alta priorità:</span>
<strong style="color: var(--danger);"> 6</strong>
</div>
<div>
<span style="color: var(--text-secondary);">Media priorità:</span>
<strong style="color: var(--warning);"> 5</strong>
</div>
<div>
<span style="color: var(--text-secondary);">Bassa priorità:</span>
<strong style="color: var(--text-secondary);"> 1</strong>
</div>
</div>
</div>
</div>
<!-- Raccomandazioni -->
<div class="section">
<div class="section-title">Raccomandazioni e Lesson Learned</div>
<div style="background: var(--bg-tertiary); padding: 20px; border-radius: 6px; margin-bottom: 16px;">
<h4 style="font-size: 14px; font-weight: 600; color: var(--success); margin-bottom: 12px;">
✅ COSA HA FUNZIONATO BENE
</h4>
<ul style="font-size: 13px; color: var(--text-secondary); line-height: 1.8; margin-left: 20px;">
<li>EDR ha bloccato immediatamente il ransomware prima della crittografia</li>
<li>Backup immutabile ha permesso ripristino rapido e sicuro</li>
<li>Crisis Team ha risposto prontamente e in modo coordinato</li>
<li>Comunicazioni CSIRT rispettate nei tempi (preallarme entro 24h)</li>
<li>Nessuna propagazione dell'attacco ad altri sistemi</li>
<li>Preservazione evidenze forensi efficace</li>
</ul>
</div>
<div style="background: var(--bg-tertiary); padding: 20px; border-radius: 6px; margin-bottom: 16px;">
<h4 style="font-size: 14px; font-weight: 600; color: var(--warning); margin-bottom: 12px;">
⚠️ COSA MIGLIORARE
</h4>
<ul style="font-size: 13px; color: var(--text-secondary); line-height: 1.8; margin-left: 20px;">
<li>Tempo di contenimento (2.2h) superiore al target per SEV-1 (<1h)</li>
<li>MFA non era obbligatoria su account amministrativi</li>
<li>Formazione awareness non sufficientemente efficace</li>
<li>Monitoraggio credential stuffing assente</li>
<li>Procedure di isolamento rapido non documentate</li>
</ul>
</div>
<div style="background: var(--bg-tertiary); padding: 20px; border-radius: 6px;">
<h4 style="font-size: 14px; font-weight: 600; color: var(--accent-primary); margin-bottom: 12px;">
💡 LESSON LEARNED CHIAVE
</h4>
<ol style="font-size: 13px; color: var(--text-secondary); line-height: 1.8; margin-left: 20px;">
<li><strong>MFA è fondamentale:</strong> L'assenza di MFA su account amministrativi è stata la vulnerabilità critica. Implementazione immediata obbligatoria.</li>
<li><strong>Backup immutabile salva:</strong> Il backup immutabile ha evitato perdita dati e permesso ripristino rapido. Investimento essenziale.</li>
<li><strong>EDR efficace ma non sufficiente:</strong> EDR ha bloccato il malware ma non ha prevenuto il phishing. Serve approccio multi-layer.</li>
<li><strong>Formazione continua necessaria:</strong> Awareness sporadica non è efficace. Serve formazione continua e simulazioni pratiche.</li>
<li><strong>Procedure automatiche riducono TTC:</strong> Isolamento manuale ha richiesto tempo. Automazione può ridurre significativamente TTC.</li>
<li><strong>Comunicazioni tempestive cruciali:</strong> Rispetto scadenze CSIRT e comunicazioni interne hanno evitato complicazioni legali/reputazionali.</li>
</ol>
</div>
</div>
<!-- Finalizzazione -->
<div class="section">
<div class="section-title">Finalizzazione Post-Incident Review</div>
<div style="background: var(--bg-tertiary); padding: 16px; border-radius: 6px; margin-bottom: 20px;">
<strong style="color: var(--text-primary);">Partecipanti PIR:</strong><br>
<ul style="font-size: 13px; color: var(--text-secondary); margin-top: 8px; margin-left: 20px;">
<li>CISO - P. Lombardi (coordinatore)</li>
<li>SOC Lead - L. Verdi</li>
<li>System Admin - G. Rossi</li>
<li>Forensics Team - A. Neri</li>
<li>IT Manager - M. Bianchi</li>
<li>Responsabile Divisione Finance - S. Neri</li>
<li>Direzione - CdA Representative</li>
</ul>
</div>
<div style="display: flex; gap: 12px; justify-content: flex-end; padding-top: 24px; border-top: 1px solid var(--border-color);">
<button class="btn" onclick="savePIRDraft()">Salva Bozza</button>
<button class="btn" onclick="exportPIR()">📄 Esporta Report</button>
<button class="btn btn-primary" onclick="completePIR()">✅ Finalizza PIR e Chiudi Incidente</button>
</div>
</div>
</div>
<script>
function savePIRDraft() {
alert('Bozza Post-Incident Review salvata con successo.');
}
function exportPIR() {
const tooltip = document.createElement('div');
tooltip.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 24px;
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
z-index: 10000;
max-width: 500px;
`;
tooltip.innerHTML = `
<h3 style="margin-bottom: 16px; color: var(--text-primary);">📄 Export Report Post-Incident Review</h3>
<p style="color: var(--text-secondary); margin-bottom: 16px; font-size: 13px;">
Report completo PIR per incidente INC-2024-047
</p>
<div style="background: var(--bg-tertiary); padding: 16px; border-radius: 4px; margin-bottom: 16px; font-family: monospace; font-size: 12px;">
📄 PIR_INC-2024-047_Report_Completo.pdf<br>
📄 PIR_INC-2024-047_Azioni_Correttive.xlsx<br>
📄 PIR_INC-2024-047_Executive_Summary.pdf<br>
📄 PIR_INC-2024-047_Lesson_Learned.docx
</div>
<button onclick="this.parentElement.remove()" style="width: 100%; padding: 10px; background: var(--accent-primary); border: none; border-radius: 6px; color: white; font-weight: 600; cursor: pointer;">
Chiudi
</button>
`;
document.body.appendChild(tooltip);
}
function completePIR() {
const tooltip = document.createElement('div');
tooltip.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--bg-secondary);
border: 2px solid var(--success);
border-radius: 8px;
padding: 32px;
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
z-index: 10000;
max-width: 600px;
text-align: center;
`;
tooltip.innerHTML = `
<div style="font-size: 48px; margin-bottom: 16px;"></div>
<h2 style="color: var(--success); margin-bottom: 16px;">Post-Incident Review Completata</h2>
<p style="color: var(--text-secondary); margin-bottom: 24px; font-size: 14px;">
Incidente INC-2024-047 ufficialmente chiuso
</p>
<div style="background: var(--bg-tertiary); padding: 20px; border-radius: 6px; margin-bottom: 24px; text-align: left;">
<p style="font-size: 13px; color: var(--text-secondary); margin-bottom: 12px;"><strong>Riepilogo Finale:</strong></p>
<ul style="font-size: 13px; color: var(--text-secondary); margin-left: 20px;">
<li>12 azioni correttive identificate e assegnate</li>
<li>6 azioni ad alta priorità in tracking</li>
<li>Risk assessment aggiornato (Org.05)</li>
<li>Lesson learned condivise con team</li>
<li>Report PIR distribuito a stakeholder</li>
<li>Relazione finale CSIRT da completare</li>
</ul>
<p style="font-size: 13px; color: var(--text-secondary); margin-top: 16px;">
<strong>Prossimi step:</strong> Monitoraggio azioni correttive tramite sistema non conformità (ID.IM-01)
</p>
</div>
<div style="display: flex; gap: 12px;">
<button onclick="window.location.href='incident-dashboard.html'" style="flex: 1; padding: 12px; background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 6px; color: var(--text-primary); font-weight: 600; cursor: pointer;">
Dashboard Incidenti
</button>
<button onclick="window.location.href='incident-notification.html?id=INC-2024-047&action=final'" style="flex: 1; padding: 12px; background: var(--accent-primary); border: none; border-radius: 6px; color: white; font-weight: 600; cursor: pointer;">
Completa Relazione Finale CSIRT
</button>
</div>
`;
document.body.appendChild(tooltip);
}
</script>
</body>
</html>

View File

@ -0,0 +1,660 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ripristino Servizi - INC-2024-047</title>
<style>
:root {
--bg-primary: #0d1117;
--bg-secondary: #161b22;
--bg-tertiary: #1c2128;
--border-color: #30363d;
--text-primary: #c9d1d9;
--text-secondary: #8b949e;
--accent-primary: #58a6ff;
--success: #3fb950;
--warning: #d29922;
--danger: #f85149;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
background-color: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
overflow-y: auto;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
.header {
background-color: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
padding: 24px 0;
margin-bottom: 32px;
position: sticky;
top: 0;
z-index: 100;
}
.header-content {
max-width: 1400px;
margin: 0 auto;
padding: 0 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
font-size: 24px;
font-weight: 600;
color: var(--text-primary);
}
.breadcrumb {
font-size: 13px;
color: var(--text-secondary);
margin-top: 4px;
}
.breadcrumb a {
color: var(--accent-primary);
text-decoration: none;
}
.btn {
padding: 8px 16px;
background-color: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-primary);
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
text-decoration: none;
}
.btn:hover {
border-color: var(--accent-primary);
}
.btn-success {
background-color: var(--success);
border-color: var(--success);
color: white;
}
.btn-success:hover {
background-color: #2ea043;
}
.alert-box {
background-color: rgba(88, 166, 255, 0.1);
border: 1px solid var(--accent-primary);
border-left: 4px solid var(--accent-primary);
border-radius: 6px;
padding: 20px;
margin-bottom: 24px;
}
.alert-title {
font-weight: 600;
margin-bottom: 8px;
font-size: 15px;
}
.alert-content {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.6;
}
.section {
background-color: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 24px;
margin-bottom: 24px;
}
.section-title {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border-color);
}
.checklist {
list-style: none;
}
.checklist li {
padding: 12px;
background-color: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 4px;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 12px;
}
.checklist input[type="checkbox"] {
width: 20px;
height: 20px;
cursor: pointer;
}
.checklist li.completed {
background-color: rgba(63, 185, 80, 0.1);
border-color: var(--success);
}
.priority-badge {
display: inline-block;
padding: 4px 8px;
border-radius: 3px;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
}
.priority-1 {
background-color: rgba(248, 81, 73, 0.2);
color: var(--danger);
border: 1px solid var(--danger);
}
.priority-2 {
background-color: rgba(210, 153, 34, 0.2);
color: var(--warning);
border: 1px solid var(--warning);
}
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.data-table th {
background-color: var(--bg-tertiary);
color: var(--text-secondary);
font-weight: 600;
text-align: left;
padding: 10px 12px;
border: 1px solid var(--border-color);
font-size: 11px;
text-transform: uppercase;
}
.data-table td {
padding: 10px 12px;
border: 1px solid var(--border-color);
color: var(--text-primary);
}
.data-table tr:hover {
background-color: var(--bg-tertiary);
}
</style>
</head>
<body>
<div class="header">
<div class="header-content">
<div>
<h1>🔄 Ripristino Servizi - INC-2024-047</h1>
<div class="breadcrumb">
<a href="dashboard.html">Dashboard NIS2</a> /
<a href="incident-dashboard.html">Gestione Incidenti</a> /
<a href="incident-detail.html?id=INC-2024-047">INC-2024-047</a> /
Ripristino
</div>
</div>
<a href="incident-detail.html?id=INC-2024-047" class="btn">← Torna all'Incidente</a>
</div>
</div>
<div class="container">
<div class="alert-box">
<div class="alert-title"> Processo di Ripristino (RC.RP-01)</div>
<div class="alert-content">
Ripristino graduale dei servizi secondo priorità BIA. Ogni sistema deve essere verificato per integrità,
funzionalità e sicurezza prima del ripristino in produzione. Monitoraggio intensivo post-ripristino per 72 ore.
</div>
</div>
<!-- Priorità Ripristino -->
<div class="section">
<div class="section-title">Priorità Ripristino (secondo BIA Org.08)</div>
<table class="data-table">
<thead>
<tr>
<th>Priorità</th>
<th>Sistema/Servizio</th>
<th>RTO Dichiarato</th>
<th>Stato</th>
<th>ETA Ripristino</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
<tr>
<td><span class="priority-badge priority-1">P1 - Critico</span></td>
<td><strong>Server ERP-PROD-01</strong><br>Sistema gestionale principale</td>
<td>≤4h</td>
<td><span style="color: var(--warning);">🔄 In ripristino</span></td>
<td>2024-03-08 14:00</td>
<td><button class="btn" onclick="viewRecoveryDetails('erp')">Dettagli</button></td>
</tr>
<tr>
<td><span class="priority-badge priority-1">P1 - Critico</span></td>
<td><strong>Applicazione SAP ERP</strong><br>Software gestionale</td>
<td>≤4h</td>
<td><span style="color: var(--text-secondary);">⏸️ In attesa server</span></td>
<td>2024-03-08 16:00</td>
<td><button class="btn" disabled>In attesa</button></td>
</tr>
</tbody>
</table>
</div>
<!-- Checklist Ripristino -->
<div class="section">
<div class="section-title">Checklist Ripristino Server ERP-PROD-01</div>
<h3 style="font-size: 16px; font-weight: 600; margin-bottom: 16px; color: var(--text-primary);">1. Valutazione Pre-Ripristino</h3>
<ul class="checklist">
<li class="completed">
<input type="checkbox" checked disabled>
<span>Minaccia completamente rimossa ed eradicata</span>
</li>
<li class="completed">
<input type="checkbox" checked disabled>
<span>Nessun indicatore di compromissione residuo</span>
</li>
<li class="completed">
<input type="checkbox" checked disabled>
<span>Backup verificato e integro (hash matching)</span>
</li>
<li class="completed">
<input type="checkbox" checked disabled>
<span>Strategia di ripristino definita: Ripristino da backup immutabile</span>
</li>
</ul>
<h3 style="font-size: 16px; font-weight: 600; margin: 24px 0 16px; color: var(--text-primary);">2. Ripristino Sistema</h3>
<ul class="checklist">
<li class="completed">
<input type="checkbox" checked disabled>
<span>Ripristino da backup immutabile avviato (2024-03-07 10:00)</span>
</li>
<li class="completed">
<input type="checkbox" checked disabled>
<span>Sistema operativo ripristinato e verificato</span>
</li>
<li>
<input type="checkbox" id="check1" onchange="updateProgress()">
<span>Applicazione ERP ripristinata e configurata</span>
</li>
<li>
<input type="checkbox" id="check2" onchange="updateProgress()">
<span>Database ripristinato e verificato integrità</span>
</li>
<li>
<input type="checkbox" id="check3" onchange="updateProgress()">
<span>Patch di sicurezza applicati (ultimi aggiornamenti)</span>
</li>
<li>
<input type="checkbox" id="check4" onchange="updateProgress()">
<span>Configurazione hardening applicata (baseline CIS)</span>
</li>
</ul>
<h3 style="font-size: 16px; font-weight: 600; margin: 24px 0 16px; color: var(--text-primary);">3. Verifica Integrità e Sicurezza</h3>
<ul class="checklist">
<li>
<input type="checkbox" id="check5" onchange="updateProgress()">
<span>Integrità dati verificata (confronto checksum con backup)</span>
</li>
<li>
<input type="checkbox" id="check6" onchange="updateProgress()">
<span>Test funzionali applicativi completati con successo</span>
</li>
<li>
<input type="checkbox" id="check7" onchange="updateProgress()">
<span>Scansione antimalware completa (nessuna minaccia rilevata)</span>
</li>
<li>
<input type="checkbox" id="check8" onchange="updateProgress()">
<span>Verifica assenza IoC residui</span>
</li>
<li>
<input type="checkbox" id="check9" onchange="updateProgress()">
<span>Configurazione EDR verificata e attiva</span>
</li>
<li>
<input type="checkbox" id="check10" onchange="updateProgress()">
<span>Log attivi e integrati nel SIEM</span>
</li>
</ul>
<h3 style="font-size: 16px; font-weight: 600; margin: 24px 0 16px; color: var(--text-primary);">4. Ripristino Accessi</h3>
<ul class="checklist">
<li>
<input type="checkbox" id="check11" onchange="updateProgress()">
<span>Reset credenziali per tutti gli account potenzialmente compromessi</span>
</li>
<li>
<input type="checkbox" id="check12" onchange="updateProgress()">
<span>MFA verificata e funzionante per tutti gli account admin</span>
</li>
<li>
<input type="checkbox" id="check13" onchange="updateProgress()">
<span>Riattivazione accessi graduale per ruolo (admin → power user → user)</span>
</li>
<li>
<input type="checkbox" id="check14" onchange="updateProgress()">
<span>Monitoraggio accessi post-ripristino attivato</span>
</li>
</ul>
<h3 style="font-size: 16px; font-weight: 600; margin: 24px 0 16px; color: var(--text-primary);">5. Ripristino Connettività</h3>
<ul class="checklist">
<li>
<input type="checkbox" id="check15" onchange="updateProgress()">
<span>Regole firewall aggiornate (IP C2 bloccati permanentemente)</span>
</li>
<li>
<input type="checkbox" id="check16" onchange="updateProgress()">
<span>Segmentazione rete verificata</span>
</li>
<li>
<input type="checkbox" id="check17" onchange="updateProgress()">
<span>Test connettività end-to-end completati</span>
</li>
<li>
<input type="checkbox" id="check18" onchange="updateProgress()">
<span>Monitoraggio traffico anomalo attivo</span>
</li>
</ul>
<h3 style="font-size: 16px; font-weight: 600; margin: 24px 0 16px; color: var(--text-primary);">6. Validazione Finale</h3>
<ul class="checklist">
<li>
<input type="checkbox" id="check19" onchange="updateProgress()">
<span>Sign-off tecnico: Team Incident Response</span>
</li>
<li>
<input type="checkbox" id="check20" onchange="updateProgress()">
<span>Sign-off business: Responsabile Divisione</span>
</li>
<li>
<input type="checkbox" id="check21" onchange="updateProgress()">
<span>Sign-off sicurezza: CISO</span>
</li>
<li>
<input type="checkbox" id="check22" onchange="updateProgress()">
<span>Dichiarazione di ripristino completato</span>
</li>
</ul>
<div style="margin-top: 24px; padding: 20px; background: var(--bg-tertiary); border-radius: 6px; display: flex; justify-content: space-between; align-items: center;">
<div>
<div style="font-size: 13px; color: var(--text-secondary); margin-bottom: 8px;">Progresso Ripristino</div>
<div style="font-size: 24px; font-weight: 700; color: var(--text-primary);" id="progressText">9%</div>
</div>
<button class="btn btn-success" onclick="completeRecovery()" id="completeBtn" disabled style="opacity: 0.5;">
✅ Completa Ripristino
</button>
</div>
</div>
<!-- Monitoraggio Post-Ripristino -->
<div class="section">
<div class="section-title">Piano Monitoraggio Post-Ripristino</div>
<div style="background: var(--bg-tertiary); padding: 16px; border-radius: 6px; margin-bottom: 16px;">
<h4 style="font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 12px;">
📊 Monitoraggio Intensivo (72 ore)
</h4>
<ul style="font-size: 13px; color: var(--text-secondary); line-height: 1.8; margin-left: 20px;">
<li>Monitoraggio continuo 24/7 da SOC</li>
<li>Soglie di alerting ridotte (maggiore sensibilità)</li>
<li>Threat hunting mirato ogni 8 ore</li>
<li>Report giornaliero stato sistema</li>
<li>Verifica integrità file system ogni 12 ore</li>
</ul>
</div>
<div style="background: var(--bg-tertiary); padding: 16px; border-radius: 6px; margin-bottom: 16px;">
<h4 style="font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 12px;">
📈 Monitoraggio Elevato (30 giorni)
</h4>
<ul style="font-size: 13px; color: var(--text-secondary); line-height: 1.8; margin-left: 20px;">
<li>Monitoraggio continuo con priorità alta</li>
<li>Threat hunting settimanale</li>
<li>Report settimanale a CISO</li>
<li>Verifica periodica IoC</li>
<li>Analisi comportamentale utenti e processi</li>
</ul>
</div>
<table class="data-table">
<thead>
<tr>
<th>Metrica</th>
<th>Baseline Pre-Incidente</th>
<th>Target Post-Ripristino</th>
<th>Frequenza Check</th>
</tr>
</thead>
<tbody>
<tr>
<td>CPU Usage</td>
<td>45-60%</td>
<td>45-60% (±5%)</td>
<td>Ogni 5 minuti</td>
</tr>
<tr>
<td>Memory Usage</td>
<td>70-75%</td>
<td>70-75% (±5%)</td>
<td>Ogni 5 minuti</td>
</tr>
<tr>
<td>Network Traffic</td>
<td>2-5 Gbps</td>
<td>2-5 Gbps</td>
<td>Continuo</td>
</tr>
<tr>
<td>Failed Login Attempts</td>
<td><5/ora</td>
<td><3/ora</td>
<td>Real-time</td>
</tr>
<tr>
<td>Processi Anomali</td>
<td>0</td>
<td>0</td>
<td>Ogni 15 minuti</td>
</tr>
<tr>
<td>Connessioni Esterne Sospette</td>
<td>0</td>
<td>0</td>
<td>Real-time</td>
</tr>
</tbody>
</table>
</div>
<!-- Comunicazioni Ripristino -->
<div class="section">
<div class="section-title">Comunicazioni Ripristino Completato</div>
<div style="background: var(--bg-tertiary); padding: 16px; border-radius: 6px; margin-bottom: 16px;">
<h4 style="font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 12px;">
📧 Template Comunicazione Interna
</h4>
<div style="font-size: 13px; color: var(--text-secondary); line-height: 1.8; font-family: monospace; background: var(--bg-primary); padding: 12px; border-radius: 4px;">
<strong>Oggetto:</strong> Ripristino Servizio ERP Completato<br><br>
Gentili colleghi,<br><br>
Vi informiamo che il servizio ERP è stato completamente ripristinato e
risulta nuovamente operativo a partire dalle ore [DATA/ORA].<br><br>
<strong>Azioni richieste agli utenti:</strong><br>
• Effettuare reset password al primo accesso<br>
• Verificare che MFA sia attiva<br>
• Segnalare immediatamente qualsiasi comportamento anomalo<br><br>
Il sistema è sotto monitoraggio intensivo per le prossime 72 ore.<br><br>
Per qualsiasi problema contattare l'helpdesk.<br><br>
Grazie per la collaborazione.<br>
IT Security Team
</div>
</div>
<div style="background: var(--bg-tertiary); padding: 16px; border-radius: 6px;">
<h4 style="font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 12px;">
📡 Aggiornamento CSIRT Italia
</h4>
<div style="font-size: 13px; color: var(--text-secondary); line-height: 1.8;">
Aggiornare la notifica CSIRT con informazioni sul ripristino completato:
<ul style="margin-left: 20px; margin-top: 8px;">
<li>Data/ora ripristino completato</li>
<li>Servizi ripristinati</li>
<li>Downtime totale effettivo vs RTO</li>
<li>Misure di hardening applicate</li>
<li>Piano di monitoraggio post-ripristino</li>
</ul>
</div>
</div>
</div>
</div>
<script>
function updateProgress() {
const total = 22;
let checked = 2; // I primi 2 sono già checked
for (let i = 1; i <= 22; i++) {
const checkbox = document.getElementById('check' + i);
if (checkbox && checkbox.checked) {
checked++;
checkbox.parentElement.classList.add('completed');
} else if (checkbox) {
checkbox.parentElement.classList.remove('completed');
}
}
const percentage = Math.round((checked / total) * 100);
document.getElementById('progressText').textContent = percentage + '%';
const completeBtn = document.getElementById('completeBtn');
if (percentage === 100) {
completeBtn.disabled = false;
completeBtn.style.opacity = '1';
} else {
completeBtn.disabled = true;
completeBtn.style.opacity = '0.5';
}
}
function completeRecovery() {
const tooltip = document.createElement('div');
tooltip.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--bg-secondary);
border: 2px solid var(--success);
border-radius: 8px;
padding: 32px;
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
z-index: 10000;
max-width: 600px;
text-align: center;
`;
tooltip.innerHTML = `
<div style="font-size: 48px; margin-bottom: 16px;"></div>
<h2 style="color: var(--success); margin-bottom: 16px;">Ripristino Completato con Successo</h2>
<div style="background: var(--bg-tertiary); padding: 20px; border-radius: 6px; margin-bottom: 24px; text-align: left;">
<p style="font-size: 13px; color: var(--text-secondary); margin-bottom: 12px;"><strong>Riepilogo Ripristino:</strong></p>
<table style="width: 100%; font-size: 13px; color: var(--text-secondary);">
<tr>
<td><strong>Sistema:</strong></td>
<td>ERP-PROD-01</td>
</tr>
<tr>
<td><strong>Data ripristino:</strong></td>
<td>${new Date().toLocaleString('it-IT')}</td>
</tr>
<tr>
<td><strong>Downtime totale:</strong></td>
<td>30h 15m</td>
</tr>
<tr>
<td><strong>RTO dichiarato:</strong></td>
<td>≤48h</td>
</tr>
<tr>
<td><strong>Conformità RTO:</strong></td>
<td style="color: var(--success); font-weight: 600;">✅ Rispettato</td>
</tr>
<tr>
<td><strong>TTR (Time to Recover):</strong></td>
<td>30.25h</td>
</tr>
</table>
<p style="font-size: 13px; color: var(--text-secondary); margin-top: 16px;">
<strong>Stato:</strong> Sistema operativo, monitoraggio intensivo attivo per 72 ore.
</p>
</div>
<div style="background: var(--bg-tertiary); padding: 16px; border-radius: 6px; margin-bottom: 24px; text-align: left;">
<p style="font-size: 13px; color: var(--text-secondary); margin-bottom: 12px;"><strong>Prossime azioni:</strong></p>
<ul style="font-size: 13px; color: var(--text-secondary); margin-left: 20px;">
<li>Comunicazione utenti inviata</li>
<li>Aggiornamento notifica CSIRT</li>
<li>Monitoraggio intensivo 72h avviato</li>
<li>Pianificazione Post-Incident Review</li>
</ul>
</div>
<div style="display: flex; gap: 12px;">
<button onclick="window.location.href='incident-detail.html?id=INC-2024-047'" style="flex: 1; padding: 12px; background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 6px; color: var(--text-primary); font-weight: 600; cursor: pointer;">
Torna all'Incidente
</button>
<button onclick="window.location.href='incident-pir.html?id=INC-2024-047'" style="flex: 1; padding: 12px; background: var(--accent-primary); border: none; border-radius: 6px; color: white; font-weight: 600; cursor: pointer;">
Avvia Post-Incident Review
</button>
</div>
`;
document.body.appendChild(tooltip);
}
function viewRecoveryDetails(system) {
alert('Dettagli processo di ripristino per: ' + system);
}
// Inizializza progresso
window.addEventListener('DOMContentLoaded', updateProgress);
</script>
</body>
</html>

View File

@ -0,0 +1,61 @@
-- NIS2 Migration 012: Consulting Firms (Studio di Consulenza)
-- Database: nis2_agile_db
-- Data: 2026-04-11
-- Idempotente: rieseguibile in sicurezza.
USE nis2_agile_db;
-- 1. Anagrafica studi di consulenza cybersecurity / NIS2 (idempotente)
CREATE TABLE IF NOT EXISTS consulting_firms (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL COMMENT 'Ragione sociale studio',
vat_number VARCHAR(20) NULL,
fiscal_code VARCHAR(16) NULL,
forma_giuridica VARCHAR(50) NULL,
address VARCHAR(255) NULL,
city VARCHAR(100) NULL,
province VARCHAR(2) NULL,
cap VARCHAR(5) NULL,
phone VARCHAR(30) NULL,
pec VARCHAR(255) NULL,
website VARCHAR(255) NULL,
plan ENUM('starter','professional','enterprise') NOT NULL DEFAULT 'professional',
max_organizations INT NOT NULL DEFAULT 50,
max_users INT NOT NULL DEFAULT 5,
status ENUM('active','trial','suspended','inactive') NOT NULL DEFAULT 'active',
created_by INT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_vat (vat_number),
INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 2. ALTER users.consulting_firm_id (idempotente)
SET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND COLUMN_NAME = 'consulting_firm_id');
SET @sql := IF(@col = 0,
'ALTER TABLE users ADD COLUMN consulting_firm_id INT NULL AFTER role',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @idx := (SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND INDEX_NAME = 'idx_user_firm');
SET @sql := IF(@idx = 0,
'ALTER TABLE users ADD INDEX idx_user_firm (consulting_firm_id)',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 3. ALTER organizations.consulting_firm_id (idempotente)
SET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'organizations' AND COLUMN_NAME = 'consulting_firm_id');
SET @sql := IF(@col = 0,
'ALTER TABLE organizations ADD COLUMN consulting_firm_id INT NULL',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @idx := (SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'organizations' AND INDEX_NAME = 'idx_org_firm');
SET @sql := IF(@idx = 0,
'ALTER TABLE organizations ADD INDEX idx_org_firm (consulting_firm_id)',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;

View File

@ -0,0 +1,41 @@
-- NIS2 Migration 013: Firm-Organization assignments + KB tracking
-- Database: nis2_agile_db
-- Data: 2026-04-11
USE nis2_agile_db;
-- 1. Mapping firm -> organization (M:N)
CREATE TABLE IF NOT EXISTS firm_org_assignments (
id INT AUTO_INCREMENT PRIMARY KEY,
consulting_firm_id INT NOT NULL,
organization_id INT NOT NULL,
assigned_to INT NOT NULL DEFAULT 0
COMMENT '0=tutti i membri del firm, user_id=membro specifico',
assigned_by INT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_firm_org_user (consulting_firm_id, organization_id, assigned_to),
INDEX idx_firm (consulting_firm_id),
INDEX idx_org (organization_id),
INDEX idx_assigned (assigned_to)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 2. Registro documenti uploadati nella KB (audit + listing)
CREATE TABLE IF NOT EXISTS kb_uploaded_documents (
id INT AUTO_INCREMENT PRIMARY KEY,
qdrant_doc_uuid VARCHAR(64) NOT NULL COMMENT 'Identifica il gruppo di chunk in Qdrant (per delete-by-filter)',
scope ENUM('SYSTEM','FIRM','ORG') NOT NULL DEFAULT 'SYSTEM',
consulting_firm_id INT NULL,
organization_id INT NULL,
uploaded_by INT NOT NULL,
title VARCHAR(255) NOT NULL,
entity_type VARCHAR(64) NULL,
source VARCHAR(255) NULL,
lang VARCHAR(5) NOT NULL DEFAULT 'it',
chunk_count INT NOT NULL DEFAULT 0,
shared_with_orgs JSON NULL COMMENT 'Array di organization_id quando scope=FIRM',
status ENUM('processing','ready','failed') NOT NULL DEFAULT 'ready',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_scope_firm (scope, consulting_firm_id),
INDEX idx_scope_org (scope, organization_id),
INDEX idx_uuid (qdrant_doc_uuid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@ -0,0 +1,66 @@
-- ============================================================================
-- Migration 020 - Asset Relevance Scoring (NIS2 GV.OC-04)
-- ----------------------------------------------------------------------------
-- Aggiunge la metodologia di scoring rilevanza NIS2 (0-100, 6 criteri pesati)
-- alla tabella assets. Adattata dai mockup docs/nis2/assets.html +
-- doc-relevant-systems.html (Determina/metodologia CdA, soglia >=40 rilevante).
--
-- Criteri: C1 Criticita Operativa (0-25), C2 Impatto Interruzione (0-25),
-- C3 Dati Trattati (0-20), C4 Dipendenze (0-15),
-- C5 Esposizione (0-10), C6 Obblighi Normativi (0-5).
-- Classificazione: >=80 critico | 60-79 alto | 40-59 medio | 20-39 basso | <20 trascurabile
-- Rilevanza NIS2: score >= 40.
--
-- IMPORTANTE (vedi CLAUDE.md / memoria): MySQL 8 Ubuntu NON supporta
-- "ADD COLUMN IF NOT EXISTS". Questo script usa una stored procedure idempotente
-- che verifica information_schema prima di ogni ALTER. Rilanciabile senza danni.
-- Eseguire con: mysql -h localhost nis2_agile_db -e "source docs/sql/020_asset_relevance.sql"
-- ============================================================================
DELIMITER //
DROP PROCEDURE IF EXISTS _mig020_add_col //
CREATE PROCEDURE _mig020_add_col(IN col VARCHAR(64), IN ddl TEXT)
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'assets' AND COLUMN_NAME = col
) THEN
SET @sql = CONCAT('ALTER TABLE assets ADD COLUMN ', ddl);
PREPARE st FROM @sql; EXECUTE st; DEALLOCATE PREPARE st;
END IF;
END //
DELIMITER ;
CALL _mig020_add_col('relevance_score', "relevance_score TINYINT UNSIGNED NULL COMMENT 'Punteggio rilevanza NIS2 0-100'");
CALL _mig020_add_col('relevance_criteria', "relevance_criteria JSON NULL COMMENT 'Dettaglio punteggi C1-C6 per audit'");
CALL _mig020_add_col('relevance_class', "relevance_class ENUM('critico','alto','medio','basso','trascurabile') NULL");
CALL _mig020_add_col('is_nis2_relevant', "is_nis2_relevant TINYINT(1) NOT NULL DEFAULT 0 COMMENT '1 se score >= 40'");
CALL _mig020_add_col('relevance_assessed_at', "relevance_assessed_at DATETIME NULL");
CALL _mig020_add_col('relevance_assessed_by', "relevance_assessed_by INT NULL");
DROP PROCEDURE IF EXISTS _mig020_add_col;
-- Indice per filtri "sistemi rilevanti" (idempotente via check)
DELIMITER //
DROP PROCEDURE IF EXISTS _mig020_add_idx //
CREATE PROCEDURE _mig020_add_idx()
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'assets' AND INDEX_NAME = 'idx_relevance'
) THEN
ALTER TABLE assets ADD INDEX idx_relevance (is_nis2_relevant, relevance_score);
END IF;
END //
DELIMITER ;
CALL _mig020_add_idx();
DROP PROCEDURE IF EXISTS _mig020_add_idx;
-- ROLLBACK (manuale):
-- ALTER TABLE assets
-- DROP COLUMN relevance_score, DROP COLUMN relevance_criteria,
-- DROP COLUMN relevance_class, DROP COLUMN is_nis2_relevant,
-- DROP COLUMN relevance_assessed_at, DROP COLUMN relevance_assessed_by,
-- DROP INDEX idx_relevance;

View File

@ -0,0 +1,36 @@
-- ============================================================================
-- Migration 021 - Tassonomia Incidenti NIS2 (Determina ACN 164179/2025)
-- ----------------------------------------------------------------------------
-- Aggiunge alla tabella incidents:
-- - nis2_incident_type: tipologia incidente significativo IS-1/IS-2/IS-3/IS-4
-- (Determinazione ACN n. 164179 del 14/04/2025, Allegati 3 e 4).
-- - entity_obligation: regime di obblighi applicabile (essential=Allegato 3,
-- important=Allegato 4). I soggetti importanti NON sono tenuti all'IS-4
-- (incidenti ricorrenti).
--
-- Fonte: D.Lgs. 138/2024 art. 23 + Determina ACN 164179/2025.
-- Idempotente via information_schema. Rilanciabile.
-- mysql -h localhost nis2_agile_db -e "source docs/sql/021_incident_nis2_taxonomy.sql"
-- ============================================================================
DELIMITER //
DROP PROCEDURE IF EXISTS _mig021_add_col //
CREATE PROCEDURE _mig021_add_col(IN col VARCHAR(64), IN ddl TEXT)
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'incidents' AND COLUMN_NAME = col
) THEN
SET @sql = CONCAT('ALTER TABLE incidents ADD COLUMN ', ddl);
PREPARE st FROM @sql; EXECUTE st; DEALLOCATE PREPARE st;
END IF;
END //
DELIMITER ;
CALL _mig021_add_col('nis2_incident_type', "nis2_incident_type ENUM('IS-1','IS-2','IS-3','IS-4') NULL COMMENT 'Tipologia incidente significativo - Determina ACN 164179/2025'");
CALL _mig021_add_col('entity_obligation', "entity_obligation ENUM('essential','important') NULL COMMENT 'Regime obblighi: essential=Allegato 3, important=Allegato 4'");
DROP PROCEDURE IF EXISTS _mig021_add_col;
-- ROLLBACK:
-- ALTER TABLE incidents DROP COLUMN nis2_incident_type, DROP COLUMN entity_obligation;

View File

@ -0,0 +1,73 @@
-- ============================================================================
-- Migration 022 - Metriche Incidente (TTD/TTC/TTR) + Post-Incident Review
-- ----------------------------------------------------------------------------
-- 1) Timestamp di fase su incidents per calcolare le metriche:
-- triaged_at, contained_at, eradicated_at, recovered_at.
-- (la tabella aveva solo detected_at e closed_at)
-- TTD = triaged_at - detected_at (Time to Detect/triage)
-- TTC = contained_at - detected_at (Time to Contain)
-- TTR = recovered_at - detected_at (Time to Recover)
-- 2) Tabella incident_pir: Post-Incident Review strutturato (RC.CO-03 / NIST CSF),
-- con Root Cause Analysis 5-Whys, metriche, costo stimato, lesson learned.
--
-- Idempotente. mysql -h localhost nis2_agile_db -e "source docs/sql/022_incident_metrics_pir.sql"
-- ============================================================================
DELIMITER //
DROP PROCEDURE IF EXISTS _mig022_add_col //
CREATE PROCEDURE _mig022_add_col(IN col VARCHAR(64), IN ddl TEXT)
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'incidents' AND COLUMN_NAME = col
) THEN
SET @sql = CONCAT('ALTER TABLE incidents ADD COLUMN ', ddl);
PREPARE st FROM @sql; EXECUTE st; DEALLOCATE PREPARE st;
END IF;
END //
DELIMITER ;
CALL _mig022_add_col('triaged_at', "triaged_at DATETIME NULL COMMENT 'Inizio triage'");
CALL _mig022_add_col('contained_at', "contained_at DATETIME NULL COMMENT 'Incidente contenuto'");
CALL _mig022_add_col('eradicated_at', "eradicated_at DATETIME NULL COMMENT 'Minaccia eradicata'");
CALL _mig022_add_col('recovered_at', "recovered_at DATETIME NULL COMMENT 'Servizi ripristinati'");
DROP PROCEDURE IF EXISTS _mig022_add_col;
-- Post-Incident Review (1:1 con incident)
CREATE TABLE IF NOT EXISTS incident_pir (
id INT AUTO_INCREMENT PRIMARY KEY,
incident_id INT NOT NULL,
organization_id INT NOT NULL,
-- Root Cause Analysis - 5 Whys
problem_statement TEXT,
why_1 TEXT, why_2 TEXT, why_3 TEXT, why_4 TEXT, why_5 TEXT,
root_cause TEXT,
-- Metriche (snapshot al momento della review, in minuti)
ttd_minutes INT NULL,
ttc_minutes INT NULL,
ttr_minutes INT NULL,
downtime_minutes INT NULL,
affected_users INT NULL,
estimated_cost_eur DECIMAL(12,2) NULL,
notification_compliance TINYINT(1) NULL COMMENT '1 se notifiche entro le tempistiche NIS2',
-- Lesson learned & azioni di miglioramento
what_went_well TEXT,
what_to_improve TEXT,
improvement_actions JSON NULL COMMENT 'lista azioni {desc, owner, due_date, status}',
participants TEXT,
reviewed_by INT NULL,
reviewed_at DATETIME NULL,
status ENUM('draft','completed') DEFAULT 'draft',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uniq_incident (incident_id),
INDEX idx_org (organization_id),
CONSTRAINT fk_pir_incident FOREIGN KEY (incident_id) REFERENCES incidents(id) ON DELETE CASCADE,
CONSTRAINT fk_pir_org FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ROLLBACK:
-- DROP TABLE IF EXISTS incident_pir;
-- ALTER TABLE incidents DROP COLUMN triaged_at, DROP COLUMN contained_at,
-- DROP COLUMN eradicated_at, DROP COLUMN recovered_at;

View File

@ -0,0 +1,128 @@
-- ═══════════════════════════════════════════════════════════════════════════
-- SEED DEMO: Agile Technology SRL (consulting firm) + 3 aziende clienti
-- Database : nis2_agile_db
-- Data : 2026-05-29
-- Scope : ambiente DEV/test. Idempotente (rieseguibile via ON DUPLICATE).
-- Rollback : vedi sezione finale (commentata) per cancellazione totale.
--
-- Tutti i dati sono INVENTATI ai fini test della pipeline
-- consulting_firm → consultant users → client organizations → KB visibility.
-- ═══════════════════════════════════════════════════════════════════════════
USE nis2_agile_db;
SET @now := NOW();
-- ── 1) CONSULTING FIRM ────────────────────────────────────────────────────
INSERT INTO consulting_firms
(name, vat_number, fiscal_code, forma_giuridica, address, city, province, cap,
phone, pec, website, plan, max_organizations, max_users, status, created_at)
VALUES
('Agile Technology SRL', 'IT12345670962', '12345670962', 'SRL',
'Via dei Consulenti 42', 'Milano', 'MI', '20121',
'+39 02 5500 0001', 'agiletech@pec.it', 'https://agile.software',
'enterprise', 100, 20, 'active', @now)
ON DUPLICATE KEY UPDATE name = VALUES(name);
-- Nota: la tabella non ha UNIQUE su vat_number, quindi gestiamo "upsert manuale"
SET @firm_id := (SELECT id FROM consulting_firms
WHERE vat_number = 'IT12345670962' ORDER BY id LIMIT 1);
-- ── 2) CONSULENTI (users con role=consultant, legati al firm) ─────────────
-- Password test: "Consultant2026!" (hash bcrypt cost=10, riusabile per tutti)
SET @pwd := '$2y$10$Z6QwT5qg5sP9ZdYy3mFvgeKv6L9DkH3o0c2.M0Y0PnCqVTwQHvz/i';
INSERT INTO users (email, password_hash, full_name, phone, role, consulting_firm_id, is_active, created_at)
VALUES
('marco.ferri@agiletech.demo', @pwd, 'Marco Ferri', '+39 333 1112201', 'consultant', @firm_id, 1, @now),
('laura.greco@agiletech.demo', @pwd, 'Laura Greco', '+39 333 1112202', 'consultant', @firm_id, 1, @now),
('paolo.rossi@agiletech.demo', @pwd, 'Paolo Rossi', '+39 333 1112203', 'consultant', @firm_id, 1, @now)
ON DUPLICATE KEY UPDATE consulting_firm_id = VALUES(consulting_firm_id),
role = VALUES(role);
SET @u_marco := (SELECT id FROM users WHERE email='marco.ferri@agiletech.demo');
SET @u_laura := (SELECT id FROM users WHERE email='laura.greco@agiletech.demo');
SET @u_paolo := (SELECT id FROM users WHERE email='paolo.rossi@agiletech.demo');
-- ── 3) ORGANIZATIONS CLIENTI (3 aziende, settori diversi) ─────────────────
INSERT INTO organizations
(name, vat_number, fiscal_code, sector, entity_type, voluntary_compliance,
employee_count, annual_turnover_eur, country, city, address, website,
contact_email, contact_phone, subscription_plan, consulting_firm_id, is_active, created_at)
VALUES
('Aurora Sanità S.p.A.', 'IT04501230965', '04501230965', 'health',
'essential', 0, 480, 92000000.00, 'IT', 'Bergamo', 'Via degli Ospedali 8',
'https://aurorasanita.demo', 'compliance@aurorasanita.demo', '+39 035 7001001',
'professional', @firm_id, 1, @now),
('NordWater Utilities S.r.l.', 'IT05512340963', '05512340963', 'water',
'essential', 0, 220, 41500000.00, 'IT', 'Brescia', 'Strada del Depuratore 14',
'https://nordwater.demo', 'security@nordwater.demo', '+39 030 4002002',
'professional', @firm_id, 1, @now),
('Logistica Veloce S.r.l.', 'IT06723450961', '06723450961', 'transport',
'important', 0, 95, 18700000.00, 'IT', 'Verona', 'Viale dei Trasporti 27',
'https://logisticaveloce.demo', 'it@logisticaveloce.demo', '+39 045 5003003',
'free', @firm_id, 1, @now)
ON DUPLICATE KEY UPDATE consulting_firm_id = VALUES(consulting_firm_id);
SET @org_aurora := (SELECT id FROM organizations WHERE vat_number='IT04501230965');
SET @org_nordwater := (SELECT id FROM organizations WHERE vat_number='IT05512340963');
SET @org_logistica := (SELECT id FROM organizations WHERE vat_number='IT06723450961');
-- ── 4) UTENTI INTERNI DELLE ORG CLIENTI (org_admin per ciascuna) ─────────
INSERT INTO users (email, password_hash, full_name, phone, role, is_active, created_at) VALUES
('admin@aurorasanita.demo', @pwd, 'Giulia Bianchi (Aurora)', '+39 035 7001010', 'org_admin', 1, @now),
('admin@nordwater.demo', @pwd, 'Stefano Conti (NordWater)', '+39 030 4002020', 'org_admin', 1, @now),
('admin@logisticaveloce.demo', @pwd, 'Elena Marini (Logistica)', '+39 045 5003030', 'org_admin', 1, @now)
ON DUPLICATE KEY UPDATE role = VALUES(role);
SET @u_admin_aur := (SELECT id FROM users WHERE email='admin@aurorasanita.demo');
SET @u_admin_nw := (SELECT id FROM users WHERE email='admin@nordwater.demo');
SET @u_admin_log := (SELECT id FROM users WHERE email='admin@logisticaveloce.demo');
-- ── 5) user_organizations: org_admin "interno" delle 3 org + consulenti ──
INSERT INTO user_organizations (user_id, organization_id, role, is_primary, joined_at) VALUES
(@u_admin_aur, @org_aurora, 'org_admin', 1, @now),
(@u_admin_nw, @org_nordwater, 'org_admin', 1, @now),
(@u_admin_log, @org_logistica, 'org_admin', 1, @now),
-- Marco è consulente delle 3 org clienti
(@u_marco, @org_aurora, 'consultant', 0, @now),
(@u_marco, @org_nordwater, 'consultant', 0, @now),
(@u_marco, @org_logistica, 'consultant', 0, @now),
-- Laura solo Aurora + NordWater
(@u_laura, @org_aurora, 'consultant', 0, @now),
(@u_laura, @org_nordwater, 'consultant', 0, @now),
-- Paolo solo Logistica
(@u_paolo, @org_logistica, 'consultant', 0, @now)
ON DUPLICATE KEY UPDATE role = VALUES(role);
-- ── 6) firm_org_assignments: mapping firm → org → consulente ─────────────
-- assigned_to = 0 → tutti i membri del firm vedono la org (default)
-- assigned_to = X → solo quel consulente è "lead"
INSERT INTO firm_org_assignments (consulting_firm_id, organization_id, assigned_to, assigned_by, created_at) VALUES
(@firm_id, @org_aurora, 0, @u_marco, @now),
(@firm_id, @org_nordwater, 0, @u_marco, @now),
(@firm_id, @org_logistica, 0, @u_paolo, @now),
-- lead specifici (denota responsabilità primaria)
(@firm_id, @org_aurora, @u_marco, @u_marco, @now),
(@firm_id, @org_nordwater, @u_laura, @u_marco, @now),
(@firm_id, @org_logistica, @u_paolo, @u_paolo, @now)
ON DUPLICATE KEY UPDATE created_at = created_at;
-- ── 7) Riepilogo finale ──────────────────────────────────────────────────
SELECT 'CONSULTING FIRM' AS section, id, name, plan, status FROM consulting_firms WHERE id=@firm_id
UNION ALL SELECT 'CONSULTANT', id, full_name, role, email FROM users WHERE consulting_firm_id=@firm_id
UNION ALL SELECT 'CLIENT ORG', id, name, sector, entity_type FROM organizations WHERE consulting_firm_id=@firm_id
UNION ALL SELECT 'FIRM-ORG MAP', id, CONCAT('firm=', consulting_firm_id), CONCAT('org=', organization_id), CONCAT('assigned_to=', assigned_to) FROM firm_org_assignments WHERE consulting_firm_id=@firm_id;
-- ═══════════════════════════════════════════════════════════════════════════
-- ROLLBACK (decommentare per cancellare TUTTI i dati demo Agile Technology)
-- ═══════════════════════════════════════════════════════════════════════════
-- SET @firm_id := (SELECT id FROM consulting_firms WHERE vat_number='IT12345670962');
-- DELETE FROM firm_org_assignments WHERE consulting_firm_id=@firm_id;
-- DELETE FROM user_organizations WHERE user_id IN (SELECT id FROM users WHERE consulting_firm_id=@firm_id);
-- DELETE FROM user_organizations WHERE organization_id IN (SELECT id FROM organizations WHERE consulting_firm_id=@firm_id);
-- DELETE FROM organizations WHERE consulting_firm_id=@firm_id;
-- DELETE FROM users WHERE consulting_firm_id=@firm_id;
-- DELETE FROM users WHERE email LIKE 'admin@%.demo' AND email IN
-- ('admin@aurorasanita.demo','admin@nordwater.demo','admin@logisticaveloce.demo');
-- DELETE FROM consulting_firms WHERE id=@firm_id;

File diff suppressed because it is too large Load Diff

View File

@ -466,6 +466,7 @@
<th>Tipo</th>
<th>Categoria</th>
<th>Criticita'</th>
<th>Rilevanza NIS2</th>
<th>Owner</th>
<th>Stato</th>
<th>Azioni</th>
@ -482,9 +483,13 @@
<td>${typeLabels[asset.asset_type] || asset.asset_type || '-'}</td>
<td>${escapeHtml(asset.category || '-')}</td>
<td><span class="criticality-badge ${crit}">${criticalityLabels[crit] || crit}</span></td>
<td>${relevanceBadge(asset)}</td>
<td>${escapeHtml(asset.owner_name || '-')}</td>
<td><span class="status-badge ${st}">${statusLabels[st] || st}</span></td>
<td>
<td style="white-space:nowrap;">
<button class="btn-icon" onclick="event.stopPropagation(); showScoringModal(${asset.id})" title="Valuta rilevanza NIS2">
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M3 3a1 1 0 011 1v12h12a1 1 0 110 2H3a1 1 0 01-1-1V4a1 1 0 011-1zm14.707 4.707a1 1 0 00-1.414-1.414L12 10.586 9.707 8.293a1 1 0 00-1.414 0L4.586 12 6 13.414l2.293-2.293L10.586 13l5.121-5.293z" clip-rule="evenodd"/></svg>
</button>
<button class="btn-icon" onclick="event.stopPropagation(); showEditAssetModal(${asset.id})" title="Modifica">
<svg viewBox="0 0 20 20" fill="currentColor"><path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"/></svg>
</button>
@ -496,6 +501,106 @@
container.innerHTML = html;
}
// ── Rilevanza NIS2 (GV.OC-04) ───────────────────────────
const relevanceClassColors = {
critico: '#dc2626', alto: '#ea580c', medio: '#ca8a04',
basso: '#2563eb', trascurabile: '#6b7280'
};
function relevanceBadge(asset) {
if (asset.relevance_score === null || asset.relevance_score === undefined || asset.relevance_score === '') {
return '<span style="color:var(--gray-400); font-size:0.8rem;">Da valutare</span>';
}
const cls = asset.relevance_class || 'trascurabile';
const color = relevanceClassColors[cls] || '#6b7280';
const rel = Number(asset.is_nis2_relevant) ? ' ✓' : '';
return `<span style="display:inline-flex;align-items:center;gap:6px;font-size:0.8rem;font-weight:600;color:${color};">
<span style="display:inline-block;min-width:30px;text-align:center;padding:2px 6px;border-radius:6px;background:${color}1a;">${asset.relevance_score}</span>
${cls.charAt(0).toUpperCase() + cls.slice(1)}${rel}</span>`;
}
let _scoringGrid = null;
async function loadScoringGrid() {
if (_scoringGrid) return _scoringGrid;
const r = await api.getScoringGrid();
if (r.success) _scoringGrid = r.data;
return _scoringGrid;
}
async function showScoringModal(id) {
try {
const [assetRes, grid] = await Promise.all([api.getAsset(id), loadScoringGrid()]);
if (!assetRes.success || !grid) { showNotification('Errore caricamento dati.', 'error'); return; }
const a = assetRes.data;
let prev = a.relevance_criteria;
if (typeof prev === 'string') { try { prev = JSON.parse(prev); } catch (e) { prev = null; } }
let body = `<p style="font-size:0.85rem;color:var(--gray-600);margin-bottom:1rem;">
Metodologia di scoring rilevanza NIS2 (requisito <strong>GV.OC-04</strong>). Soglia rilevanza: <strong>&ge;${grid.threshold} punti</strong>.
Il punteggio aggiorna automaticamente anche la criticita dell'asset.</p>`;
for (const [key, def] of Object.entries(grid.grid)) {
const sel = prev && prev[key] ? prev[key].value : '';
let opts = `<option value="">— seleziona —</option>`;
for (const [ov, od] of Object.entries(def.options)) {
opts += `<option value="${ov}" data-pts="${od.points}" ${ov === sel ? 'selected' : ''}>${od.label} (${od.points})</option>`;
}
body += `<div class="form-group" style="margin-bottom:0.75rem;">
<label class="form-label" style="font-weight:600;">${def.label} <span style="color:var(--gray-400);font-weight:400;">(max ${def.max})</span></label>
<div style="font-size:0.78rem;color:var(--gray-500);margin-bottom:4px;">${def.help}</div>
<select class="form-select score-criterion" data-key="${key}" onchange="updateScorePreview()">${opts}</select>
</div>`;
}
body += `<div id="score-preview" style="margin-top:1rem;padding:0.9rem;border-radius:10px;background:var(--gray-50);font-weight:600;text-align:center;">
Totale: <span id="score-total">0</span>/100 — <span id="score-class"></span></div>`;
showModal(`Valuta Rilevanza NIS2 — ${escapeHtml(a.name)}`, body, {
size: 'lg',
footer: `<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
<button class="btn btn-primary" onclick="submitScoring(${id})">Calcola e Salva</button>`
});
updateScorePreview();
} catch (e) {
showNotification('Errore nell\'apertura della valutazione.', 'error');
}
}
function updateScorePreview() {
let total = 0, complete = true;
document.querySelectorAll('.score-criterion').forEach(s => {
if (!s.value) { complete = false; return; }
total += parseInt(s.selectedOptions[0].dataset.pts || '0', 10);
});
let cls = total >= 80 ? 'critico' : total >= 60 ? 'alto' : total >= 40 ? 'medio' : total >= 20 ? 'basso' : 'trascurabile';
const color = relevanceClassColors[cls];
document.getElementById('score-total').textContent = total;
const clsEl = document.getElementById('score-class');
clsEl.textContent = complete ? `${cls.charAt(0).toUpperCase() + cls.slice(1)}${total >= 40 ? ' — Rilevante NIS2 ✓' : ''}` : '(completa tutti i criteri)';
clsEl.style.color = complete ? color : 'var(--gray-400)';
}
async function submitScoring(id) {
const criteria = {};
let complete = true;
document.querySelectorAll('.score-criterion').forEach(s => {
if (!s.value) complete = false;
criteria[s.dataset.key] = s.value;
});
if (!complete) { showNotification('Compila tutti i 6 criteri.', 'warning'); return; }
try {
const r = await api.scoreAsset(id, criteria);
if (r.success) {
showNotification(`Rilevanza calcolata: ${r.data.score}/100 (${r.data.class}).`, 'success');
closeModal();
loadAssets();
} else {
showNotification(r.message || 'Errore nel calcolo.', 'error');
}
} catch (e) {
showNotification('Errore di connessione.', 'error');
}
}
// ── Asset Detail View ───────────────────────────────────
async function showAssetDetail(id) {
try {
@ -581,6 +686,7 @@
size: 'lg',
footer: `
<button class="btn btn-secondary" onclick="closeModal()">Chiudi</button>
<button class="btn btn-secondary" onclick="closeModal(); showScoringModal(${a.id})">Valuta Rilevanza NIS2</button>
<button class="btn btn-primary" onclick="closeModal(); showEditAssetModal(${a.id})">Modifica</button>
`
});

775
public/guida.html Normal file
View File

@ -0,0 +1,775 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Guida all'uso - NIS2 Agile</title>
<link rel="stylesheet" href="css/style.css">
<style>
/* Stili specifici per la guida — non interferiscono col resto */
.guide-wrap { max-width: 920px; margin: 0 auto; }
.guide-toc {
position: sticky; top: 16px;
background: var(--surface, #fff);
border: 1px solid var(--border, #e5e7eb);
border-radius: 10px; padding: 16px 20px;
margin-bottom: 24px;
}
.guide-toc h4 { margin: 0 0 10px; font-size: 0.9rem; color: var(--text-muted, #6b7280); text-transform: uppercase; letter-spacing: 0.05em; }
.guide-toc ul { list-style: none; padding: 0; margin: 0; columns: 2; column-gap: 24px; }
.guide-toc li { padding: 4px 0; }
.guide-toc a { color: var(--primary, #1e40af); text-decoration: none; font-size: 0.93rem; }
.guide-toc a:hover { text-decoration: underline; }
.guide-section { margin: 40px 0; padding-top: 16px; scroll-margin-top: 16px; }
.guide-section h2 {
color: var(--primary, #1e40af);
border-bottom: 2px solid var(--primary, #1e40af);
padding-bottom: 8px; margin-bottom: 20px;
display: flex; align-items: center; gap: 12px;
}
.guide-section h2 .num {
background: var(--primary, #1e40af); color: #fff;
width: 32px; height: 32px; border-radius: 50%;
display: inline-flex; align-items: center; justify-content: center;
font-size: 0.9rem; font-weight: 700;
}
.guide-section h3 { color: #374151; margin-top: 24px; }
.plain-box {
background: #eff6ff; border-left: 4px solid #1e40af;
padding: 14px 18px; border-radius: 6px; margin: 14px 0;
}
.plain-box strong { color: #1e40af; }
.example-box {
background: #fef3c7; border-left: 4px solid #f59e0b;
padding: 14px 18px; border-radius: 6px; margin: 14px 0;
}
.example-box strong { color: #92400e; }
.norm-box {
background: #f3f4f6; border-left: 4px solid #6b7280;
padding: 14px 18px; border-radius: 6px; margin: 14px 0;
font-size: 0.93rem;
}
.norm-box .article-tag {
display: inline-block; background: #1e40af; color: #fff;
padding: 2px 8px; border-radius: 4px; font-size: 0.78rem;
font-weight: 600; margin-right: 8px;
}
.step-list { counter-reset: step; list-style: none; padding-left: 0; }
.step-list li {
counter-increment: step; padding: 12px 12px 12px 56px;
position: relative; margin: 8px 0;
background: #f9fafb; border-radius: 6px;
}
.step-list li::before {
content: counter(step);
position: absolute; left: 14px; top: 12px;
background: var(--primary, #1e40af); color: #fff;
width: 30px; height: 30px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-weight: 700;
}
.glossario dt {
font-weight: 700; color: var(--primary, #1e40af);
margin-top: 14px; font-size: 1.02rem;
}
.glossario dd { margin-left: 0; margin-bottom: 6px; color: #374151; }
.glossario .acro { font-family: 'Courier New', monospace; background: #e0e7ff; padding: 2px 6px; border-radius: 3px; }
.pillar-card {
border: 1px solid #e5e7eb; border-radius: 8px;
padding: 16px; margin: 10px 0;
background: #fff;
}
.pillar-card h4 {
margin: 0 0 8px; color: var(--primary, #1e40af);
display: flex; align-items: center; gap: 8px;
}
.pillar-card .pillar-num {
background: var(--primary, #1e40af); color: #fff;
padding: 2px 8px; border-radius: 4px; font-size: 0.8rem;
}
@media (max-width: 768px) {
.guide-toc ul { columns: 1; }
}
</style>
</head>
<body>
<div class="app-layout">
<aside class="sidebar" id="sidebar"></aside>
<main class="main-content">
<header class="content-header">
<h2>Guida all'uso di NIS2 Agile</h2>
<div class="content-header-actions">
<span class="text-muted">Per chi inizia da zero</span>
</div>
</header>
<div class="content-body">
<div class="guide-wrap">
<!-- Intro -->
<div class="card">
<div class="card-body">
<p style="font-size:1.05rem; line-height:1.7; margin:0;">
Benvenuto. Questa guida ti accompagna passo passo nell'uso della piattaforma <strong>NIS2 Agile</strong>,
spiegando con parole semplici cosa devi fare e perché — senza dare per scontato che tu sia un esperto
di cybersecurity. Troverai per ogni argomento <strong style="color:#1e40af;">la spiegazione in parole semplici</strong>,
un <strong style="color:#92400e;">esempio pratico</strong> e <strong style="color:#6b7280;">cosa dice la norma</strong>.
</p>
</div>
</div>
<!-- TOC -->
<nav class="guide-toc" aria-label="Indice">
<h4>Indice</h4>
<ul>
<li><a href="#cap-1">1. Cos'è la NIS2 (5 minuti)</a></li>
<li><a href="#cap-2">2. La tua azienda è "in scope"?</a></li>
<li><a href="#cap-3">3. Cosa fa la piattaforma</a></li>
<li><a href="#cap-4">4. Il percorso tipico</a></li>
<li><a href="#cap-5">5. Gap Analysis (Art. 21)</a></li>
<li><a href="#cap-6">6. Gestione dei Rischi</a></li>
<li><a href="#cap-7">7. Incidenti (Art. 23)</a></li>
<li><a href="#cap-8">8. Policy e procedure</a></li>
<li><a href="#cap-9">9. Fornitori (Supply Chain)</a></li>
<li><a href="#cap-10">10. Formazione (Art. 20)</a></li>
<li><a href="#cap-11">11. Asset</a></li>
<li><a href="#cap-12">12. Audit &amp; Report</a></li>
<li><a href="#cap-13">13. Segnalazioni interne</a></li>
<li><a href="#cap-14">14. AI: come usarla</a></li>
<li><a href="#cap-15">15. Glossario</a></li>
</ul>
</nav>
<!-- Cap 1 -->
<section id="cap-1" class="guide-section">
<h2><span class="num">1</span> Cos'è la NIS2 (in 5 minuti)</h2>
<div class="plain-box">
<strong>In parole semplici.</strong> NIS2 è una legge europea (Direttiva UE 2022/2555, recepita in Italia
con il D.Lgs. 138/2024) che obbliga molte aziende a proteggere bene i propri sistemi informatici.
Lo scopo è evitare che attacchi hacker, fughe di dati o blocchi dei sistemi danneggino cittadini,
servizi essenziali (ospedali, energia, acqua, trasporti) e l'economia europea.
</div>
<p>NIS2 stabilisce quattro grandi obblighi:</p>
<ol>
<li><strong>Misurare il rischio cyber</strong> della propria azienda e migliorare le difese (Art. 21).</li>
<li><strong>Segnalare gli incidenti</strong> gravi alle autorità entro tempi precisi (Art. 23).</li>
<li><strong>Formare il personale</strong>, soprattutto i dirigenti (Art. 20).</li>
<li><strong>Controllare i fornitori</strong> che hanno accesso ai propri sistemi (Art. 21, lettera d).</li>
</ol>
<div class="example-box">
<strong>Esempio reale.</strong> Una clinica privata con 250 dipendenti è "in scope" NIS2 perché
opera nel settore sanitario (settore essenziale). Se subisce un ransomware che blocca le cartelle cliniche
per 8 ore, deve notificarlo al CSIRT Italia entro 24 ore con una prima segnalazione e poi entro 72 ore
con i dettagli completi.
</div>
<div class="norm-box">
<span class="article-tag">Considerando UE</span>
La direttiva sostituisce la "NIS1" del 2016 ampliando settori coinvolti, inasprendo le sanzioni
(fino al 2% del fatturato globale o 10 milioni €) e introducendo la responsabilità diretta degli
organi di vertice (amministratori) sulla cybersecurity.
</div>
</section>
<!-- Cap 2 -->
<section id="cap-2" class="guide-section">
<h2><span class="num">2</span> La tua azienda è "in scope"?</h2>
<div class="plain-box">
<strong>In parole semplici.</strong> "In scope" vuol dire che la legge si applica a te. Dipende da
<em>cosa fai</em> (settore) e <em>quanto sei grande</em> (dipendenti e fatturato).
</div>
<h3>I settori coinvolti</h3>
<p>Sono divisi in due gruppi:</p>
<div class="pillar-card">
<h4>Settori <strong style="color:#dc2626;">essenziali</strong> (controlli più stringenti)</h4>
<p>Energia, trasporti, banche, finanza, sanità, acqua potabile e reflue, infrastrutture digitali
(data center, DNS, TLD), Pubblica Amministrazione, spazio.</p>
</div>
<div class="pillar-card">
<h4>Settori <strong style="color:#f59e0b;">importanti</strong> (controlli normali)</h4>
<p>Servizi postali, gestione rifiuti, chimica, alimentare, manifattura (apparecchi medici, computer,
veicoli, ecc.), provider digitali (motori di ricerca, social, marketplace), ricerca.</p>
</div>
<h3>Le soglie dimensionali</h3>
<ul>
<li><strong>Media impresa</strong> = 50249 dipendenti <em>oppure</em> fatturato tra 10 e 50 milioni €.</li>
<li><strong>Grande impresa</strong> = ≥250 dipendenti <em>oppure</em> fatturato &gt;50 milioni €.</li>
</ul>
<p>In generale: medie e grandi imprese nei settori sopra elencati sono in scope. Le piccole sono in scope
solo in casi particolari (es. fornitori di servizi DNS, TLD, registrar).</p>
<div class="example-box">
<strong>Esempio pratico.</strong> "Aurora Sanità S.p.A." con 480 dipendenti e 92 milioni € di fatturato,
settore sanitario → <strong>essenziale + grande</strong> → pienamente in scope. Deve registrarsi sul portale
ACN (Agenzia per la Cybersicurezza Nazionale) entro le scadenze.
</div>
<div class="norm-box">
<span class="article-tag">Art. 2 + Allegati I e II</span>
L'ambito di applicazione è definito dagli articoli 2 della Direttiva e specificato negli Allegati I (settori
essenziali) e II (settori importanti) del D.Lgs. 138/2024.
</div>
</section>
<!-- Cap 3 -->
<section id="cap-3" class="guide-section">
<h2><span class="num">3</span> Cosa fa la piattaforma NIS2 Agile</h2>
<p>La piattaforma ti aiuta a fare <strong>tutto quello che la NIS2 chiede</strong>, in modo organizzato e
documentabile. Non sostituisce il giudizio di un consulente o un CISO, ma ti dà gli strumenti per:</p>
<ul>
<li>Capire <strong>quanto sei conforme</strong> oggi (Gap Analysis con 80 domande).</li>
<li>Tenere un <strong>registro dei rischi</strong> aggiornato.</li>
<li>Gestire gli <strong>incidenti</strong> con i moduli di notifica già pronti per il CSIRT.</li>
<li>Generare <strong>policy</strong> di sicurezza usando l'AI.</li>
<li>Monitorare i <strong>fornitori critici</strong>.</li>
<li>Pianificare la <strong>formazione</strong> dei dipendenti.</li>
<li>Inventariare gli <strong>asset</strong> critici.</li>
<li>Estrarre <strong>report</strong> per audit interni o per le autorità.</li>
</ul>
</section>
<!-- Cap 4 -->
<section id="cap-4" class="guide-section">
<h2><span class="num">4</span> Il percorso tipico (cosa fare per primo)</h2>
<p>Se è la tua prima volta, segui questi passi in ordine:</p>
<ol class="step-list">
<li><strong>Completa l'Onboarding</strong> — inserisci i dati aziendali (puoi caricare la visura
e l'AI estrae i dati automaticamente). Classifica la tua azienda come essenziale/importante.</li>
<li><strong>Fai un primo Assessment (Gap Analysis)</strong> — rispondi alle 80 domande, anche se
in più sessioni. Otterrai un punteggio di maturità complessiva.</li>
<li><strong>Crea il Risk Register</strong> — parti dai rischi più ovvi (ransomware, phishing,
guasto sistemi). L'AI può suggerirti rischi tipici del tuo settore.</li>
<li><strong>Inserisci gli asset critici</strong> — i sistemi/dati senza i quali l'azienda si ferma.</li>
<li><strong>Mappa i fornitori critici</strong> — quelli con accesso ai tuoi sistemi o dati.</li>
<li><strong>Genera/approva le policy fondamentali</strong> — usa l'AI per le bozze, poi rivedile.</li>
<li><strong>Pianifica la formazione</strong> — soprattutto per i dirigenti (obbligo Art. 20).</li>
<li><strong>Quando arriva un incidente</strong> — usalo dal modulo Incidenti per gestire la notifica
24h/72h/30d.</li>
<li><strong>Genera il report esecutivo</strong> dalla sezione Report — utile per il board.</li>
</ol>
<div class="example-box">
<strong>Tempi indicativi.</strong> Un primo ciclo "decente" si fa in 68 settimane di lavoro
spalmate, con 2 persone (un IT e un compliance). Non puntare alla perfezione subito: meglio
coprire <em>tutte</em> le 10 categorie al 50% che 3 al 100% e 7 a zero.
</div>
</section>
<!-- Cap 5 -->
<section id="cap-5" class="guide-section">
<h2><span class="num">5</span> Gap Analysis — le 10 misure dell'Art. 21</h2>
<div class="plain-box">
<strong>In parole semplici.</strong> L'Art. 21 elenca 10 famiglie di "cose che devi avere".
La Gap Analysis ti fa domande per ognuna e calcola quanto sei attrezzato. Non serve essere
perfetti — serve <strong>sapere dove sei messo male</strong> e <strong>aver iniziato a migliorare</strong>.
</div>
<p>Le 10 misure (semplificate):</p>
<div class="pillar-card">
<h4><span class="pillar-num">a)</span> Politiche di analisi del rischio</h4>
<p><em>Hai scritto come la tua azienda gestisce il rischio cyber?</em> Ti serve almeno una policy
approvata dall'amministratore delegato che descriva il metodo (es. ISO 27005).</p>
</div>
<div class="pillar-card">
<h4><span class="pillar-num">b)</span> Gestione degli incidenti</h4>
<p><em>Sai cosa fare quando succede qualcosa?</em> Devi avere una procedura scritta che dica:
chi viene chiamato, in che ordine, chi notifica al CSIRT, chi parla con i giornalisti.</p>
</div>
<div class="pillar-card">
<h4><span class="pillar-num">c)</span> Continuità operativa e gestione delle crisi</h4>
<p><em>Se cade un server, in quanto tempo riparti?</em> RTO (tempo per ripartire) e RPO (quanti dati
puoi perdere) vanno definiti e <strong>testati</strong> almeno una volta l'anno.</p>
</div>
<div class="pillar-card">
<h4><span class="pillar-num">d)</span> Sicurezza della supply chain</h4>
<p><em>I tuoi fornitori sono sicuri?</em> Manda loro un questionario sicurezza,
chiedi le loro certificazioni, metti clausole NIS2 nei contratti.</p>
</div>
<div class="pillar-card">
<h4><span class="pillar-num">e)</span> Sicurezza nell'acquisto, sviluppo e manutenzione</h4>
<p><em>Quando comprate software, è sicuro?</em> Patch management, test prima del go-live, gestione
vulnerabilità (CVE).</p>
</div>
<div class="pillar-card">
<h4><span class="pillar-num">f)</span> Politiche di valutazione dell'efficacia</h4>
<p><em>Misuri se le tue misure funzionano?</em> Audit periodici (interni o esterni), KPI cyber,
revisione annuale del SoA (Statement of Applicability).</p>
</div>
<div class="pillar-card">
<h4><span class="pillar-num">g)</span> Igiene cibernetica di base e formazione</h4>
<p><em>I tuoi dipendenti sanno riconoscere un phishing?</em> Password manager, MFA, formazione
almeno annuale per tutti i livelli.</p>
</div>
<div class="pillar-card">
<h4><span class="pillar-num">h)</span> Crittografia</h4>
<p><em>I dati sensibili sono cifrati?</em> AES-256 per i dati a riposo, TLS 1.3 per i dati in
transito. Gestione corretta delle chiavi.</p>
</div>
<div class="pillar-card">
<h4><span class="pillar-num">i)</span> Sicurezza del personale, controllo accessi e gestione asset</h4>
<p><em>Chi può accedere a cosa?</em> Principio del minimo privilegio, revisione accessi 2 volte
l'anno, offboarding rapido quando un dipendente esce.</p>
</div>
<div class="pillar-card">
<h4><span class="pillar-num">j)</span> Autenticazione a più fattori (MFA)</h4>
<p><em>Hai MFA almeno su email, VPN, amministrazione?</em> Obbligatorio per tutti gli accessi
critici. Anche SMS è meglio di nulla, app authenticator è meglio di SMS, hardware token (FIDO2)
è il top.</p>
</div>
<div class="example-box">
<strong>Esempio Aurora Sanità.</strong> Punteggio iniziale 58%. I gap principali sono in
Supply Chain (40%) e Crittografia (50%). Piano di azione: assessment dei 12 fornitori critici
entro 6 mesi, attivare TLS 1.3 ovunque e cifratura at-rest sui DB cartelle cliniche entro 9 mesi.
</div>
<h3>Come si fa nella piattaforma</h3>
<ol class="step-list">
<li>Vai su <strong>Gap Analysis</strong> e clicca "Nuovo Assessment".</li>
<li>Rispondi alle 80 domande (sei modalità: implementato / parziale / non implementato / non applicabile).</li>
<li>Per ogni risposta, indica il <strong>livello di maturità</strong> (15).</li>
<li>Salva: puoi continuare in più sessioni.</li>
<li>Quando finisci, clicca <strong>"Analisi AI"</strong> per ricevere raccomandazioni prioritarie.</li>
</ol>
</section>
<!-- Cap 6 -->
<section id="cap-6" class="guide-section">
<h2><span class="num">6</span> Gestione dei Rischi</h2>
<div class="plain-box">
<strong>In parole semplici.</strong> Il "registro dei rischi" è un elenco di cose brutte che
<em>potrebbero</em> accadere, con quanto è probabile e quanto farebbe male. Per ognuna decidi
cosa fare: ridurla, accettarla, assicurarla, o eliminarla.
</div>
<h3>La matrice 5×5</h3>
<p>Ogni rischio ha due valori:</p>
<ul>
<li><strong>Probabilità (15)</strong>: 1 = quasi mai, 5 = quasi certo.</li>
<li><strong>Impatto (15)</strong>: 1 = trascurabile, 5 = catastrofico.</li>
</ul>
<p>Il prodotto (probabilità × impatto) dà il <strong>punteggio di rischio</strong> da 1 a 25.
Sopra 16 è critico, 915 alto, 48 medio, sotto 4 basso.</p>
<h3>Le quattro strategie di trattamento</h3>
<ul>
<li><strong>Mitigare</strong>: riduco probabilità o impatto (es. installo backup offsite per ridurre l'impatto di un ransomware).</li>
<li><strong>Trasferire</strong>: passo il rischio a un altro (es. cyber-insurance).</li>
<li><strong>Accettare</strong>: il rischio è basso, lo accetto consapevolmente (con firma del board).</li>
<li><strong>Evitare</strong>: smetto di fare l'attività che genera il rischio.</li>
</ul>
<div class="example-box">
<strong>Esempio.</strong> Rischio "Ransomware su sistema PACS" (Aurora Sanità) → probabilità 4,
impatto 5 → punteggio inerente 20 (critico). Trattamento: mitigazione = MFA su RDP + backup
immutabili + patch mensili. Dopo le misure: probabilità 2, impatto 4 → punteggio residuo 8 (medio).
</div>
<h3>Come si fa nella piattaforma</h3>
<ol class="step-list">
<li>Vai su <strong>Rischi</strong> e clicca "Nuovo Rischio" (o "AI Suggerisci" per partire dai
rischi tipici del tuo settore).</li>
<li>Compila titolo, descrizione, categoria, minaccia e vulnerabilità.</li>
<li>Imposta probabilità e impatto inerenti (15 ciascuno).</li>
<li>Scegli la strategia di trattamento e descrivi le azioni concrete.</li>
<li>Imposta probabilità e impatto residui (dopo le misure).</li>
<li>Assegna un responsabile e una data di revisione.</li>
</ol>
<div class="norm-box">
<span class="article-tag">Art. 21 (2)(a)</span>
Le politiche di analisi dei rischi e di sicurezza dei sistemi informatici devono essere documentate,
approvate dagli organi di vertice, e aggiornate almeno una volta l'anno.
</div>
</section>
<!-- Cap 7 -->
<section id="cap-7" class="guide-section">
<h2><span class="num">7</span> Incidenti — gli obblighi 24h / 72h / 30d</h2>
<div class="plain-box">
<strong>In parole semplici.</strong> Se hai un incidente "significativo" devi avvisare il CSIRT
(la squadra nazionale di risposta cyber) in tre tappe: una prima allerta entro 24 ore, una
notifica completa entro 72 ore, e un report finale entro 30 giorni.
</div>
<h3>Quando un incidente è "significativo"?</h3>
<p>Almeno uno di questi criteri:</p>
<ul>
<li>Ha colpito <strong>≥ 500 utenti</strong>.</li>
<li>Ha bloccato i servizi per <strong>&gt; 4 ore</strong>.</li>
<li>Ha <strong>impatto transfrontaliero</strong> (altri Stati UE).</li>
<li>È un <strong>cyber attack</strong> intenzionale (ransomware, DDoS, intrusione…).</li>
<li>Ha generato danni economici diretti &gt; <strong>100.000 €</strong>.</li>
</ul>
<h3>La timeline</h3>
<div class="pillar-card">
<h4><span class="pillar-num">24h</span> Early Warning</h4>
<p>Prima segnalazione "veloce". Bastano: cosa è successo, sospetti di malevolenza, impatto preliminare.
Non devi avere ancora tutte le risposte.</p>
</div>
<div class="pillar-card">
<h4><span class="pillar-num">72h</span> Notifica completa</h4>
<p>Aggiornamento dettagliato: indicatori di compromissione, sistemi colpiti, misure di contenimento
applicate, stima impatto.</p>
</div>
<div class="pillar-card">
<h4><span class="pillar-num">30d</span> Final Report</h4>
<p>Analisi completa: root cause, azioni correttive, lezioni apprese, raccomandazioni per il futuro.</p>
</div>
<div class="example-box">
<strong>Esempio.</strong> Aurora Sanità subisce un DDoS sul portale prenotazioni alle 09:00 di lunedì.
Sono colpiti 8.500 utenti per 4 ore → significativo. Workflow: Early Warning entro martedì 09:00 →
Notifica entro giovedì 09:00 → Final Report entro mercoledì 28 (30 giorni dopo).
</div>
<h3>Come si fa nella piattaforma</h3>
<ol class="step-list">
<li>Vai su <strong>Incidenti</strong> e clicca "Registra Incidente".</li>
<li>Compila titolo, classificazione, severità, ora di rilevazione.</li>
<li>Il sistema calcola automaticamente le 3 scadenze (24h/72h/30d).</li>
<li>Quando arriva il momento, usa i bottoni <strong>"Invia Early Warning"</strong>,
<strong>"Invia Notifica"</strong> e <strong>"Invia Final Report"</strong>: le email partono verso
l'indirizzo CSIRT configurato.</li>
<li>Aggiorna lo stato dell'incidente (analisi → contenimento → eradicazione → recovery → chiuso).</li>
<li>Compila root cause e lezioni apprese alla chiusura.</li>
</ol>
<div class="norm-box">
<span class="article-tag">Art. 23</span>
Stabilisce gli obblighi di segnalazione degli incidenti significativi al CSIRT competente. Il mancato
rispetto delle scadenze è sanzionabile fino al 2% del fatturato globale per le entità essenziali.
</div>
</section>
<!-- Cap 8 -->
<section id="cap-8" class="guide-section">
<h2><span class="num">8</span> Policy e procedure</h2>
<div class="plain-box">
<strong>In parole semplici.</strong> Una "policy" è un documento scritto che dice <em>come</em>
si fa una cosa in azienda. Serve sia per fare bene, sia per dimostrare alle autorità che ci hai pensato.
</div>
<p>Le policy minime per NIS2:</p>
<ol>
<li>Politica di Sicurezza delle Informazioni (master).</li>
<li>Procedura di Gestione Incidenti.</li>
<li>Politica di Continuità Operativa (BCP/DR).</li>
<li>Politica di Controllo Accessi.</li>
<li>Politica di Crittografia.</li>
<li>Politica Supply Chain.</li>
<li>Politica Vulnerability Management.</li>
<li>Acceptable Use Policy (per i dipendenti).</li>
</ol>
<div class="example-box">
<strong>Esempio.</strong> Genera con l'AI la bozza della "Politica di Crittografia": clicchi
"Genera con AI", indichi il settore (sanità), l'AI ti propone un documento di 45 pagine con
sezioni standard (algoritmi accettati, gestione chiavi, KMS, audit). Tu rivedi, adatti e fai
approvare dal CDA. Il sistema marca la versione, la data di approvazione e la prossima revisione.
</div>
<h3>Stati di una policy</h3>
<p>Bozza → In revisione → Approvata → Pubblicata → Archiviata (quando sostituita).</p>
</section>
<!-- Cap 9 -->
<section id="cap-9" class="guide-section">
<h2><span class="num">9</span> Supply Chain (Fornitori)</h2>
<div class="plain-box">
<strong>In parole semplici.</strong> Se un tuo fornitore IT viene bucato, l'attaccante può entrare
in casa tua. NIS2 ti obbliga a valutare i fornitori "critici" (quelli con accesso ai tuoi sistemi
o dati sensibili).
</div>
<h3>Cosa fare per ogni fornitore critico</h3>
<ol>
<li>Inserirlo nell'anagrafica con dati di contratto.</li>
<li>Classificare la criticità (low/medium/high/critical).</li>
<li>Inviargli il questionario sicurezza (40+ domande standard).</li>
<li>Verificare la risposta e assegnare un risk score (010).</li>
<li>Rinnovare la valutazione almeno una volta l'anno.</li>
<li>Inserire clausole NIS2 nel contratto (right to audit, notifica incidenti, etc.).</li>
</ol>
<div class="example-box">
<strong>Esempio.</strong> "MedTech Manutenzione PACS S.r.l." ha accesso remoto al PACS di
Aurora Sanità → criticità "critical". Risk score iniziale 6/10 (ha ISO 27001, ma sub-appalto
non chiaro). Azione: clausola contrattuale che vieta sub-appalto senza autorizzazione e
impone notifica incidenti entro 24h.
</div>
</section>
<!-- Cap 10 -->
<section id="cap-10" class="guide-section">
<h2><span class="num">10</span> Formazione (Art. 20)</h2>
<div class="plain-box">
<strong>In parole semplici.</strong> NIS2 è chiara: <em>gli amministratori (CDA, direttori)
devono essere formati sulla cybersecurity</em>. Non è un nice-to-have, è obbligatorio. E sono
loro che rispondono in prima persona se l'azienda non è conforme.
</div>
<h3>Chi formare</h3>
<ol>
<li><strong>Organi di vertice</strong> (CDA, amministratori): formazione specifica annuale.</li>
<li><strong>Personale IT/sicurezza</strong>: formazione tecnica continua.</li>
<li><strong>Tutto il personale</strong>: awareness training (phishing, password, segnalazioni)
almeno annuale.</li>
</ol>
<div class="example-box">
<strong>Esempio.</strong> Aurora Sanità organizza: 1 corso da 4h per CDA, 1 corso da 16h per il team
IT, una sessione obbligatoria di 1h per tutti i dipendenti più simulazioni di phishing trimestrali.
Tutto loggato nel modulo Formazione.
</div>
<h3>Come si fa nella piattaforma</h3>
<ol class="step-list">
<li>Vai su <strong>Formazione</strong> &rarr; "Nuovo Corso".</li>
<li>Definisci titolo, contenuti, durata, ruoli target.</li>
<li>Assegna il corso ai dipendenti (singoli o per gruppo).</li>
<li>Imposta scadenza e prerequisiti (es. obbligatorio prima dell'onboarding).</li>
<li>Monitora il completamento dal cruscotto "Compliance Status".</li>
</ol>
<div class="norm-box">
<span class="article-tag">Art. 20</span>
Gli organi di gestione delle entità essenziali e importanti devono seguire una formazione che
consenta loro di acquisire conoscenze e competenze sufficienti per individuare i rischi e valutare
le pratiche di gestione del rischio di cibersicurezza.
</div>
</section>
<!-- Cap 11 -->
<section id="cap-11" class="guide-section">
<h2><span class="num">11</span> Asset (cosa hai e cosa proteggi)</h2>
<div class="plain-box">
<strong>In parole semplici.</strong> Non puoi proteggere quello che non sai di avere.
L'inventario asset elenca tutti i sistemi, applicazioni, database e infrastrutture critiche.
</div>
<h3>Tipi di asset</h3>
<ul>
<li><strong>Hardware</strong>: server, PLC, dispositivi mobili.</li>
<li><strong>Software</strong>: applicazioni gestionali, SCADA, CRM.</li>
<li><strong>Network</strong>: firewall, router, switch.</li>
<li><strong>Data</strong>: database, file storage, backup.</li>
<li><strong>Service</strong>: portali web, API.</li>
<li><strong>Personnel</strong>: utenti privilegiati, ruoli chiave.</li>
<li><strong>Facility</strong>: data center, sale server.</li>
</ul>
<div class="example-box">
<strong>Esempio.</strong> Aurora Sanità inserisce: il PACS, il database cartelle cliniche,
il portale prenotazioni. Per ognuno: criticità, vendor, location, owner. Quando arriva un incidente
o un rischio, può essere associato a uno o più asset → tracciabilità totale.
</div>
</section>
<!-- Cap 12 -->
<section id="cap-12" class="guide-section">
<h2><span class="num">12</span> Audit &amp; Report</h2>
<div class="plain-box">
<strong>In parole semplici.</strong> Quando le autorità (ACN) verranno a chiederti conto della tua
compliance, devi poter mostrare documenti, evidenze, registri. Qui li trovi tutti.
</div>
<h3>Cosa puoi fare</h3>
<ul>
<li><strong>Controlli</strong>: lista dei 10 controlli Art. 21 + mapping ISO 27001 con stato di
implementazione (0100%).</li>
<li><strong>Evidence Files</strong>: carica documenti probatori (verbali, screenshot, certificati).</li>
<li><strong>Audit Log</strong>: registro immutabile di tutto quello che succede in piattaforma
(utenti, modifiche, accessi). Garantito con catena hash SHA-256.</li>
<li><strong>Report Esecutivo</strong>: PDF/HTML stampabile per il CDA.</li>
<li><strong>Export CSV</strong>: rischi, incidenti, controlli, asset.</li>
</ul>
<div class="example-box">
<strong>Esempio.</strong> Pre-audit ISO 27001: genera il report esecutivo, esporta in CSV
rischi e controlli, scarica l'audit log certificato. L'auditor ha tutto in 5 minuti.
</div>
</section>
<!-- Cap 13 -->
<section id="cap-13" class="guide-section">
<h2><span class="num">13</span> Segnalazioni interne (Whistleblowing)</h2>
<div class="plain-box">
<strong>In parole semplici.</strong> NIS2 incoraggia (e in alcuni casi obbliga) ad avere un canale
anonimo dove dipendenti e collaboratori possono segnalare problemi di sicurezza senza paura di
ritorsioni.
</div>
<h3>Tipi di segnalazione</h3>
<ul>
<li><strong>Whistleblowing</strong>: anonimo, comportamenti illeciti.</li>
<li><strong>Feedback bug/UX</strong>: segnalazioni operative sulla piattaforma stessa,
classificate dall'AI e (opzionalmente) risolte automaticamente.</li>
</ul>
<div class="example-box">
<strong>Esempio.</strong> Un dipendente nota che le credenziali admin del CRM sono salvate in
un foglio Excel condiviso. Invia una segnalazione anonima → il responsabile compliance la riceve
con codice di tracciamento → il dipendente può seguire lo stato senza rivelare la propria identità.
</div>
</section>
<!-- Cap 14 -->
<section id="cap-14" class="guide-section">
<h2><span class="num">14</span> AI — come usarla bene</h2>
<div class="plain-box">
<strong>In parole semplici.</strong> La piattaforma usa Claude (Anthropic) per assisterti. L'AI
è bravissima a partire dal foglio bianco, ma <strong>non sostituisce</strong> il tuo giudizio:
rivedi sempre quello che produce prima di approvarlo.
</div>
<h3>Dove trovi l'AI</h3>
<ul>
<li><strong>Analisi Assessment</strong>: dopo aver completato la Gap Analysis, genera
un'analisi delle priorità con raccomandazioni.</li>
<li><strong>AI Suggerisci Rischi</strong>: parte dal tuo settore + asset e propone una lista
di rischi tipici.</li>
<li><strong>Genera Policy</strong>: bozza di policy a partire dalla categoria.</li>
<li><strong>Classifica Incidente</strong>: dato un incidente, suggerisce severità e classificazione.</li>
<li><strong>Knowledge Base RAG</strong>: chiedi all'AI partendo dai documenti che hai caricato.</li>
<li><strong>Classificazione Feedback</strong>: tag automatico per le segnalazioni di bug/UX.</li>
</ul>
<div class="example-box">
<strong>Buona pratica.</strong> Quando l'AI suggerisce 8 rischi, non importarli tutti ciecamente.
Scegli i 4 più rilevanti per il tuo contesto, scartane uno che è duplicato di un asset interno,
e personalizza descrizione/probabilità per la tua realtà.
</div>
<h3>Limiti che devi conoscere</h3>
<ul>
<li>L'AI non vede i tuoi dati grezzi: solo metadati anonimizzati (settore, range dipendenti,
categoria asset).</li>
<li>Le bozze di policy <strong>devono</strong> essere riviste da un umano competente prima
della pubblicazione.</li>
<li>L'AI non è un consulente legale né un sostituto del CISO.</li>
</ul>
</section>
<!-- Cap 15 -->
<section id="cap-15" class="guide-section">
<h2><span class="num">15</span> Glossario rapido</h2>
<dl class="glossario">
<dt><span class="acro">ACN</span> Agenzia per la Cybersicurezza Nazionale</dt>
<dd>L'autorità italiana che vigila sulla NIS2 e raccoglie le notifiche di incidenti.</dd>
<dt><span class="acro">CSIRT</span> Computer Security Incident Response Team</dt>
<dd>Squadra nazionale di risposta agli incidenti. Riceve le notifiche 24h/72h/30d.</dd>
<dt><span class="acro">ENISA</span> Agenzia UE per la Cybersicurezza</dt>
<dd>Coordina a livello europeo le linee guida operative NIS2.</dd>
<dt><span class="acro">RTO</span> Recovery Time Objective</dt>
<dd>Quanto tempo massimo puoi stare giù prima di tornare operativo. Esempio: RTO 4 ore.</dd>
<dt><span class="acro">RPO</span> Recovery Point Objective</dt>
<dd>Quanti dati puoi perderti al massimo. Esempio: RPO 1 ora = backup almeno orari.</dd>
<dt><span class="acro">MFA</span> Multi-Factor Authentication</dt>
<dd>Autenticazione a più fattori: password + codice OTP/app/hardware.</dd>
<dt><span class="acro">ISMS</span> Information Security Management System</dt>
<dd>Sistema di gestione della sicurezza delle informazioni (es. ISO 27001).</dd>
<dt><span class="acro">SoA</span> Statement of Applicability</dt>
<dd>Documento che elenca quali controlli ISO 27001 applichi e quali no, con motivazione.</dd>
<dt><span class="acro">BCP / DRP</span> Business Continuity Plan / Disaster Recovery Plan</dt>
<dd>I piani per garantire la continuità operativa e il ripristino dopo un disastro.</dd>
<dt><span class="acro">SCADA</span> Supervisory Control And Data Acquisition</dt>
<dd>Sistemi di controllo industriale (es. impianti di acqua, energia, manifattura).</dd>
<dt><span class="acro">PACS</span> Picture Archiving and Communication System</dt>
<dd>Sistema sanitario che archivia le immagini diagnostiche (TAC, RMN, ecografie).</dd>
<dt><span class="acro">TMS</span> Transport Management System</dt>
<dd>Software che gestisce le spedizioni e la flotta in azienda di logistica.</dd>
<dt><span class="acro">DDoS</span> Distributed Denial of Service</dt>
<dd>Attacco che satura un servizio con traffico inutile per metterlo offline.</dd>
<dt><span class="acro">BEC</span> Business Email Compromise</dt>
<dd>Frode via email che impersona dirigenti per ottenere bonifici fraudolenti.</dd>
<dt><span class="acro">NCR / CAPA</span> Non-Conformity Report / Corrective and Preventive Action</dt>
<dd>Non conformità (es. da audit) e relative azioni correttive/preventive.</dd>
<dt><span class="acro">RAG</span> Retrieval-Augmented Generation</dt>
<dd>Tecnica AI che fa rispondere l'assistente partendo da documenti caricati nella Knowledge Base.</dd>
</dl>
</section>
<div class="card" style="margin-top:40px;">
<div class="card-body" style="text-align:center;">
<p style="margin:0;color:var(--text-muted,#6b7280);">
Per dubbi specifici, in ogni pagina trovi il pulsante <strong>?</strong> in alto a destra
che apre la guida contestuale di quella sezione. Buon lavoro!
</p>
</div>
</div>
</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>
// La guida è accessibile anche da non autenticati (utile per onboarding)
// ma se sei loggato carichi sidebar + i18n normalmente.
if (typeof loadSidebar === 'function') loadSidebar();
if (typeof I18n !== 'undefined' && I18n.init) I18n.init();
</script>
</body>
</html>

1268
public/index-en.html Normal file

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NIS2 Agile — Compliance NIS2 semplificata per aziende e consulenti</title>
<meta name="description" content="Piattaforma SaaS multi-tenant per la compliance alla Direttiva NIS2 (EU 2022/2555) e D.Lgs. 138/2024. Gap analysis AI, risk management, incident response Art.23, policy, formazione.">
<link rel="canonical" href="https://nis2.agile.software/">
<link rel="alternate" hreflang="it" href="https://nis2.agile.software/">
<link rel="alternate" hreflang="en" href="https://nis2.agile.software/index-en.html">
<link rel="alternate" hreflang="x-default" href="https://nis2.agile.software/">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap');
@ -623,6 +627,11 @@
margin-bottom: 16px;
}
/* ── NAV TOGGLE (hamburger mobile ≤768px) ── */
.nav-toggle { display: none; background: none; border: 1px solid rgba(255,255,255,0.15); border-radius: 8px; cursor: pointer; padding: 8px 10px; color: var(--text-white); min-height: 40px; min-width: 40px; }
.nav-toggle:hover { background: rgba(255,255,255,0.08); border-color: var(--cyan); }
.nav-toggle svg { display: block; }
/* ── RESPONSIVE ── */
@media (max-width: 900px) {
nav { padding: 0 20px; }
@ -637,11 +646,28 @@
.steps-wrap::before { display: none; }
.lg231-card { flex-direction: column; padding: 28px; }
.footer-inner { flex-direction: column; align-items: flex-start; }
.nav-actions .btn-ghost { display: none; }
.form-grid { grid-template-columns: 1fr; }
.form-box { padding: 28px 20px; }
}
@media (max-width: 768px) {
.nav-actions { display: none; }
.nav-actions.open {
display: flex; flex-direction: column; align-items: stretch;
position: fixed; top: 64px; left: 0; right: 0;
background: rgba(15,23,42,0.97); backdrop-filter: blur(16px);
padding: 20px; gap: 12px; z-index: 99;
border-bottom: 1px solid var(--border-color);
max-height: calc(100vh - 64px); overflow-y: auto;
}
.nav-actions.open .btn, .nav-actions.open .lang-switch-nis { width: 100%; justify-content: center; }
.nav-actions.open .lang-switch-nis { border-left: none; padding-left: 0; margin-left: 0; }
.nav-actions.open .evix-suite-pill { margin: 0; width: 100%; justify-content: center; }
.nav-toggle { display: inline-flex; align-items: center; justify-content: center; }
}
</style>
<link rel="stylesheet" href="mobile-conversion.css?v=20260519a">
<!-- Plausible Analytics -->
<script defer data-domain="nis2.agile.software" src="https://analytics.agile.software/js/script.js"></script>
</head>
<body>
@ -651,14 +677,27 @@
<div class="nav-icon"><i class="fa-solid fa-shield-halved"></i></div>
<span class="nav-name">NIS2 <span>Agile</span></span>
</a>
<div class="nav-actions">
<a href="https://evix.agile.software" target="_blank" rel="noopener" class="evix-suite-pill" title="Parte della EViX Suite — Scopri tutti i moduli" style="display:inline-flex;align-items:center;gap:8px;padding:7px 14px;background:linear-gradient(135deg,rgba(227,30,36,0.18),rgba(227,30,36,0.08));border:1.5px solid rgba(227,30,36,0.5);color:#FCA5A5;border-radius:100px;font-size:12px;font-weight:700;letter-spacing:0.04em;text-transform:uppercase;text-decoration:none;transition:all .2s;white-space:nowrap;margin-left:auto;margin-right:14px;">
<i class="fa-solid fa-layer-group" style="font-size:11px"></i>
<span class="evix-pill-label">EViX Suite</span>
<i class="fa-solid fa-arrow-up-right-from-square" style="font-size:9px;opacity:.75"></i>
</a>
<div class="nav-actions" id="navActions">
<a href="/login.html" class="btn btn-ghost btn-sm">
<i class="fa-solid fa-right-to-bracket"></i> Accedi
</a>
<a href="#richiedi-accesso" class="btn btn-primary btn-sm">
<i class="fa-solid fa-envelope"></i> Richiedi accesso
</a>
<div class="lang-switch-nis" style="display:flex;align-items:center;gap:4px;padding-left:10px;border-left:1px solid rgba(255,255,255,0.15);margin-left:6px;">
<a href="index.html" class="active" hreflang="it" style="color:#EF4444;text-decoration:none;padding:4px 8px;border-radius:5px;font-weight:700;font-size:12px;letter-spacing:0.04em;background:rgba(239,68,68,0.12);">IT</a>
<span style="color:rgba(255,255,255,0.3);font-size:12px;">|</span>
<a href="index-en.html" hreflang="en" style="color:#CBD5E1;text-decoration:none;padding:4px 8px;border-radius:5px;font-weight:700;font-size:12px;letter-spacing:0.04em;">EN</a>
</div>
</div>
<button class="nav-toggle" onclick="document.getElementById('navActions').classList.toggle('open')" aria-label="Menu">
<svg width="22" height="22" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
</button>
</nav>
<!-- HERO -->
@ -666,6 +705,14 @@
<div class="container">
<div class="hero-wrap">
<div class="hero-left">
<a href="https://evix.agile.software" target="_blank" rel="noopener"
style="display:inline-flex;align-items:center;gap:8px;background:rgba(227,30,36,0.1);border:1px solid rgba(227,30,36,0.35);color:#E31E24;font-size:12.5px;font-weight:700;letter-spacing:0.04em;text-transform:uppercase;padding:7px 14px;border-radius:100px;text-decoration:none;margin-bottom:14px;transition:all .2s;"
onmouseover="this.style.background='rgba(227,30,36,0.18)';this.style.borderColor='#E31E24'"
onmouseout="this.style.background='rgba(227,30,36,0.1)';this.style.borderColor='rgba(227,30,36,0.35)'">
<i class="fa-solid fa-layer-group" style="font-size:11px"></i>
Modulo della EViX Suite
<i class="fa-solid fa-arrow-up-right-from-square" style="font-size:10px;opacity:.8"></i>
</a>
<div class="hero-badge">
<i class="fa-solid fa-circle-check"></i>
D.Lgs. 138/2024 — In vigore dal 16 ottobre 2024
@ -1013,12 +1060,23 @@
<textarea name="messaggio" placeholder="Descrivi brevemente la tua esigenza o il settore di appartenenza NIS2..."></textarea>
</div>
</div>
<div class="form-group full" style="margin-top:4px">
<label style="display:flex;gap:8px;align-items:flex-start;cursor:pointer;font-weight:400;font-size:13px;color:var(--text-light)">
<input type="checkbox" name="consent" id="ahubConsent" required style="margin-top:4px;flex-shrink:0;accent-color:var(--cyan)">
<span>Acconsento al trattamento dei miei dati ai sensi del GDPR (art. 13). I dati saranno usati esclusivamente per rispondere alla richiesta. <a href="https://agile.software/privacy" target="_blank" style="color:var(--cyan)">Privacy policy</a> <span style="color:var(--cyan)">*</span></span>
</label>
</div>
<!-- Honeypot anti-bot -->
<div style="position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden" aria-hidden="true">
<label>Lascia vuoto</label>
<input type="text" name="website_url" tabindex="-1" autocomplete="off">
</div>
<button type="submit" class="btn btn-primary form-submit" id="submitBtn">
<i class="fa-solid fa-paper-plane"></i> Invia richiesta
</button>
<p class="form-note">
<i class="fa-solid fa-lock" style="color:var(--cyan);margin-right:4px;font-size:11px;"></i>
I tuoi dati sono trattati nel rispetto del GDPR. Nessuna cessione a terzi.
Risposta entro 24h lavorative · Powered by AgileHub
</p>
</form>
<div class="form-success" id="formSuccess">
@ -1048,6 +1106,9 @@
<a href="/login.html" class="btn btn-ghost btn-lg">
<i class="fa-solid fa-right-to-bracket"></i> Accedi
</a>
<a href="https://evix.agile.software" target="_blank" rel="noopener" class="btn btn-ghost btn-lg" style="border-color:rgba(227,30,36,0.45);color:#E31E24">
<i class="fa-solid fa-layer-group"></i> Scopri tutta la EViX Suite
</a>
</div>
<p class="cta-note">Sei un consulente o MSSP? Il form di richiesta ti permette di attivare accesso per il tuo intero portfolio clienti.</p>
</div>
@ -1065,7 +1126,13 @@
<div class="footer-links">
<a href="#richiedi-accesso">Richiedi accesso</a>
<a href="/login.html">Accedi</a>
<a href="https://lg231.agile.software/" target="_blank">231 Agile</a>
<a href="https://evix.agile.software" target="_blank" rel="noopener" style="color:#E31E24;font-weight:600">EViX Suite ↗</a>
<a href="https://lg231.agile.software/" target="_blank" rel="noopener">231 Agile</a>
<a href="https://qsa.agile.software/" target="_blank" rel="noopener">Agile QSA</a>
<a href="https://trpg.agile.software/" target="_blank" rel="noopener">TRPG Agile</a>
<a href="https://sustainai.agile.software/" target="_blank" rel="noopener">SustainAI</a>
<a href="https://platform.agile.software/" target="_blank" rel="noopener" style="color:#A78BFA;font-weight:600">Built on Agile Platform ↗</a>
<a href="https://agile.software/" target="_blank" rel="noopener">Agile Technology</a>
<a href="mailto:info@agile.software">info@agile.software</a>
</div>
<div class="footer-copy">
@ -1075,6 +1142,39 @@
</footer>
<script>
/* AgileHub Lead Pipeline — NIS2 Agile (tenant agile-technology) */
const AHUB_PK = '02fd04c434fb5e1a83d11ee001f88e2677e8660f';
const AHUB_ENDPOINT = 'https://agilehub.agile.software/api/public/applets/' + AHUB_PK + '/submit';
/* Provenance tracking */
let __scrollMax = 0;
window.addEventListener('scroll', () => {
const h = document.documentElement;
const pct = Math.round((h.scrollTop || window.scrollY) / (h.scrollHeight - h.clientHeight) * 100);
if (pct > __scrollMax) __scrollMax = Math.min(pct, 100);
}, { passive: true });
const __pageLoadedAt = Date.now();
function ahubMetadata() {
const u = new URLSearchParams(location.search);
const md = {
utm_source: u.get('utm_source') || null,
utm_medium: u.get('utm_medium') || null,
utm_campaign: u.get('utm_campaign') || null,
utm_content: u.get('utm_content') || null,
utm_term: u.get('utm_term') || null,
referrer: document.referrer || null,
page_title: document.title,
page_url: location.href,
scroll_depth_pct: __scrollMax,
time_on_page_sec: Math.round((Date.now() - __pageLoadedAt) / 1000),
viewport: window.innerWidth + 'x' + window.innerHeight,
language: navigator.language,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
};
return md;
}
document.getElementById('inviteForm').addEventListener('submit', async function(e) {
e.preventDefault();
const btn = document.getElementById('submitBtn');
@ -1082,35 +1182,52 @@ document.getElementById('inviteForm').addEventListener('submit', async function(
const successEl = document.getElementById('formSuccess');
const form = e.target;
/* Honeypot anti-bot — silent reject */
if (form.website_url && form.website_url.value) {
form.style.display = 'none';
successEl.style.display = 'block';
return;
}
if (!form.consent.checked) {
errEl.textContent = 'È necessario accettare il trattamento dati per inviare la richiesta.';
errEl.style.display = 'block';
return;
}
errEl.style.display = 'none';
btn.disabled = true;
btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Invio in corso...';
const data = {
name: form.nome.value.trim(),
email: form.email.value.trim(),
phone: form.telefono.value.trim(),
company: form.azienda.value.trim(),
tipo: form.tipo.value,
size: form.n_dipendenti.value,
product_interest: form.interesse.value,
source: 'nis2-landing',
notes: form.messaggio.value.trim()
const payload = {
name: form.nome.value.trim(),
email: form.email.value.trim(),
phone: form.telefono.value.trim(),
company: form.azienda.value.trim(),
tipo: form.tipo.value,
consent: true,
fields: {
n_dipendenti: form.n_dipendenti.value,
interesse: form.interesse.value,
messaggio: form.messaggio.value.trim()
},
metadata: ahubMetadata()
};
try {
const res = await fetch('/api/mktg-lead/submit', {
const res = await fetch(AHUB_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
body: JSON.stringify(payload)
});
const json = await res.json();
if (res.ok && json.success) {
if (res.ok && json.success !== false) {
form.style.display = 'none';
successEl.style.display = 'block';
} else {
errEl.textContent = json.message || 'Errore nell\'invio. Riprova o scrivici a info@agile.software.';
const code = json.error && json.error.code ? ' (' + json.error.code + ')' : '';
errEl.textContent = (json.error && json.error.message) || ('Errore nell\'invio' + code + '. Scrivici a info@agile.software.');
errEl.style.display = 'block';
btn.disabled = false;
btn.innerHTML = '<i class="fa-solid fa-paper-plane"></i> Invia richiesta';
@ -1123,5 +1240,19 @@ document.getElementById('inviteForm').addEventListener('submit', async function(
}
});
</script>
<!-- Mobile conversion layer — sticky CTA + trust ribbon -->
<script src="mobile-conversion.js?v=20260519a"
data-primary-text="Richiedi accesso"
data-primary-action="anchor"
data-primary-href="#richiedi-accesso"
data-primary-icon="→"
data-accent="#EF4444"
data-accent2="#DC2626"
data-trust-items='["D.Lgs. 138/2024","Gap analysis AI","Incident response Art.23","Multi-tenant SaaS"]'
data-trust-after="section#hero"
data-trust-theme="dark"
data-scroll-trigger="400"
defer></script>
</body>
</html>

View File

@ -230,6 +230,9 @@ $actionMap = [
'POST:{id}/notification' => 'sendNotification',
'POST:{id}/finalReport' => 'sendFinalReport',
'POST:{id}/aiClassify' => 'aiClassify',
'GET:{id}/metrics' => 'metrics',
'GET:{id}/pir' => 'getPir',
'POST:{id}/pir' => 'savePir',
],
// ── PolicyController ────────────────────────────
@ -269,10 +272,13 @@ $actionMap = [
'assets' => [
'GET:list' => 'list',
'POST:create' => 'create',
'GET:scoringGrid' => 'scoringGrid',
'GET:relevantSystems' => 'relevantSystems',
'GET:dependencyMap' => 'dependencyMap',
'GET:{id}' => 'get',
'PUT:{id}' => 'update',
'DELETE:{id}' => 'delete',
'GET:dependencyMap' => 'dependencyMap',
'POST:{id}/score' => 'score',
],
// ── AuditController ─────────────────────────────
@ -284,7 +290,9 @@ $actionMap = [
'GET:report' => 'generateReport',
'GET:logs' => 'getAuditLogs',
'GET:iso27001Mapping' => 'getIsoMapping',
'GET:nistCsfMapping' => 'getNistCsfMapping',
'GET:executiveReport' => 'executiveReport',
'GET:relevantSystemsRegister' => 'relevantSystemsRegister',
'GET:export' => 'export',
'GET:chainVerify' => 'chainVerify',
'GET:exportCertified' => 'exportCertified',

548
public/js/ai-assistant.js Normal file
View File

@ -0,0 +1,548 @@
/**
* AgileHub AI Assistant Widget (L1 Self-Service)
* Embeddabile in qualsiasi prodotto della suite.
*
* Flusso: L1 AI (KB search) -> feedback -> L2 Formatore -> L3 Ticket
*
* Uso:
* <script src="js/ai-assistant.js"
* data-product="TRPG"
* data-tenant-id="3"
* data-api-url="https://agilehub.agile.software"
* data-user-name="Mario Rossi"
* data-user-email="mario@studio.it"
* data-user-role="consultant"
* data-lang="it">
* </script>
*
* Prerequisiti backend:
* - POST /api/support-sessions (crea sessione)
* - POST /api/support-sessions/:id/message (AI risponde con KB)
* - POST /api/support-sessions/:id/resolve (feedback positivo)
* - POST /api/support-sessions/:id/forward-expert (escala L2)
* - POST /api/support-sessions/:id/escalate (escala L3 ticket)
*/
(function () {
'use strict';
// ==================== CONFIG ====================
var script = document.currentScript || document.querySelector('script[data-product][data-api-url]');
var CFG = {
apiUrl: (script && script.dataset.apiUrl) || '',
product: (script && script.dataset.product) || 'GENERIC',
tenantId: (script && script.dataset.tenantId) || '1',
userName: (script && script.dataset.userName) || '',
userEmail: (script && script.dataset.userEmail) || '',
userRole: (script && script.dataset.userRole) || '',
lang: (script && script.dataset.lang) || 'it'
};
var LABELS = {
it: {
title: 'Assistente AgileHub',
placeholder: 'Scrivi una domanda...',
viewing: 'Stai guardando',
suggestions_intro: 'Posso aiutarti con:',
feedback_yes: 'Si, grazie',
feedback_no: 'No, aiuto',
escalation_title: 'Come posso aiutarti meglio?',
btn_rephrase: 'Riformula la domanda',
btn_expert: 'Chiedi al formatore',
btn_bug: 'Segnala un bug',
expert_sent: 'Domanda inoltrata al formatore. Riceverai una notifica quando la risposta sara pronta.',
expert_time: 'Tempo stimato: entro 4 ore lavorative.',
resolved_thanks: 'Grazie per il feedback! La tua valutazione migliora l\'assistente.',
preparing: 'Mi sto preparando...',
thinking: 'Sto pensando...',
source: 'Fonte',
resume: 'Riprendiamo la conversazione.',
mic_unsupported: 'Microfono non supportato dal browser.',
error_generic: 'Errore di connessione. Riprova tra poco.'
},
en: {
title: 'AgileHub Assistant',
placeholder: 'Ask a question...',
viewing: 'You are viewing',
suggestions_intro: 'I can help you with:',
feedback_yes: 'Yes, thanks',
feedback_no: 'No, help me',
escalation_title: 'How can I help you better?',
btn_rephrase: 'Rephrase question',
btn_expert: 'Ask an expert',
btn_bug: 'Report a bug',
expert_sent: 'Your question has been forwarded to an expert. You\'ll be notified when the answer is ready.',
expert_time: 'Estimated time: within 4 business hours.',
resolved_thanks: 'Thanks for the feedback! Your rating improves the assistant.',
preparing: 'Preparing...',
thinking: 'Thinking...',
source: 'Source',
resume: 'Let\'s pick up where we left off.',
mic_unsupported: 'Microphone not supported by this browser.',
error_generic: 'Connection error. Please try again shortly.'
}
};
function t(key) {
var lang = CFG.lang;
return (LABELS[lang] && LABELS[lang][key]) || (LABELS.it[key]) || key;
}
// ==================== STATE ====================
var sessionId = null;
var messages = []; // { role: 'user'|'assistant'|'system', content, sources?, feedbackShown? }
var panelOpen = false;
var recognition = null;
var isRecording = false;
// ==================== API HELPERS ====================
function apiHeaders() {
var h = { 'Content-Type': 'application/json', 'x-tenant-id': CFG.tenantId };
// Try to get JWT from the host app
if (typeof Api !== 'undefined' && Api.auth && Api.auth.getToken) {
var tok = Api.auth.getToken();
if (tok) h['Authorization'] = 'Bearer ' + tok;
}
var stored = typeof localStorage !== 'undefined' ? localStorage.getItem('nexus_token') : null;
if (!h['Authorization'] && stored) h['Authorization'] = 'Bearer ' + stored;
return h;
}
function apiPost(path, body) {
return fetch(CFG.apiUrl + path, {
method: 'POST', headers: apiHeaders(), body: JSON.stringify(body)
}).then(function (r) { return r.json(); });
}
function apiGet(path) {
return fetch(CFG.apiUrl + path, { headers: apiHeaders() }).then(function (r) { return r.json(); });
}
// ==================== CONTEXT DETECTION ====================
function detectPage() {
// Try HelpSystem if available
if (typeof HelpSystem !== 'undefined' && HelpSystem._detectPage) {
return HelpSystem._detectPage();
}
// Fallback: extract from URL hash or path
var hash = location.hash.replace('#', '').replace('/', '') || '';
return hash || document.title || '';
}
function getContextualSuggestions() {
if (typeof HelpSystem === 'undefined') return [];
var page = typeof HelpSystem._detectPage === 'function' ? HelpSystem._detectPage() : null;
var helpData = null;
if (page && HelpSystem._helpContent) helpData = HelpSystem._helpContent[page];
if (page && HelpSystem._help) {
var raw = HelpSystem._help[page];
if (raw) helpData = raw[CFG.lang] || raw.it || raw;
}
if (!helpData) return [];
var suggestions = [];
if (helpData.faq) {
suggestions = helpData.faq.slice(0, 3).map(function (f) {
return (typeof f === 'string') ? f : (f.question || f.q || '');
});
} else if (helpData.sections) {
suggestions = helpData.sections.slice(0, 3).map(function (s) {
if (!s.heading) return '';
if (typeof s.heading === 'object') return s.heading[CFG.lang] || s.heading.it || '';
return s.heading;
});
}
return suggestions.filter(Boolean);
}
// ==================== SESSION MANAGEMENT ====================
function createSession(cb) {
apiPost('/api/support-sessions', {
product: CFG.product,
callerEmail: CFG.userEmail,
callerName: CFG.userName
}).then(function (res) {
if (res.success && res.data) {
sessionId = res.data.sessionId || res.data.id;
if (res.data.welcomeMessage) {
messages.push({ role: 'assistant', content: res.data.welcomeMessage });
}
// Store session for resume
try { sessionStorage.setItem('agilehub_ai_session_' + CFG.product, sessionId); } catch (e) {}
}
if (cb) cb();
}).catch(function () {
messages.push({ role: 'system', content: t('error_generic') });
if (cb) cb();
});
}
function tryResumeSession(cb) {
try {
var stored = sessionStorage.getItem('agilehub_ai_session_' + CFG.product);
if (stored) {
sessionId = stored;
messages.push({ role: 'system', content: t('resume') });
cb(true);
return;
}
} catch (e) {}
cb(false);
}
function sendMessage(text, cb) {
if (!sessionId) {
createSession(function () { if (sessionId) sendMessage(text, cb); else cb(); });
return;
}
messages.push({ role: 'user', content: text });
render();
apiPost('/api/support-sessions/' + sessionId + '/message', {
content: text
}).then(function (res) {
if (res.success && res.data) {
var reply = res.data.reply || res.data.message || res.data.answer || '';
var sources = res.data.knowledgeArticles || res.data.sources || [];
messages.push({ role: 'assistant', content: reply, sources: sources, feedbackShown: true });
} else {
messages.push({ role: 'assistant', content: (res.error && res.error.message) || t('error_generic') });
}
cb();
}).catch(function () {
messages.push({ role: 'assistant', content: t('error_generic') });
cb();
});
}
function resolveSession() {
if (!sessionId) return;
apiPost('/api/support-sessions/' + sessionId + '/resolve', {
rating: 5, resolutionSummary: 'auto'
});
}
function forwardToExpert(question, context) {
if (!sessionId) return;
return apiPost('/api/support-sessions/' + sessionId + '/forward-expert', {
question: question, context: context
});
}
function escalateToTicket(reason) {
if (!sessionId) return;
return apiPost('/api/support-sessions/' + sessionId + '/escalate', {
reason: reason
});
}
// ==================== VOICE INPUT ====================
function initVoice() {
var SR = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SR) return null;
recognition = new SR();
recognition.lang = CFG.lang === 'en' ? 'en-US' : 'it-IT';
recognition.interimResults = true;
recognition.continuous = false;
return recognition;
}
function toggleMic() {
if (!recognition) {
if (!initVoice()) {
messages.push({ role: 'system', content: t('mic_unsupported') });
render();
return;
}
}
if (isRecording) {
recognition.stop();
isRecording = false;
render();
return;
}
var input = document.getElementById('aia-input');
recognition.onresult = function (e) {
var transcript = '';
for (var i = e.resultIndex; i < e.results.length; i++) {
transcript += e.results[i][0].transcript;
}
if (input) input.value = transcript;
if (e.results[e.results.length - 1].isFinal) {
isRecording = false;
render();
if (transcript.trim()) submitInput(transcript.trim());
}
};
recognition.onerror = function () { isRecording = false; render(); };
recognition.onend = function () { isRecording = false; render(); };
recognition.start();
isRecording = true;
render();
}
// ==================== RENDERING ====================
function esc(s) {
return String(s).replace(/[&<>"']/g, function (c) {
return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c];
});
}
function renderMessages() {
return messages.map(function (m, idx) {
var isUser = m.role === 'user';
var isSystem = m.role === 'system';
var bubble = '<div style="margin-bottom:10px;display:flex;justify-content:'
+ (isUser ? 'flex-end' : 'flex-start') + '">'
+ '<div style="max-width:82%;padding:10px 14px;border-radius:14px;font-size:13px;line-height:1.5;'
+ 'background:' + (isUser ? 'linear-gradient(135deg,#7C3AED,#3B82F6)' : isSystem ? '#FEF3C7' : '#f3f4f6')
+ ';color:' + (isUser ? '#fff' : '#111827')
+ ';white-space:pre-wrap">'
+ esc(m.content);
// Sources
if (m.sources && m.sources.length > 0) {
bubble += '<div style="margin-top:8px;padding-top:6px;border-top:1px solid rgba(0,0,0,.1);font-size:11px;color:#6B7280">';
m.sources.forEach(function (s, si) {
bubble += '<div>' + t('source') + ' [' + (si + 1) + '] ' + esc(s.question || s.title || '') + '</div>';
});
bubble += '</div>';
}
bubble += '</div></div>';
// Feedback buttons
if (m.feedbackShown && m.role === 'assistant' && idx === messages.length - 1) {
bubble += '<div id="aia-feedback" style="display:flex;gap:8px;justify-content:flex-start;margin-bottom:10px">'
+ '<button onclick="window.__aiaFeedback(true)" style="padding:6px 14px;border-radius:8px;border:1px solid #D1D5DB;'
+ 'background:#fff;cursor:pointer;font-size:12px;color:#059669">&#x1F44D; ' + esc(t('feedback_yes')) + '</button>'
+ '<button onclick="window.__aiaFeedback(false)" style="padding:6px 14px;border-radius:8px;border:1px solid #D1D5DB;'
+ 'background:#fff;cursor:pointer;font-size:12px;color:#DC2626">&#x1F44E; ' + esc(t('feedback_no')) + '</button>'
+ '</div>';
}
return bubble;
}).join('');
}
function renderEscalation() {
return '<div id="aia-escalation" style="background:#F9FAFB;border-radius:12px;padding:14px;margin-bottom:10px">'
+ '<div style="font-weight:600;margin-bottom:10px;font-size:13px">' + esc(t('escalation_title')) + '</div>'
+ '<button onclick="window.__aiaEscalate(\'rephrase\')" style="display:block;width:100%;text-align:left;padding:10px 12px;'
+ 'border:1px solid #E5E7EB;border-radius:8px;background:#fff;cursor:pointer;margin-bottom:6px;font-size:12px">'
+ '&#x1F504; ' + esc(t('btn_rephrase')) + '</button>'
+ '<button onclick="window.__aiaEscalate(\'expert\')" style="display:block;width:100%;text-align:left;padding:10px 12px;'
+ 'border:1px solid #E5E7EB;border-radius:8px;background:#fff;cursor:pointer;margin-bottom:6px;font-size:12px">'
+ '&#x1F468;&#x200D;&#x1F3EB; ' + esc(t('btn_expert')) + '</button>'
+ '<button onclick="window.__aiaEscalate(\'bug\')" style="display:block;width:100%;text-align:left;padding:10px 12px;'
+ 'border:1px solid #E5E7EB;border-radius:8px;background:#fff;cursor:pointer;font-size:12px">'
+ '&#x1F41B; ' + esc(t('btn_bug')) + '</button>'
+ '</div>';
}
function render() {
var panel = document.getElementById('aia-panel');
if (!panel) return;
var page = detectPage();
var suggestions = (messages.length === 0) ? getContextualSuggestions() : [];
var showEscalation = panel.dataset.showEscalation === '1';
panel.innerHTML =
// Header
'<div style="padding:14px 16px;background:linear-gradient(135deg,#7C3AED,#3B82F6);color:#fff;'
+ 'display:flex;justify-content:space-between;align-items:center;border-radius:16px 16px 0 0">'
+ '<div style="display:flex;align-items:center;gap:8px;font-weight:600;font-size:14px">'
+ '<i class="fa-solid fa-wand-magic-sparkles"></i> ' + esc(t('title'))
+ '</div>'
+ '<button onclick="window.__aiaToggle()" style="background:none;border:none;color:#fff;font-size:18px;cursor:pointer">&times;</button>'
+ '</div>'
// Context bar
+ (page ? '<div style="padding:8px 16px;background:#EDE9FE;font-size:11px;color:#6D28D9">'
+ '&#x1F4CD; ' + esc(t('viewing')) + ': <strong>' + esc(page) + '</strong></div>' : '')
// Messages area
+ '<div id="aia-msgs" style="flex:1;overflow-y:auto;padding:14px 16px;background:#fafafa;min-height:200px">'
+ (messages.length === 0 && suggestions.length > 0
? '<div style="color:#6B7280;font-size:13px;margin-bottom:12px">' + esc(t('suggestions_intro')) + '</div>'
+ suggestions.map(function (s) {
return '<button onclick="window.__aiaSuggest(\'' + esc(s).replace(/'/g, "\\'") + '\')" '
+ 'style="display:block;width:100%;text-align:left;padding:8px 12px;margin-bottom:6px;'
+ 'border:1px solid #E5E7EB;border-radius:8px;background:#fff;cursor:pointer;font-size:12px;color:#4B5563">'
+ '&#x1F4A1; ' + esc(s) + '</button>';
}).join('')
: '')
+ renderMessages()
+ (showEscalation ? renderEscalation() : '')
+ '</div>'
// Input bar
+ '<form id="aia-form" onsubmit="return window.__aiaSubmit(event)" '
+ 'style="display:flex;gap:6px;padding:10px;border-top:1px solid #E5E7EB;background:#fff;border-radius:0 0 16px 16px">'
+ '<input id="aia-input" type="text" placeholder="' + esc(t('placeholder')) + '" autocomplete="off" '
+ 'style="flex:1;padding:10px 12px;border:1px solid #D1D5DB;border-radius:10px;font-size:13px;font-family:inherit">'
+ '<button type="button" onclick="window.__aiaMic()" style="padding:8px;border:none;border-radius:10px;'
+ 'background:' + (isRecording ? '#EF4444' : '#F3F4F6') + ';cursor:pointer;font-size:16px" title="Voce">'
+ (isRecording ? '&#x23F9;' : '&#x1F3A4;') + '</button>'
+ '<button type="submit" style="padding:8px 14px;border:none;border-radius:10px;'
+ 'background:linear-gradient(135deg,#7C3AED,#3B82F6);color:#fff;font-weight:600;cursor:pointer">'
+ '<i class="fa-solid fa-paper-plane"></i></button>'
+ '</form>';
// Scroll to bottom
var msgsEl = document.getElementById('aia-msgs');
if (msgsEl) msgsEl.scrollTop = msgsEl.scrollHeight;
}
// ==================== HANDLERS ====================
function submitInput(text) {
if (!text) return;
var input = document.getElementById('aia-input');
if (input) input.value = '';
var panel = document.getElementById('aia-panel');
if (panel) panel.dataset.showEscalation = '0';
// Show thinking
messages.push({ role: 'assistant', content: t('thinking') });
render();
messages.pop(); // remove thinking
sendMessage(text, function () { render(); });
}
window.__aiaToggle = function () {
panelOpen = !panelOpen;
var panel = document.getElementById('aia-panel');
var fab = document.getElementById('ai-chat-fab');
if (panel) panel.style.display = panelOpen ? 'flex' : 'none';
if (fab) fab.style.display = panelOpen ? 'none' : 'flex';
if (panelOpen && messages.length === 0) {
tryResumeSession(function (resumed) {
if (!resumed) {
createSession(function () { render(); });
} else {
render();
}
});
}
if (panelOpen) render();
};
window.__aiaSubmit = function (e) {
e.preventDefault();
var input = document.getElementById('aia-input');
var text = (input && input.value || '').trim();
if (text) submitInput(text);
return false;
};
window.__aiaSuggest = function (text) { submitInput(text); };
window.__aiaMic = function () { toggleMic(); };
window.__aiaFeedback = function (positive) {
var fbEl = document.getElementById('aia-feedback');
if (fbEl) fbEl.remove();
if (positive) {
resolveSession();
messages.push({ role: 'system', content: t('resolved_thanks') });
render();
} else {
// Show escalation options
var panel = document.getElementById('aia-panel');
if (panel) panel.dataset.showEscalation = '1';
render();
}
};
window.__aiaEscalate = function (action) {
var panel = document.getElementById('aia-panel');
if (panel) panel.dataset.showEscalation = '0';
if (action === 'rephrase') {
render();
var input = document.getElementById('aia-input');
if (input) input.focus();
return;
}
if (action === 'expert') {
var lastUserMsg = '';
var lastAiMsg = '';
for (var i = messages.length - 1; i >= 0; i--) {
if (!lastAiMsg && messages[i].role === 'assistant') lastAiMsg = messages[i].content;
if (!lastUserMsg && messages[i].role === 'user') lastUserMsg = messages[i].content;
if (lastUserMsg && lastAiMsg) break;
}
forwardToExpert(lastUserMsg, lastAiMsg).then(function () {
messages.push({ role: 'system', content: t('expert_sent') + '\n' + t('expert_time') });
render();
});
return;
}
if (action === 'bug') {
var reason = '';
for (var j = messages.length - 1; j >= 0; j--) {
if (messages[j].role === 'user') { reason = messages[j].content; break; }
}
escalateToTicket(reason).then(function () {
messages.push({ role: 'system', content: 'Ticket tecnico creato. Il team di sviluppo lo prendera in carico.' });
render();
});
return;
}
};
// ==================== INIT ====================
function init() {
// Ensure FAB exists (may have been created by product's app.js)
var fab = document.getElementById('ai-chat-fab');
if (!fab) {
fab = document.createElement('button');
fab.id = 'ai-chat-fab';
fab.setAttribute('aria-label', 'Chiedi ad ARIA');
fab.style.cssText = 'position:fixed;bottom:24px;right:24px;width:56px;height:56px;'
+ 'border-radius:50%;border:none;cursor:pointer;z-index:9998;'
+ 'background:linear-gradient(135deg,#7C3AED,#3B82F6);color:#fff;'
+ 'box-shadow:0 6px 20px rgba(124,58,237,.4);font-size:22px;'
+ 'display:flex;align-items:center;justify-content:center';
fab.innerHTML = '<i class="fa-solid fa-wand-magic-sparkles"></i>';
document.body.appendChild(fab);
} else {
// Rimuovi il vecchio pannello AI del prodotto (evita doppia finestra)
var oldPanel = document.getElementById('ai-chat-panel');
if (oldPanel) oldPanel.remove();
// Clona il FAB per eliminare TUTTI gli addEventListener precedenti
var cleanFab = fab.cloneNode(true);
fab.parentNode.replaceChild(cleanFab, fab);
fab = cleanFab;
}
fab.style.display = 'flex';
fab.style.alignItems = 'center';
fab.style.justifyContent = 'center';
fab.onclick = window.__aiaToggle;
// Ensure panel exists
var panel = document.getElementById('aia-panel');
if (!panel) {
panel = document.createElement('div');
panel.id = 'aia-panel';
panel.style.cssText = 'position:fixed;bottom:90px;right:24px;width:380px;max-height:560px;'
+ 'background:#fff;border-radius:16px;box-shadow:0 12px 32px rgba(0,0,0,.2);'
+ 'display:none;z-index:9999;overflow:hidden;flex-direction:column;'
+ 'font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,sans-serif';
document.body.appendChild(panel);
}
panel.dataset.showEscalation = '0';
}
// Auto-init when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

View File

@ -212,6 +212,10 @@ class NIS2API {
sendEarlyWarning(id) { return this.post(`/incidents/${id}/early-warning`, {}); }
sendNotification(id) { return this.post(`/incidents/${id}/notification`, {}); }
sendFinalReport(id) { return this.post(`/incidents/${id}/final-report`, {}); }
aiClassifyIncident(id) { return this.post(`/incidents/${id}/aiClassify`, {}); }
getIncidentMetrics(id) { return this.get(`/incidents/${id}/metrics`); }
getIncidentPir(id) { return this.get(`/incidents/${id}/pir`); }
saveIncidentPir(id, data) { return this.post(`/incidents/${id}/pir`, data); }
// ═══════════════════════════════════════════════════════════════════
// Policies
@ -251,6 +255,11 @@ class NIS2API {
createAsset(data) { return this.post('/assets/create', data); }
getAsset(id) { return this.get(`/assets/${id}`); }
updateAsset(id, data) { return this.put(`/assets/${id}`, data); }
deleteAsset(id) { return this.delete(`/assets/${id}`); }
// NIS2 relevance scoring (GV.OC-04)
getScoringGrid() { return this.get('/assets/scoringGrid'); }
scoreAsset(id, criteria) { return this.post(`/assets/${id}/score`, { criteria }); }
listRelevantSystems() { return this.get('/assets/relevantSystems'); }
// ═══════════════════════════════════════════════════════════════════
// Audit

870
public/js/bug-reporter.js Normal file
View File

@ -0,0 +1,870 @@
/**
* AgileHub Bug Reporter Widget
* Embeddabile in qualsiasi prodotto (AllTax, TRPG, DFM, WMS, ecc.)
*
* Features:
* - Bottone 🐛 segnalazione bug (testo + voce)
* - Ctrl+V paste screenshot dalla clipboard
* - Bottone 📸 screenshot rapido della videata
* - Drag & drop immagini
* - Tab "Mie Segnalazioni" / "Risolte"
* - Campanella 🔔 notifiche con badge
*
* Uso:
* <script src="https://agilehub.agile.software/widgets/bug-reporter.js"
* data-product="TRPG"
* data-tenant-id="1"
* data-api-url="https://agilehub.agile.software"
* data-user-name="Marco Consulente"
* data-user-email="marco@studio.it"
* data-user-role="consultant">
* </script>
*/
(function() {
'use strict';
// ==================== CONFIG ====================
const script = document.currentScript || document.querySelector('script[data-product]');
const WIDGET_VERSION = '1.4.15';
const WIDGET_BUILD = '20260415d';
const CFG = {
apiUrl: (script && script.dataset.apiUrl) || 'http://localhost',
apiKey: (script && script.dataset.apiKey) || '',
product: (script && script.dataset.product) || '',
tenantId: (script && script.dataset.tenantId) || '1',
userName: (script && script.dataset.userName) || '',
userEmail: (script && script.dataset.userEmail) || '',
userRole: (script && script.dataset.userRole) || '',
lang: (script && script.dataset.lang) || 'it'
};
// L'API è sempre via proxy /api (sia per la dashboard che per i prodotti esterni).
const API = CFG.apiUrl + '/api';
// Headers standard per tutte le chiamate API
function _headers(json) {
const h = { 'x-tenant-id': CFG.tenantId };
if (CFG.apiKey) h['X-API-Key'] = CFG.apiKey;
if (json) h['Content-Type'] = 'application/json';
return h;
}
// Auto-detect utente dal contesto dell'app (se data-user-email non è nel tag)
function _detectUser() {
if (CFG.userEmail) return;
try {
// 1. sessionStorage 'user' (pattern TRPG — JSON con email/name/role)
const userJson = sessionStorage.getItem('user');
if (userJson) {
const u = JSON.parse(userJson);
if (u && u.email) { CFG.userEmail = u.email; CFG.userName = u.name || u.full_name || CFG.userName; CFG.userRole = u.role || CFG.userRole; return; }
}
// 2. api.getUser() (pattern TRPG/AllTax — globale)
if (typeof api !== 'undefined') {
const u = (typeof api.getUser === 'function' ? api.getUser() : api.user) || {};
if (u.email) { CFG.userEmail = u.email; CFG.userName = u.name || u.full_name || CFG.userName; CFG.userRole = u.role || CFG.userRole; return; }
}
// 3. JWT decode da access_token / nexus_token
const token = sessionStorage.getItem('access_token') || sessionStorage.getItem('nexus_token') || localStorage.getItem('nexus_token');
if (token && token.includes('.')) {
const payload = JSON.parse(atob(token.split('.')[1].replace(/-/g,'+').replace(/_/g,'/')));
if (payload.email) { CFG.userEmail = payload.email; CFG.userName = payload.name || payload.full_name || CFG.userName; CFG.userRole = payload.role || CFG.userRole; }
}
} catch(e) { /* silent */ }
}
// Ritenta: lo script è nel HEAD, l'utente potrebbe non essere ancora loggato
setTimeout(_detectUser, 500);
setTimeout(_detectUser, 3000);
setTimeout(_detectUser, 8000);
// ==================== STATE ====================
let _attachments = [];
let _notifCount = 0;
let _notifPollTimer = null;
let _supportSessionId = null;
let _chatMessages = [];
let _chatMode = false; // AI chat e nel widget separato ai-assistant.js
let _chatLoading = false;
// ==================== STYLES ====================
const STYLES = `
#nx-bar{position:fixed;bottom:94px;right:24px;display:flex;flex-direction:column;gap:14px;z-index:99990;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif}
#nx-bar button{width:56px;height:56px;border-radius:50%;border:none;cursor:pointer;font-size:22px;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 12px rgba(0,0,0,.25);transition:transform .15s}
#nx-bar button:hover{transform:scale(1.1)}
#nx-bug-btn{background:#ef4444;color:#fff}
#nx-bell-btn{background:#3b82f6;color:#fff;position:relative}
#nx-bell-badge{position:absolute;top:-4px;right:-4px;background:#ef4444;color:#fff;border-radius:10px;min-width:18px;height:18px;font-size:10px;font-weight:700;display:flex;align-items:center;justify-content:center;padding:0 4px}
#nx-modal-bg{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:99991;display:flex;align-items:center;justify-content:center}
#nx-modal{background:#fff;border-radius:14px;width:92%;max-width:500px;max-height:92vh;overflow:hidden;display:flex;flex-direction:column;box-shadow:0 20px 60px rgba(0,0,0,.3)}
#nx-modal *{box-sizing:border-box}
.nx-header{display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid #e5e7eb}
.nx-header h3{margin:0;font-size:17px;font-weight:600}
.nx-close{cursor:pointer;font-size:20px;color:#999;background:none;border:none;width:auto;height:auto;box-shadow:none}
.nx-close:hover{color:#333;transform:none}
.nx-tabs{display:flex;border-bottom:1px solid #e5e7eb}
.nx-tab{flex:1;padding:10px;text-align:center;font-size:13px;font-weight:500;cursor:pointer;border:none;background:none;color:#6b7280;border-bottom:2px solid transparent;transition:all .15s}
.nx-tab.active{color:#3b82f6;border-bottom-color:#3b82f6}
.nx-body{flex:1;overflow-y:auto;padding:16px 20px}
.nx-row{display:flex;gap:8px;margin-bottom:10px}
.nx-select,.nx-input,.nx-textarea{width:100%;padding:9px 12px;border-radius:8px;border:1px solid #d1d5db;font-size:14px;font-family:inherit}
.nx-textarea{height:90px;resize:vertical}
.nx-textarea.drag-over{border-color:#3b82f6;background:#eff6ff}
.nx-btn{padding:10px;border-radius:8px;border:1px solid #d1d5db;cursor:pointer;background:#f9fafb;font-size:14px;text-align:center;flex:1}
.nx-btn:hover{background:#f3f4f6}
.nx-btn-primary{background:#3b82f6;color:#fff;border:none;font-weight:600;width:100%;padding:12px;font-size:15px}
.nx-btn-primary:hover{background:#2563eb}
.nx-btn-primary:disabled{opacity:.5;cursor:not-allowed}
.nx-att-list{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px}
.nx-att-item{position:relative;width:60px;height:60px;border-radius:6px;overflow:hidden;border:1px solid #e5e7eb}
.nx-att-item img{width:100%;height:100%;object-fit:cover}
.nx-att-remove{position:absolute;top:-4px;right:-4px;width:18px;height:18px;border-radius:50%;background:#ef4444;color:#fff;font-size:11px;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;line-height:1}
.nx-ctx{background:#f8fafc;border-radius:8px;padding:8px 12px;margin-bottom:10px;font-size:12px;color:#6b7280}
.nx-result{margin-top:12px;padding:12px;border-radius:8px;font-size:14px}
.nx-result.ok{background:#f0fdf4;border:1px solid #bbf7d0;color:#166534}
.nx-result.err{background:#fef2f2;border:1px solid #fecaca;color:#991b1b}
.nx-ticket{padding:12px;border:1px solid #e5e7eb;border-radius:10px;margin-bottom:8px;cursor:pointer;transition:background .1s}
.nx-ticket:hover{background:#f9fafb}
.nx-ticket-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:4px}
.nx-ticket-id{font-size:12px;font-weight:600;color:#3b82f6}
.nx-ticket-status{font-size:11px;padding:2px 8px;border-radius:10px;font-weight:500}
.nx-ticket-subject{font-size:14px;font-weight:500;margin-bottom:4px}
.nx-ticket-meta{font-size:12px;color:#9ca3af}
.nx-ticket-ai{font-size:13px;color:#7c3aed;background:#f5f3ff;padding:8px;border-radius:6px;margin-top:6px}
.nx-notif{padding:10px 12px;border-bottom:1px solid #f3f4f6;cursor:pointer;transition:background .1s}
.nx-notif:hover{background:#f9fafb}
.nx-notif.unread{background:#eff6ff}
.nx-notif-title{font-size:14px;font-weight:500;margin-bottom:2px}
.nx-notif-body{font-size:13px;color:#6b7280}
.nx-notif-meta{font-size:11px;color:#9ca3af;margin-top:4px}
.nx-notif-dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:6px}
.nx-notif-dot.BUG_FIX{background:#22c55e}.nx-notif-dot.UPDATE{background:#3b82f6}
.nx-notif-dot.ALERT{background:#ef4444}.nx-notif-dot.INFO{background:#9ca3af}
.nx-empty{text-align:center;padding:30px;color:#9ca3af;font-size:14px}
.nx-chat-area{flex:1;overflow-y:auto;padding:12px;min-height:200px;max-height:50vh;display:flex;flex-direction:column;gap:8px}
.nx-chat-msg{max-width:85%;padding:10px 14px;border-radius:14px;font-size:14px;line-height:1.5;word-wrap:break-word;animation:nxFadeIn .2s}
.nx-chat-msg.user{align-self:flex-end;background:#4f46e5;color:#fff;border-bottom-right-radius:4px}
.nx-chat-msg.ai{align-self:flex-start;background:#f3f4f6;color:#1f2937;border-bottom-left-radius:4px}
.nx-chat-msg.system{align-self:center;background:#fef3c7;color:#92400e;font-size:13px;border-radius:8px;text-align:center}
.nx-chat-input{display:flex;gap:8px;padding:12px;border-top:1px solid #e5e7eb}
.nx-chat-input input{flex:1;padding:10px 14px;border:1px solid #d1d5db;border-radius:20px;font-size:14px;font-family:inherit;outline:none}
.nx-chat-input input:focus{border-color:#4f46e5}
.nx-chat-input button{width:40px;height:40px;border-radius:50%;border:none;background:#4f46e5;color:#fff;cursor:pointer;font-size:16px;display:flex;align-items:center;justify-content:center}
.nx-chat-input button:disabled{opacity:.4;cursor:not-allowed}
.nx-chat-actions{display:flex;gap:8px;padding:0 12px 12px;justify-content:center}
.nx-chat-actions button{padding:8px 16px;border-radius:8px;border:1px solid #d1d5db;background:#f9fafb;font-size:13px;cursor:pointer;font-family:inherit}
.nx-chat-actions button:hover{background:#f3f4f6}
.nx-chat-actions .resolve{border-color:#22c55e;color:#16a34a}
.nx-chat-actions .classic{border-color:#3b82f6;color:#2563eb}
.nx-typing{display:flex;gap:4px;padding:6px 14px;align-self:flex-start}
.nx-typing span{width:6px;height:6px;border-radius:50%;background:#9ca3af;animation:nxBounce .6s infinite alternate}
.nx-typing span:nth-child(2){animation-delay:.15s}
.nx-typing span:nth-child(3){animation-delay:.3s}
@keyframes nxFadeIn{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}
@keyframes nxBounce{to{transform:translateY(-4px);opacity:.4}}
.nx-status-OPEN{background:#dbeafe;color:#1d4ed8}
.nx-status-IN_PROGRESS{background:#fef3c7;color:#92400e}
.nx-status-PENDING{background:#f3f4f6;color:#4b5563}
.nx-status-RESOLVED{background:#d1fae5;color:#065f46}
.nx-status-CLOSED{background:#e5e7eb;color:#6b7280}
`;
// ==================== INIT ====================
function init() {
// Inject styles
const style = document.createElement('style');
style.textContent = STYLES;
document.head.appendChild(style);
// Bottom bar: 🛟 supporto + 🔔 notifiche
// Standard suite Agile: bug=fa-life-ring "Chiedi supporto" / bell=fa-bell
const bar = document.createElement('div');
bar.id = 'nx-bar';
bar.innerHTML = `
<button id="nx-bell-btn" title="Notifiche"><i class="fas fa-bell"></i><span id="nx-bell-badge" style="display:none">0</span></button>
<button id="nx-bug-btn" title="Chiedi supporto"><i class="fas fa-life-ring"></i></button>
`;
document.body.appendChild(bar);
document.getElementById('nx-bug-btn').onclick = () => openModal('new');
document.getElementById('nx-bell-btn').onclick = () => openModal('notif');
// Hook campanella topbar del prodotto (se esiste)
const topBell = document.getElementById('btn-notifications');
if (topBell) {
// Rimuovi il vecchio dropdown notifiche del prodotto (evita doppia finestra)
const oldDropdown = document.getElementById('notif-dropdown');
if (oldDropdown) oldDropdown.remove();
// Clona per rimuovere vecchi listener
const newBell = topBell.cloneNode(true);
topBell.parentNode.replaceChild(newBell, topBell);
newBell.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); openModal('notif'); });
window._nxTopBadge = function(c) {
const b = newBell.querySelector('.badge') || document.getElementById('worklist-badge');
if (b) { b.textContent = c > 99 ? '99+' : c; b.style.display = c > 0 ? '' : 'none'; }
};
}
// Start notification polling
pollNotifications();
_notifPollTimer = setInterval(pollNotifications, 60000);
}
// ==================== NOTIFICATION POLLING ====================
async function pollNotifications() {
try {
const res = await fetch(`${API}/notifications/unread-count?product=${CFG.product}`, {
headers: _headers()
});
const d = await res.json();
if (d.success) {
_notifCount = d.data.unread;
const badge = document.getElementById('nx-bell-badge');
if (badge) {
badge.style.display = _notifCount > 0 ? 'flex' : 'none';
badge.textContent = _notifCount > 9 ? '9+' : String(_notifCount);
}
// Aggiorna anche campanella topbar del prodotto (se hookata)
if (window._nxTopBadge) window._nxTopBadge(_notifCount);
}
} catch(e) { /* silent */ }
}
// ==================== MODAL ====================
function openModal(tab) {
closeModal();
_attachments = [];
_chatMode = false; // AI chat e nel widget separato ai-assistant.js
_supportSessionId = null;
_chatMessages = [];
// Nasconde FAB ARIA quando il modale segnalazioni e aperto
var aiaFab = document.getElementById('ai-chat-fab');
if (aiaFab) aiaFab.style.display = 'none';
const bg = document.createElement('div');
bg.id = 'nx-modal-bg';
bg.onclick = (e) => { if (e.target === bg) closeModal(); };
bg.innerHTML = `
<div id="nx-modal">
<div class="nx-header">
<h3 id="nx-title">Segnalazioni</h3>
<button class="nx-close" onclick="document.getElementById('nx-modal-bg')?.remove();var f=document.getElementById('ai-chat-fab');if(f)f.style.display='flex'"></button>
</div>
<div class="nx-tabs">
<button class="nx-tab" data-tab="new">Nuova</button>
<button class="nx-tab" data-tab="mine">Le Mie</button>
<button class="nx-tab" data-tab="done">Risolte</button>
<button class="nx-tab" data-tab="notif">Notifiche</button>
</div>
<div class="nx-body" id="nx-body"></div>
</div>
`;
document.body.appendChild(bg);
// Tab click
bg.querySelectorAll('.nx-tab').forEach(t => {
t.onclick = () => switchTab(t.dataset.tab);
});
switchTab(tab || 'new');
}
function closeModal() {
document.getElementById('nx-modal-bg')?.remove();
// Rimostra FAB ARIA quando il modale segnalazioni si chiude
var aiaFab = document.getElementById('ai-chat-fab');
if (aiaFab) aiaFab.style.display = 'flex';
}
function switchTab(tab) {
document.querySelectorAll('.nx-tab').forEach(t => {
t.classList.toggle('active', t.dataset.tab === tab);
});
const body = document.getElementById('nx-body');
if (!body) return;
if (tab === 'new') {
if (_chatMode && !_supportSessionId) renderChatMode(body);
else if (_chatMode && _supportSessionId) renderChatUI(body);
else renderNewForm(body);
}
else if (tab === 'mine') renderMyTickets(body, 'ACTIVE');
else if (tab === 'done') renderMyTickets(body, 'DONE');
else if (tab === 'notif') renderNotifications(body);
}
// ==================== TAB: CHAT MODE (Pre-Ticket AI Dialog) ====================
async function renderChatMode(body) {
_chatMessages = [];
_supportSessionId = null;
body.innerHTML = '<div style="text-align:center;padding:30px;color:#9ca3af">Avvio assistente...</div>';
try {
const res = await fetch(`${API}/support-sessions`, {
method: 'POST',
headers: _headers(true),
body: JSON.stringify({ product: CFG.product, callerEmail: CFG.userEmail, callerName: CFG.userName })
});
const d = await res.json();
if (d.success) {
_supportSessionId = d.data.sessionId;
_chatMessages.push({ role: 'ai', text: d.data.welcomeMessage });
} else {
_chatMessages.push({ role: 'ai', text: 'Ciao! Descrivi il problema e ti aiuto a risolverlo.' });
}
} catch(e) {
_chatMessages.push({ role: 'ai', text: 'Ciao! Descrivi il problema e ti aiuto a risolverlo.' });
}
renderChatUI(body);
}
function renderChatUI(body) {
body.style.padding = '0';
body.innerHTML = `
<div class="nx-chat-area" id="nx-chat-area"></div>
<div class="nx-chat-actions">
<button class="resolve" onclick="window._nxChatResolve()"> Risolto, grazie!</button>
<button class="classic" onclick="window._nxChatSkip()">📝 Segnalazione classica</button>
</div>
<div class="nx-chat-input">
<input type="text" id="nx-chat-input" placeholder="Scrivi il tuo problema..." autofocus>
<button id="nx-chat-send" onclick="window._nxChatSend()"></button>
</div>
`;
// Render messages
const area = document.getElementById('nx-chat-area');
_chatMessages.forEach(m => appendChatBubble(area, m.role, m.text));
area.scrollTop = area.scrollHeight;
// Enter to send
document.getElementById('nx-chat-input').addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !_chatLoading) window._nxChatSend();
});
}
function appendChatBubble(area, role, text) {
const div = document.createElement('div');
div.className = `nx-chat-msg ${role}`;
div.textContent = text;
area.appendChild(div);
}
function showTyping(area) {
const div = document.createElement('div');
div.className = 'nx-typing';
div.id = 'nx-typing';
div.innerHTML = '<span></span><span></span><span></span>';
area.appendChild(div);
area.scrollTop = area.scrollHeight;
}
function hideTyping() {
document.getElementById('nx-typing')?.remove();
}
window._nxChatSend = async () => {
const input = document.getElementById('nx-chat-input');
const text = input.value.trim();
if (!text || _chatLoading) return;
input.value = '';
_chatLoading = true;
const area = document.getElementById('nx-chat-area');
_chatMessages.push({ role: 'user', text });
appendChatBubble(area, 'user', text);
area.scrollTop = area.scrollHeight;
const sendBtn = document.getElementById('nx-chat-send');
sendBtn.disabled = true;
showTyping(area);
try {
const res = await fetch(`${API}/support-sessions/${_supportSessionId}/message`, {
method: 'POST',
headers: _headers(true),
body: JSON.stringify({ content: text })
});
const d = await res.json();
hideTyping();
if (d.success) {
const reply = d.data.reply;
_chatMessages.push({ role: 'ai', text: reply });
appendChatBubble(area, 'ai', reply);
// Check for escalation/forward action from AI tool calls
if (d.data.action) {
if (d.data.action.type === 'ESCALATED') {
const ticketId = d.data.action.data?.ticketId;
_chatMessages.push({ role: 'system', text: `Ticket #${ticketId} creato. Il team lo prendera in carico.` });
appendChatBubble(area, 'system', `Ticket #${ticketId} creato. Il team lo prendera in carico.`);
}
if (d.data.action.type === 'FORWARDED') {
const expertName = d.data.action.data?.expertName || 'esperto';
_chatMessages.push({ role: 'system', text: `Domanda inoltrata a ${expertName}. Riceverai una risposta via email.` });
appendChatBubble(area, 'system', `Domanda inoltrata a ${expertName}. Riceverai una risposta via email.`);
}
}
} else {
_chatMessages.push({ role: 'ai', text: 'Mi dispiace, non sono riuscito a elaborare la risposta. Puoi riprovare?' });
appendChatBubble(area, 'ai', 'Mi dispiace, non sono riuscito a elaborare la risposta. Puoi riprovare?');
}
} catch(e) {
hideTyping();
_chatMessages.push({ role: 'ai', text: 'Connessione non disponibile. Prova la segnalazione classica.' });
appendChatBubble(area, 'ai', 'Connessione non disponibile. Prova la segnalazione classica.');
}
area.scrollTop = area.scrollHeight;
_chatLoading = false;
sendBtn.disabled = false;
input.focus();
};
window._nxChatResolve = async () => {
if (_supportSessionId) {
try {
await fetch(`${API}/support-sessions/${_supportSessionId}/resolve`, {
method: 'POST',
headers: _headers(true),
body: JSON.stringify({ rating: 5 })
});
} catch(e) { /* silent */ }
}
const area = document.getElementById('nx-chat-area');
if (area) {
appendChatBubble(area, 'system', 'Grazie! Se hai altre domande, siamo qui. 😊');
area.scrollTop = area.scrollHeight;
}
// Reset after 2 seconds
setTimeout(() => { _supportSessionId = null; _chatMessages = []; _chatMode = true; closeModal(); }, 2000);
};
window._nxChatSkip = () => {
_chatMode = false;
const body = document.getElementById('nx-body');
if (body) {
body.style.padding = '16px 20px';
renderNewForm(body);
}
};
// ==================== TAB: NUOVA SEGNALAZIONE ====================
function renderNewForm(body) {
const ctx = getContext();
body.innerHTML = `
<div class="nx-ctx">📍 ${ctx.pageUrl.split('#')[1] || ctx.pageUrl.split('/').pop()} ${ctx.companyName ? '| 🏢 ' + ctx.companyName : ''}</div>
<div class="nx-row">
<select class="nx-select" id="nx-tipo" style="flex:1">
<option value="COMPLAINT">🐛 Bug / Errore</option>
<option value="SUPPORT_REQUEST">🎨 Interfaccia</option>
<option value="INFO_REQUEST">💡 Suggerimento</option>
<option value="OTHER"> Domanda</option>
</select>
<select class="nx-select" id="nx-prio" style="flex:1">
<option value="MEDIUM">Media</option>
<option value="LOW">Bassa</option>
<option value="HIGH">Alta</option>
<option value="URGENT">Urgente</option>
</select>
</div>
<textarea class="nx-textarea" id="nx-desc" placeholder="Descrivi il problema... (puoi incollare screenshot con Ctrl+V)"></textarea>
<div class="nx-att-list" id="nx-att-list"></div>
<div class="nx-row">
<button class="nx-btn" id="nx-voice-btn" onclick="window._nxVoice()">🎤 Parla</button>
<button class="nx-btn" id="nx-screenshot-btn" onclick="window._nxScreenshot()">📸 Screenshot</button>
<label class="nx-btn" style="cursor:pointer">
📎 Allega
<input type="file" accept="image/*,.pdf" multiple style="display:none" onchange="window._nxAttachFiles(this.files)">
</label>
</div>
<button class="nx-btn nx-btn-primary" id="nx-submit" onclick="window._nxSubmit()">Invia Segnalazione</button>
<div id="nx-result" style="display:none"></div>
`;
// Paste handler
const desc = document.getElementById('nx-desc');
desc.addEventListener('paste', handlePaste);
// Drag & drop
desc.addEventListener('dragover', (e) => { e.preventDefault(); desc.classList.add('drag-over'); });
desc.addEventListener('dragleave', () => desc.classList.remove('drag-over'));
desc.addEventListener('drop', (e) => {
e.preventDefault();
desc.classList.remove('drag-over');
if (e.dataTransfer.files.length) addFiles(e.dataTransfer.files);
});
}
// ==================== PASTE / SCREENSHOT / ATTACH ====================
function handlePaste(e) {
const items = e.clipboardData?.items;
if (!items) return;
for (const item of items) {
if (item.type.startsWith('image/')) {
e.preventDefault();
const blob = item.getAsFile();
blobToAttachment(blob, `screenshot_${Date.now()}.png`);
}
}
}
function blobToAttachment(blob, filename) {
const reader = new FileReader();
reader.onload = () => {
_attachments.push({
filename: filename,
mimeType: blob.type,
size: blob.size,
data: reader.result.split(',')[1], // base64 without prefix
preview: reader.result
});
renderAttachments();
};
reader.readAsDataURL(blob);
}
function addFiles(files) {
for (const file of files) {
if (_attachments.length >= 10) break;
blobToAttachment(file, file.name);
}
}
window._nxAttachFiles = (files) => addFiles(files);
window._nxScreenshot = async () => {
const btn = document.getElementById('nx-screenshot-btn');
try {
// Hide modal temporarily
const modal = document.getElementById('nx-modal-bg');
if (modal) modal.style.display = 'none';
btn.textContent = '⏳ Cattura...';
// Wait a frame for modal to hide
await new Promise(r => setTimeout(r, 100));
// Use html2canvas if available, otherwise use Screen Capture API
if (typeof html2canvas !== 'undefined') {
const canvas = await html2canvas(document.body, { useCORS: true, scale: 1 });
canvas.toBlob((blob) => {
if (blob) blobToAttachment(blob, `screenshot_${Date.now()}.png`);
if (modal) modal.style.display = 'flex';
btn.textContent = '📸 Screenshot';
}, 'image/png');
} else if (navigator.mediaDevices?.getDisplayMedia) {
const stream = await navigator.mediaDevices.getDisplayMedia({ video: { mediaSource: 'screen' } });
const track = stream.getVideoTracks()[0];
const imageCapture = new ImageCapture(track);
const bitmap = await imageCapture.grabFrame();
track.stop();
const canvas = document.createElement('canvas');
canvas.width = bitmap.width;
canvas.height = bitmap.height;
canvas.getContext('2d').drawImage(bitmap, 0, 0);
canvas.toBlob((blob) => {
if (blob) blobToAttachment(blob, `screenshot_${Date.now()}.png`);
if (modal) modal.style.display = 'flex';
btn.textContent = '📸 Screenshot';
}, 'image/png');
} else {
// Fallback: canvas from body (basic)
if (modal) modal.style.display = 'flex';
btn.textContent = '📸 Screenshot';
alert('Screenshot non supportato in questo browser. Usa Ctrl+V per incollare.');
}
} catch(err) {
const modal = document.getElementById('nx-modal-bg');
if (modal) modal.style.display = 'flex';
btn.textContent = '📸 Screenshot';
if (err.name !== 'NotAllowedError') {
console.warn('Screenshot error:', err);
}
}
};
function renderAttachments() {
const list = document.getElementById('nx-att-list');
if (!list) return;
list.innerHTML = _attachments.map((a, i) => `
<div class="nx-att-item">
<img src="${a.preview}" alt="${a.filename}">
<button class="nx-att-remove" onclick="window._nxRemoveAtt(${i})"></button>
</div>
`).join('');
}
window._nxRemoveAtt = (i) => { _attachments.splice(i, 1); renderAttachments(); };
// ==================== VOICE ====================
window._nxVoice = () => {
if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
alert('Riconoscimento vocale non supportato. Usa Chrome.'); return;
}
const recognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)();
recognition.lang = CFG.lang === 'it' ? 'it-IT' : CFG.lang + '-' + CFG.lang.toUpperCase();
recognition.continuous = false;
const btn = document.getElementById('nx-voice-btn');
btn.textContent = '🔴 Parla ora...';
btn.style.background = '#fee2e2';
recognition.onresult = (e) => {
const desc = document.getElementById('nx-desc');
desc.value += (desc.value ? '\n' : '') + e.results[0][0].transcript;
btn.textContent = '🎤 Parla'; btn.style.background = '';
};
recognition.onerror = recognition.onend = () => { btn.textContent = '🎤 Parla'; btn.style.background = ''; };
recognition.start();
};
// ==================== SUBMIT ====================
window._nxSubmit = async () => {
const desc = document.getElementById('nx-desc')?.value?.trim();
const tipo = document.getElementById('nx-tipo')?.value;
const prio = document.getElementById('nx-prio')?.value;
const submitBtn = document.getElementById('nx-submit');
const result = document.getElementById('nx-result');
if (!desc && _attachments.length === 0) { alert('Descrivi il problema o allega uno screenshot'); return; }
submitBtn.disabled = true;
submitBtn.textContent = 'Invio in corso...';
const ctx = getContext();
const versionTag = `[v${WIDGET_VERSION} build ${WIDGET_BUILD}]`;
const payload = {
transcription: desc ? `${desc}\n\n${versionTag}` : `Segnalazione con screenshot ${versionTag}`,
callerName: CFG.userName || ctx.userName,
callerEmail: CFG.userEmail || ctx.userEmail,
kind: tipo,
priority: prio,
queue: 'SUPPORT',
product: CFG.product,
pageUrl: ctx.pageUrl,
reporterRole: CFG.userRole || ctx.userRole,
lang: CFG.lang,
appVersion: WIDGET_VERSION,
appBuild: WIDGET_BUILD
};
try {
// 1. Create ticket
const res = await fetch(`${API}/tickets/voice-report`, {
method: 'POST',
headers: _headers(true),
body: JSON.stringify(payload)
});
const data = await res.json();
if (data.success) {
const ticketId = data.data.ticket.id;
// 2. Attach files
if (_attachments.length > 0) {
await fetch(`${API}/tickets/${ticketId}/attachments-batch`, {
method: 'POST',
headers: _headers(true),
body: JSON.stringify({ attachments: _attachments.map(a => ({ filename: a.filename, mimeType: a.mimeType, size: a.size, data: a.data })) })
});
}
result.className = 'nx-result ok';
result.style.display = 'block';
result.innerHTML = `✅ Segnalazione #${ticketId} inviata!<br><small>${data.data.ticket.aiResponse || 'Grazie, il team la prendera in carico.'}</small>`;
submitBtn.textContent = '✓ Inviato';
// Play audio if available
if (data.data.voice?.audioBase64) {
try { new Audio(data.data.voice.audioBase64).play(); } catch(e) {}
}
} else {
throw new Error(data.error?.message || 'Errore');
}
} catch(err) {
result.className = 'nx-result err';
result.style.display = 'block';
result.textContent = '❌ ' + (err.message || 'Servizio non raggiungibile');
submitBtn.disabled = false;
submitBtn.textContent = 'Invia Segnalazione';
}
};
// ==================== TAB: MIE SEGNALAZIONI / RISOLTE ====================
async function renderMyTickets(body, statusFilter) {
body.innerHTML = '<div class="nx-empty">Caricamento...</div>';
const email = CFG.userEmail;
if (!email) { body.innerHTML = '<div class="nx-empty">Email utente non configurata</div>'; return; }
try {
const res = await fetch(`${API}/tickets/my?callerEmail=${encodeURIComponent(email)}&status=${statusFilter}&product=${CFG.product}`, {
headers: _headers()
});
const data = await res.json();
if (!data.success || !data.data.tickets.length) {
body.innerHTML = `<div class="nx-empty">${statusFilter === 'DONE' ? 'Nessuna segnalazione risolta' : 'Nessuna segnalazione aperta'}</div>`;
return;
}
body.innerHTML = data.data.tickets.map(t => `
<div class="nx-ticket" onclick="window._nxToggleTicket(this)">
<div class="nx-ticket-head">
<span class="nx-ticket-id">#${t.id}</span>
<span class="nx-ticket-status nx-status-${t.status}">${t.status.replace('_', ' ')}</span>
</div>
<div class="nx-ticket-subject">${t.subject || t.operativeSummary?.substring(0, 80) || 'Senza oggetto'}</div>
<div class="nx-ticket-meta">${t.priority} | ${timeAgo(t.createdAt)}</div>
${t.aiResponse ? `<div class="nx-ticket-ai" style="display:none">💬 ${t.aiResponse}</div>` : ''}
${t.messages?.length ? `<div class="nx-ticket-ai" style="display:none">📝 ${t.messages.map(m => `<b>${m.role}</b>: ${m.content}`).join('<br>')}</div>` : ''}
${t.status === 'RESOLVED' ? `<button class="nx-reopen-btn" onclick="event.stopPropagation(); window._nxReopenTicket(${t.id}, this)" style="margin-top:8px;width:100%;padding:8px;border:1px solid #ef4444;border-radius:8px;background:#fef2f2;color:#dc2626;font-size:13px;font-weight:600;cursor:pointer">Non risolto — Riapri</button>` : ''}
</div>
`).join('');
} catch(err) {
body.innerHTML = '<div class="nx-empty">Servizio non raggiungibile</div>';
}
}
window._nxToggleTicket = (el) => {
el.querySelectorAll('.nx-ticket-ai').forEach(d => {
d.style.display = d.style.display === 'none' ? 'block' : 'none';
});
};
window._nxReopenTicket = async (ticketId, btn) => {
btn.disabled = true;
btn.textContent = 'Riapertura...';
try {
const res = await fetch(`${API}/tickets/${ticketId}/status`, {
method: 'PATCH',
headers: _headers(true),
body: JSON.stringify({ status: 'OPEN', performedBy: CFG.userEmail || 'utente' })
});
const data = await res.json();
if (data.success) {
btn.textContent = 'Riaperto!';
btn.style.background = '#d1fae5';
btn.style.color = '#065f46';
btn.style.borderColor = '#10b981';
// Aggiunge un messaggio al ticket con la segnalazione dell'utente
await fetch(`${API}/tickets/${ticketId}/message`, {
method: 'POST',
headers: _headers(true),
body: JSON.stringify({ content: 'Riaperto dall\'utente: il problema non era risolto.', role: 'USER' })
});
setTimeout(() => renderTickets(btn.closest('.nx-body') || document.querySelector('.nx-body')), 1000);
} else {
btn.textContent = data.error?.message || 'Errore';
btn.style.background = '#fee2e2';
}
} catch(e) {
btn.textContent = 'Errore connessione';
}
};
// ==================== TAB: NOTIFICHE ====================
async function renderNotifications(body) {
body.innerHTML = '<div class="nx-empty">Caricamento...</div>';
try {
const res = await fetch(`${API}/notifications?product=${CFG.product}&limit=30`, {
headers: _headers()
});
const data = await res.json();
if (!data.success || !data.data.notifications.length) {
body.innerHTML = '<div class="nx-empty">Nessuna notifica</div>';
return;
}
let html = `<div style="text-align:right;margin-bottom:8px">
<button class="nx-btn" style="font-size:12px;padding:4px 10px" onclick="window._nxReadAll()">Segna tutte lette</button>
</div>`;
html += data.data.notifications.map(n => `
<div class="nx-notif ${n.readAt ? '' : 'unread'}" onclick="window._nxReadNotif(${n.id}, this)">
<div class="nx-notif-title"><span class="nx-notif-dot ${n.type}"></span>${n.title}</div>
${n.body ? `<div class="nx-notif-body">${n.body}</div>` : ''}
<div class="nx-notif-meta">${n.type} | ${timeAgo(n.createdAt)}</div>
</div>
`).join('');
body.innerHTML = html;
} catch(err) {
body.innerHTML = '<div class="nx-empty">Servizio non raggiungibile</div>';
}
}
window._nxReadNotif = async (id, el) => {
try {
await fetch(`${API}/notifications/${id}/read`, {
method: 'PUT', headers: _headers()
});
el.classList.remove('unread');
pollNotifications();
} catch(e) {}
};
window._nxReadAll = async () => {
try {
await fetch(`${API}/notifications/read-all?product=${CFG.product}`, {
method: 'PUT', headers: _headers()
});
document.querySelectorAll('.nx-notif.unread').forEach(el => el.classList.remove('unread'));
pollNotifications();
} catch(e) {}
};
// ==================== UTILS ====================
function getContext() {
return {
pageUrl: window.location.href,
userName: CFG.userName || (typeof api !== 'undefined' && api.user?.name) || '',
userEmail: CFG.userEmail || (typeof api !== 'undefined' && api.user?.email) || '',
userRole: CFG.userRole || (typeof api !== 'undefined' && api.user?.role) || '',
companyName: (typeof api !== 'undefined' && api.companies?.[0]?.name) || ''
};
}
function timeAgo(dateStr) {
const diff = (Date.now() - new Date(dateStr).getTime()) / 1000;
if (diff < 60) return 'ora';
if (diff < 3600) return Math.floor(diff / 60) + ' min fa';
if (diff < 86400) return Math.floor(diff / 3600) + ' ore fa';
return Math.floor(diff / 86400) + ' giorni fa';
}
// ==================== MAINTENANCE CHECK (polling maintenance.json) ====================
var _maintOverlay = null;
function checkMaintenance() {
fetch('/maintenance.json?_=' + Date.now()).then(function(r) { return r.json(); }).then(function(d) {
if (d && d.active === true) {
if (!_maintOverlay) {
_maintOverlay = document.createElement('div');
_maintOverlay.id = 'agile-maint-overlay';
_maintOverlay.style.cssText = 'position:fixed;inset:0;z-index:99999;background:rgba(10,11,15,0.95);display:flex;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif';
_maintOverlay.innerHTML = '<div style="text-align:center;max-width:400px;padding:32px">'
+ '<div style="font-size:48px;margin-bottom:16px">🔧</div>'
+ '<h2 style="color:#fff;font-size:22px;font-weight:700;margin:0 0 12px">Aggiornamento in corso</h2>'
+ '<p style="color:#9ca3af;font-size:15px;line-height:1.6;margin:0 0 20px">Stiamo applicando un miglioramento al sistema. Torneremo operativi tra pochi istanti.</p>'
+ '<div style="width:48px;height:48px;border:3px solid rgba(124,58,237,0.3);border-top-color:#7c3aed;border-radius:50%;margin:0 auto;animation:agile-spin 1s linear infinite"></div>'
+ '<style>@keyframes agile-spin{to{transform:rotate(360deg)}}</style>'
+ '</div>';
document.body.appendChild(_maintOverlay);
}
} else {
if (_maintOverlay) { _maintOverlay.remove(); _maintOverlay = null; }
}
}).catch(function() {
if (_maintOverlay) { _maintOverlay.remove(); _maintOverlay = null; }
});
}
setInterval(checkMaintenance, 10000);
setTimeout(checkMaintenance, 3000);
// ==================== START ====================
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

View File

@ -214,6 +214,7 @@ function loadSidebar() {
{
label: 'Sistema', i18nKey: 'nav.system',
items: [
{ name: 'Guida all\'uso', href: 'guida.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/></svg>` },
{ name: 'Impostazioni', href: 'settings.html', icon: iconCog(), i18nKey: 'nav.settings' },
{ name: 'Architettura', href: 'architecture.html', icon: iconCubeTransparent() },
{ name: 'Simulazione Demo', href: 'simulate.html', icon: `<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>` },

View File

@ -207,11 +207,13 @@ const HelpSystem = (function () {
}
],
references: [
'Art. 23.1 - Obbligo di notifica degli incidenti significativi',
'Art. 23.4 (a) - Early warning entro 24 ore',
'Art. 23.4 (b) - Notifica entro 72 ore',
'Art. 23.4 (d) - Relazione finale entro un mese',
'Art. 23.3 - Definizione di incidente significativo'
'Direttiva (UE) 2022/2555 - Art. 23.1 - Obbligo di notifica degli incidenti significativi',
'Direttiva (UE) 2022/2555 - Art. 23.4 (a) - Early warning entro 24 ore',
'Direttiva (UE) 2022/2555 - Art. 23.4 (b) - Notifica entro 72 ore',
'Direttiva (UE) 2022/2555 - Art. 23.4 (d) - Relazione finale entro un mese',
'D.Lgs. 4 settembre 2024, n. 138 - Art. 23 - Notifica degli incidenti (recepimento NIS2)',
'Determinazione ACN n. 164179 del 14/04/2025 - Classificazione incidenti significativi (Allegato 3 soggetti essenziali, Allegato 4 soggetti importanti) e tipologie IS-1/IS-2/IS-3/IS-4',
'Determinazione ACN n. 333017/2025 - Piattaforma digitale ACN per le notifiche'
]
},
@ -381,6 +383,15 @@ const HelpSystem = (function () {
'Il livello di criticita\' influenza la valutazione dei rischi associati.'
]
},
{
heading: 'Rilevanza NIS2 (scoring 0-100)',
items: [
'Il pulsante <strong>Valuta Rilevanza NIS2</strong> applica una metodologia di scoring documentata a 6 criteri pesati: Criticita Operativa (0-25), Impatto Interruzione (0-25), Dati Trattati (0-20), Dipendenze (0-15), Esposizione (0-10), Obblighi Normativi (0-5).',
'Un sistema e considerato <strong>rilevante NIS2 quando il punteggio &ge; 40</strong>. Classi: &ge;80 Critico, 60-79 Alto, 40-59 Medio, 20-39 Basso, &lt;20 Trascurabile.',
'Il punteggio aggiorna automaticamente anche la criticita dell\'asset e alimenta il registro formale dei <strong>Sistemi Rilevanti</strong>.',
'La metodologia supporta il requisito di censimento e classificazione dei sistemi informativi e di rete rilevanti, da approvare a livello di Direzione.'
]
},
{
heading: 'Mappa delle Dipendenze',
items: [
@ -399,10 +410,11 @@ const HelpSystem = (function () {
}
],
references: [
'Art. 21.2 (i) - Sicurezza delle risorse umane, politiche di controllo dell\'accesso e gestione degli attivi',
'Art. 21.2 (a) - Politiche di analisi dei rischi e di sicurezza dei sistemi informatici',
'Art. 21.2 (c) - Continuita\' operativa, gestione dei backup e ripristino in caso di disastro',
'Considerando 79 - Adeguatezza delle misure rispetto ai rischi per le reti e i sistemi informativi'
'Direttiva (UE) 2022/2555 - Art. 21.2 (i) - Sicurezza delle risorse umane, controllo degli accessi e gestione degli attivi',
'Direttiva (UE) 2022/2555 - Art. 21.2 (a) - Politiche di analisi dei rischi e di sicurezza dei sistemi informatici',
'Direttiva (UE) 2022/2555 - Art. 21.2 (c) - Continuita\' operativa, gestione dei backup e ripristino',
'D.Lgs. 4 settembre 2024, n. 138 - Art. 24 - Obblighi in materia di misure di gestione del rischio',
'Identificazione e classificazione dei sistemi rilevanti - metodologia di scoring 0-100 approvata dalla Direzione'
]
},
@ -904,6 +916,22 @@ const HelpSystem = (function () {
// ── Rendering del contenuto HTML di help ─────────────────────────
// Mapping pageId -> ancora capitolo nella Guida completa (guida.html)
var _guideAnchor = {
'dashboard': 'cap-4',
'assessment': 'cap-5',
'risks': 'cap-6',
'incidents': 'cap-7',
'policies': 'cap-8',
'supply-chain': 'cap-9',
'training': 'cap-10',
'assets': 'cap-11',
'reports': 'cap-12',
'feedback': 'cap-13',
'workflow': 'cap-4',
'settings': 'cap-3'
};
function _renderHelpContent(pageId) {
var data = _helpContent[pageId];
if (!data) return null;
@ -936,6 +964,15 @@ const HelpSystem = (function () {
html += '</div>';
}
// Link alla guida completa (per utenti che vogliono approfondire)
var anchor = _guideAnchor[pageId] || '';
var guideHref = 'guida.html' + (anchor ? '#' + anchor : '');
html += '<div style="margin-top:20px;padding:14px 18px;background:#eff6ff;border-left:4px solid #1e40af;border-radius:6px;">';
html += '<strong style="color:#1e40af;">Sei nuovo?</strong> ';
html += 'Apri la <a href="' + guideHref + '" style="color:#1e40af;font-weight:600;">Guida all\'uso completa</a> ';
html += 'con spiegazioni in parole semplici, esempi pratici e cosa dice la norma.';
html += '</div>';
html += '</div>';
return { title: data.title, content: html };

230
public/js/kb.js Normal file
View File

@ -0,0 +1,230 @@
/**
* NIS2 Agile - Knowledge Base UI (Migration 012-014)
*
* Gestisce upload documento, listing, search semantica con visibilita' multi-livello.
*/
(function () {
'use strict';
function getJwt() {
try {
return localStorage.getItem('access_token') || sessionStorage.getItem('access_token') || '';
} catch (e) { return ''; }
}
function authHeaders() {
return {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + getJwt(),
};
}
function escHtml(s) {
var d = document.createElement('div');
d.textContent = s == null ? '' : String(s);
return d.innerHTML;
}
function fmtScopeBadge(scope) {
return '<span class="kb-scope-badge ' + escHtml(scope) + '">' + escHtml(scope) + '</span>';
}
var btnUpload = document.getElementById('btn-kb-upload');
var btnSubmit = document.getElementById('btn-kb-submit');
var btnCancel = document.getElementById('btn-kb-cancel');
var btnSearch = document.getElementById('btn-kb-search');
var formCard = document.getElementById('kb-form-card');
var statusEl = document.getElementById('kb-status');
var listEl = document.getElementById('kb-doc-list');
var resultsEl = document.getElementById('kb-search-results');
var shareSel = document.getElementById('kb-shared-with');
if (!btnUpload || !formCard) return;
function setupScopeForUser() {
// Decodifica JWT per estrarre role e consulting_firm_id
var jwt = getJwt();
if (!jwt) return;
try {
var payload = JSON.parse(atob(jwt.split('.')[1]));
// Il JWT NIS2 contiene solo user_id; chiamo /api/auth/me per role + firm
fetch('/api/auth/me', { headers: authHeaders() })
.then(function (r) { return r.json(); })
.then(function (res) {
var u = (res && res.data && (res.data.user || res.data)) || {};
var role = u.role || '';
var firmId = u.consulting_firm_id || null;
var optSystem = document.querySelector('[data-scope-opt="SYSTEM"]');
var optFirm = document.querySelector('[data-scope-opt="FIRM"]');
var optOrg = document.querySelector('[data-scope-opt="ORG"]');
if (role === 'super_admin' && optSystem) optSystem.style.display = 'inline-flex';
if (firmId && optFirm) optFirm.style.display = 'inline-flex';
if (optOrg) optOrg.style.display = 'inline-flex';
if (firmId) loadFirmOrgs();
});
} catch (e) { /* ignore */ }
}
function loadFirmOrgs() {
if (!shareSel) return;
fetch('/api/knowledgebase/firmOrgs', { headers: authHeaders() })
.then(function (r) { return r.json(); })
.then(function (res) {
var orgs = (res && res.data && res.data.organizations) || [];
if (!orgs.length) {
shareSel.innerHTML = '<option value="">Nessuna organizzazione nello studio</option>';
return;
}
shareSel.innerHTML = orgs.map(function (o) {
var label = o.name + (o.vat_number ? ' (P.IVA ' + o.vat_number + ')' : '');
return '<option value="' + o.id + '">' + escHtml(label) + '</option>';
}).join('');
}).catch(function () {
shareSel.innerHTML = '<option value="">Errore caricamento organizzazioni</option>';
});
}
function loadDocList() {
if (!listEl) return;
listEl.innerHTML = '<div class="text-muted">Caricamento...</div>';
fetch('/api/knowledgebase/list', { headers: authHeaders() })
.then(function (r) { return r.json(); })
.then(function (res) {
var docs = (res && res.data && res.data.documents) || [];
if (!docs.length) {
listEl.innerHTML = '<div class="text-muted">Nessun documento visibile</div>';
return;
}
listEl.innerHTML = docs.map(function (d) {
return '<div class="kb-doc-row">' +
'<div>' +
'<strong>' + escHtml(d.title) + '</strong> ' + fmtScopeBadge(d.scope) +
'<div style="font-size:.78rem;color:var(--gray-500);">' +
escHtml(d.entity_type || '') + ' &middot; ' + (d.chunk_count || 0) + ' chunk &middot; ' + escHtml(d.created_at) +
'</div>' +
'</div>' +
'<button class="btn btn-sm btn-danger" data-del-id="' + d.id + '">Elimina</button>' +
'</div>';
}).join('');
// Wire delete buttons
listEl.querySelectorAll('[data-del-id]').forEach(function (btn) {
btn.addEventListener('click', function () {
if (!confirm('Eliminare definitivamente questo documento dalla KB?')) return;
var id = this.getAttribute('data-del-id');
fetch('/api/knowledgebase/' + id, {
method: 'DELETE',
headers: authHeaders(),
}).then(function (r) { return r.json(); }).then(function (res) {
if (res && res.success) loadDocList();
else alert('Errore: ' + ((res && res.message) || 'sconosciuto'));
});
});
});
}).catch(function () {
listEl.innerHTML = '<div class="text-danger">Errore caricamento</div>';
});
}
btnUpload.addEventListener('click', function () {
var visible = formCard.style.display !== 'none';
formCard.style.display = visible ? 'none' : 'block';
if (!visible) setupScopeForUser();
});
btnCancel.addEventListener('click', function () { formCard.style.display = 'none'; });
document.querySelectorAll('input[name="kb-scope"]').forEach(function (r) {
r.addEventListener('change', function () {
var block = document.getElementById('kb-share-block');
if (block) block.style.display = (this.value === 'FIRM') ? 'block' : 'none';
});
});
btnSubmit.addEventListener('click', function () {
var title = (document.getElementById('kb-title') || {}).value || '';
var text = (document.getElementById('kb-text') || {}).value || '';
var entityType = (document.getElementById('kb-entity-type') || {}).value || 'custom';
var scopeEl = document.querySelector('input[name="kb-scope"]:checked');
var scope = scopeEl ? scopeEl.value : 'ORG';
if (!title.trim()) { statusEl.textContent = 'Inserisci un titolo'; statusEl.style.color = '#ef4444'; return; }
if (text.length < 50) { statusEl.textContent = 'Testo troppo breve (min 50 caratteri)'; statusEl.style.color = '#ef4444'; return; }
var sharedWith = [];
if (scope === 'FIRM' && shareSel) {
for (var i = 0; i < shareSel.options.length; i++) {
if (shareSel.options[i].selected && shareSel.options[i].value) {
sharedWith.push(parseInt(shareSel.options[i].value, 10));
}
}
}
btnSubmit.disabled = true;
statusEl.style.color = '#0ea5e9';
statusEl.textContent = 'Indicizzazione in corso...';
fetch('/api/knowledgebase/ingest', {
method: 'POST',
headers: authHeaders(),
body: JSON.stringify({
title: title.trim(),
text: text,
entity_type: entityType,
scope: scope,
shared_with_orgs: sharedWith,
}),
}).then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
.then(function (res) {
btnSubmit.disabled = false;
if (res.ok && res.body && res.body.success !== false) {
var d = res.body.data || res.body;
statusEl.style.color = '#22C55E';
statusEl.textContent = 'Documento indicizzato (' + (d.scope || scope) + ', ' + (d.chunks || '?') + ' chunk)' +
(sharedWith.length ? ' — condiviso con ' + sharedWith.length + ' org.' : '');
document.getElementById('kb-title').value = '';
document.getElementById('kb-text').value = '';
loadDocList();
} else {
statusEl.style.color = '#ef4444';
statusEl.textContent = 'Errore: ' + ((res.body && res.body.message) || 'sconosciuto');
}
}).catch(function (e) {
btnSubmit.disabled = false;
statusEl.style.color = '#ef4444';
statusEl.textContent = 'Errore di rete: ' + (e.message || '');
});
});
btnSearch.addEventListener('click', function () {
var q = (document.getElementById('kb-search-query') || {}).value || '';
if (!q.trim()) return;
resultsEl.innerHTML = '<div class="text-muted">Ricerca in corso...</div>';
fetch('/api/knowledgebase/search', {
method: 'POST',
headers: authHeaders(),
body: JSON.stringify({ query: q.trim(), top_k: 5 }),
}).then(function (r) { return r.json(); })
.then(function (res) {
var hits = (res && res.data && res.data.results) || [];
if (!hits.length) {
resultsEl.innerHTML = '<div class="text-muted">Nessun risultato</div>';
return;
}
resultsEl.innerHTML = hits.map(function (h, i) {
return '<div style="padding:10px;border:1px solid var(--gray-100);border-radius:6px;margin-bottom:8px;">' +
'<div><strong>[' + (i + 1) + '] ' + escHtml(h.title) + '</strong> ' +
fmtScopeBadge(h.scope) + ' <span style="font-size:.72rem;color:var(--gray-500);">score=' + h.score + '</span></div>' +
'<div style="font-size:.84rem;color:var(--gray-700);margin-top:6px;line-height:1.4;">' +
escHtml(h.content.substring(0, 300)) + (h.content.length > 300 ? '...' : '') +
'</div></div>';
}).join('');
});
});
// Carica lista al boot
loadDocList();
})();

134
public/kb.html Normal file
View File

@ -0,0 +1,134 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Knowledge Base - NIS2 Agile</title>
<link rel="stylesheet" href="css/style.css">
<style>
.kb-scope-radios { display: flex; gap: 18px; flex-wrap: wrap; font-size: .9rem; }
.kb-scope-radios label { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; }
.kb-share-block { margin-top: 12px; }
.kb-doc-list { margin-top: 24px; }
.kb-doc-row { padding: 10px 12px; border-bottom: 1px solid var(--gray-100); display: flex; justify-content: space-between; align-items: center; }
.kb-scope-badge { font-size: .72rem; padding: 2px 8px; border-radius: 10px; font-weight: 600; }
.kb-scope-badge.SYSTEM { background: #DBEAFE; color: #1E40AF; }
.kb-scope-badge.FIRM { background: #FEF3C7; color: #92400E; }
.kb-scope-badge.ORG { background: #DCFCE7; color: #166534; }
</style>
</head>
<body>
<div class="app-layout">
<aside class="sidebar" id="sidebar"></aside>
<main class="main-content">
<header class="content-header">
<h2>Knowledge Base</h2>
<div class="content-header-actions">
<button class="btn btn-primary" id="btn-kb-upload">
<svg viewBox="0 0 20 20" fill="currentColor" style="width:18px;height:18px;vertical-align:middle;"><path d="M5.5 13a3.5 3.5 0 01-.369-6.98 4 4 0 117.753-1.977A4.5 4.5 0 1113.5 13H11V9.413l1.293 1.293a1 1 0 001.414-1.414l-3-3a1 1 0 00-1.414 0l-3 3a1 1 0 001.414 1.414L9 9.414V13H5.5z"/><path d="M9 13h2v5a1 1 0 11-2 0v-5z"/></svg>
Carica documento
</button>
</div>
</header>
<div class="content-body">
<p class="text-muted" style="margin-top:0;">
Carica documenti (procedure, policy, normative, training material) che l'AI usera' per
rispondere alle domande con citazioni precise. La visibilita' segue il modello a 3 livelli:
<strong>SYSTEM</strong> (vendor), <strong>FIRM</strong> (studio + condivisioni esplicite),
<strong>ORG</strong> (singola organizzazione cliente).
</p>
<!-- Form upload (hidden by default) -->
<div class="card" id="kb-form-card" style="display:none;margin-top:16px;">
<div class="card-body">
<div class="form-grid" style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
<div class="form-group">
<label for="kb-title">Titolo</label>
<input type="text" id="kb-title" class="form-control" placeholder="Es: Procedura interna gestione incidenti NIS2">
</div>
<div class="form-group">
<label for="kb-entity-type">Tipo</label>
<select id="kb-entity-type" class="form-control">
<option value="custom">Procedura interna</option>
<option value="platform_guide">Linea guida</option>
<option value="csrd">Normativa</option>
<option value="training">Training</option>
</select>
</div>
</div>
<div class="form-group" style="margin-top:12px;">
<label for="kb-text">Contenuto (testo libero, 50&ndash;50.000 caratteri)</label>
<textarea id="kb-text" rows="8" class="form-control"
placeholder="Incolla qui il testo della procedura, della checklist o del template..."></textarea>
</div>
<div style="margin-top:12px;border-top:1px solid var(--gray-100);padding-top:12px;">
<label style="font-weight:600;font-size:.85rem;">Visibilita'</label>
<div class="kb-scope-radios" style="margin-top:6px;">
<label data-scope-opt="SYSTEM" style="display:none;">
<input type="radio" name="kb-scope" value="SYSTEM"> Sistema (super_admin)
</label>
<label data-scope-opt="FIRM" style="display:none;">
<input type="radio" name="kb-scope" value="FIRM"> Studio + condivisioni
</label>
<label data-scope-opt="ORG">
<input type="radio" name="kb-scope" value="ORG" checked> Solo organizzazione corrente
</label>
</div>
</div>
<div id="kb-share-block" class="kb-share-block" style="display:none;">
<label style="font-size:.78rem;color:var(--gray-600);">
Organizzazioni clienti destinatarie (Ctrl/Cmd per selezione multipla, opzionale):
</label>
<select id="kb-shared-with" multiple size="5" class="form-control">
<option value="">Caricamento organizzazioni del firm...</option>
</select>
<div style="font-size:.72rem;color:var(--gray-500);margin-top:4px;">
Lascia vuoto per condividere solo con i collaboratori dello studio.
</div>
</div>
<div style="margin-top:16px;display:flex;gap:8px;align-items:center;">
<button class="btn btn-primary" id="btn-kb-submit">Indicizza documento</button>
<button class="btn btn-secondary" id="btn-kb-cancel">Annulla</button>
<span id="kb-status" style="margin-left:8px;font-size:.85rem;"></span>
</div>
</div>
</div>
<!-- Lista documenti -->
<div class="card kb-doc-list">
<div class="card-header">
<h3>Documenti caricati visibili</h3>
</div>
<div class="card-body">
<div id="kb-doc-list">
<div class="text-muted">Caricamento...</div>
</div>
</div>
</div>
<!-- Search preview -->
<div class="card" style="margin-top:24px;">
<div class="card-header"><h3>Test search semantica</h3></div>
<div class="card-body">
<div style="display:flex;gap:8px;">
<input type="text" id="kb-search-query" class="form-control" placeholder="Es: come gestire un incidente NIS2?">
<button class="btn btn-secondary" id="btn-kb-search">Cerca</button>
</div>
<div id="kb-search-results" style="margin-top:12px;"></div>
</div>
</div>
</div>
</main>
</div>
<script src="js/common.js"></script>
<script src="js/api.js"></script>
<script src="js/kb.js"></script>
</body>
</html>

View File

@ -0,0 +1,189 @@
/*
* Mobile Conversion Layer Agile Technology suite
* Riutilizzabile su EViX, QSA, NIS2, TRPG, LG231, ecc.
* Si attiva SOLO su mobile (768px) desktop invariato.
* Classi prefissate .mobconv-* per zero conflitti con CSS host.
* */
/* ── Variabili di tema (override via inline style sul container) ── */
:root {
--mobconv-accent: #E31E24;
--mobconv-accent2: #B71518;
--mobconv-bg: #0A0A0A;
--mobconv-text: #FFFFFF;
--mobconv-sub: rgba(255,255,255,0.75);
--mobconv-border: rgba(255,255,255,0.12);
}
/* ── Sticky CTA bar (bottom) — visibile solo mobile ────────────── */
.mobconv-sticky {
display: none; /* nascosta di default */
position: fixed;
left: 0; right: 0;
bottom: 0;
z-index: 9990;
background: linear-gradient(180deg, rgba(10,10,10,0.96), rgba(10,10,10,1));
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
border-top: 1px solid var(--mobconv-border);
padding: 10px 16px;
padding-bottom: calc(10px + env(safe-area-inset-bottom, 0px));
box-shadow: 0 -8px 32px rgba(0,0,0,0.45);
transform: translateY(100%);
transition: transform .28s ease;
}
.mobconv-sticky-row { max-width: 540px; margin: 0 auto; }
.mobconv-cta { max-width: 100%; }
/* iPhone XS / SE: padding ridotto per non sovrastare */
@media (max-width: 380px) {
.mobconv-sticky { padding: 8px 12px; padding-bottom: calc(8px + env(safe-area-inset-bottom, 0px)); }
.mobconv-cta { font-size: 14px !important; padding: 0 12px !important; min-height: 44px !important; }
.mobconv-pill { min-height: 44px !important; min-width: 44px !important; }
}
.mobconv-sticky.show { transform: translateY(0); }
.mobconv-sticky-row {
display: flex;
gap: 8px;
align-items: stretch;
}
.mobconv-pill {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 48px;
min-height: 48px;
padding: 0 12px;
border-radius: 14px;
background: rgba(255,255,255,0.08);
color: var(--mobconv-text);
border: 1px solid var(--mobconv-border);
text-decoration: none;
font-size: 18px;
transition: background .15s, transform .15s;
}
.mobconv-pill:active { transform: scale(0.96); background: rgba(255,255,255,0.14); }
.mobconv-pill.wa { background: rgba(37,211,102,0.18); border-color: rgba(37,211,102,0.45); color: #25D366; }
.mobconv-cta {
flex: 1 1 auto;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 48px;
padding: 0 16px;
border-radius: 14px;
background: linear-gradient(135deg, var(--mobconv-accent), var(--mobconv-accent2));
color: #FFFFFF;
text-decoration: none;
font-weight: 700;
font-size: 14.5px;
letter-spacing: 0.01em;
box-shadow: 0 8px 24px rgba(0,0,0,0.35), 0 0 0 1px rgba(255,255,255,0.06) inset;
transition: transform .15s;
}
.mobconv-cta:active { transform: scale(0.98); }
/* ── WhatsApp FAB (alternativa standalone — usabile su Diamante-like) ── */
.mobconv-wa-fab {
display: none;
position: fixed;
right: 16px;
bottom: calc(20px + env(safe-area-inset-bottom, 0px));
z-index: 9980;
width: 56px;
height: 56px;
border-radius: 50%;
background: #25D366;
color: white;
align-items: center;
justify-content: center;
text-decoration: none;
font-size: 26px;
box-shadow: 0 8px 28px rgba(37,211,102,0.45);
transition: transform .2s, box-shadow .2s;
animation: mobconv-pulse 2.4s ease-out infinite;
}
.mobconv-wa-fab:active { transform: scale(0.92); }
@keyframes mobconv-pulse {
0% { box-shadow: 0 8px 28px rgba(37,211,102,0.45), 0 0 0 0 rgba(37,211,102,0.55); }
70% { box-shadow: 0 8px 28px rgba(37,211,102,0.45), 0 0 0 18px rgba(37,211,102,0); }
100% { box-shadow: 0 8px 28px rgba(37,211,102,0.45), 0 0 0 0 rgba(37,211,102,0); }
}
/* ── Trust ribbon (sotto hero su mobile, opzionale) ─────────────── */
.mobconv-trust {
display: none;
background: linear-gradient(180deg, rgba(0,0,0,0.04), rgba(0,0,0,0.02));
border-top: 1px solid rgba(0,0,0,0.06);
border-bottom: 1px solid rgba(0,0,0,0.06);
padding: 14px 12px;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.mobconv-trust::-webkit-scrollbar { display: none; }
.mobconv-trust-row {
display: inline-flex;
align-items: center;
gap: 18px;
white-space: nowrap;
padding: 0 4px;
}
.mobconv-trust-item {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12.5px;
font-weight: 600;
color: rgba(0,0,0,0.7);
letter-spacing: 0.01em;
}
.mobconv-trust-item::before {
content: "✓";
color: var(--mobconv-accent);
font-weight: 900;
font-size: 11px;
}
.mobconv-trust-item:not(:first-child)::after {
content: "·";
color: rgba(0,0,0,0.25);
margin-left: 18px;
font-size: 16px;
font-weight: 900;
}
/* Variante dark per siti dark-theme */
.mobconv-trust.dark {
background: linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02));
border-top-color: rgba(255,255,255,0.08);
border-bottom-color: rgba(255,255,255,0.08);
}
.mobconv-trust.dark .mobconv-trust-item { color: rgba(255,255,255,0.85); }
.mobconv-trust.dark .mobconv-trust-item:not(:first-child)::after { color: rgba(255,255,255,0.2); }
/* ── Mobile-only: attiva tutto sotto i 768px ─────────────────────── */
@media (max-width: 768px) {
.mobconv-sticky { display: block; }
.mobconv-wa-fab[data-enabled="true"] { display: inline-flex; }
/* Hide WA FAB quando sticky è visibile (ridondante) */
.mobconv-sticky.show ~ .mobconv-wa-fab { display: none !important; }
.mobconv-trust { display: block; }
/* iOS: no zoom su input/select (font-size ≥16px) */
input, select, textarea {
font-size: max(16px, 1rem) !important;
}
/* Spazio extra in bottom della pagina per non coprire CTA esistenti */
body.mobconv-padded {
padding-bottom: calc(96px + env(safe-area-inset-bottom, 0px)) !important;
}
}
/* Su desktop nasconde TUTTO senza margine d'errore */
@media (min-width: 769px) {
.mobconv-sticky, .mobconv-wa-fab, .mobconv-trust { display: none !important; }
}

192
public/mobile-conversion.js Normal file
View File

@ -0,0 +1,192 @@
/*
* Mobile Conversion Layer Agile Technology suite
* Versione 1.0 2026-05-18
*
* Si attiva solo su mobile (768px).
* Configurabile via <script data-*>:
*
* <script src="mobile-conversion.js"
* data-primary-text="Prenota demo"
* data-primary-action="agilehub-open"
* data-primary-href="#contact"
* data-phone="+390000000000"
* data-whatsapp="390000000000"
* data-wa-message="Buongiorno, vorrei info su…"
* data-show-wa-fab="false"
* data-trust-items='["12 moduli","4 anni","AI"]'
* data-trust-after="section.hero"
* data-trust-theme="dark"
* data-accent="#E31E24"
* data-accent2="#B71518"
* data-scroll-trigger="400"
* defer></script>
*
* data-primary-action:
* - "agilehub-open" apre la modale AgileHubApplet (se presente)
* - "anchor" smooth scroll a data-primary-href
* - "external" location.assign(data-primary-href)
* */
(function () {
'use strict';
/* ── Config dal <script data-*> ── */
var script = document.currentScript || (function () {
var s = document.getElementsByTagName('script');
for (var i = s.length - 1; i >= 0; i--) {
if (s[i].src && s[i].src.indexOf('mobile-conversion') > -1) return s[i];
}
return s[s.length - 1];
})();
var ds = script.dataset || {};
var CFG = {
primaryText: ds.primaryText || 'Contattaci',
primaryAction: ds.primaryAction || 'anchor',
primaryHref: ds.primaryHref || '#contact',
primaryIcon: ds.primaryIcon || '→',
phone: ds.phone || '',
whatsapp: ds.whatsapp || '',
waMessage: ds.waMessage || '',
showWaFab: ds.showWaFab === 'true',
trustItems: ds.trustItems || null, // JSON array
trustAfter: ds.trustAfter || null, // CSS selector
trustTheme: ds.trustTheme || 'light', // 'light' | 'dark'
accent: ds.accent || '#E31E24',
accent2: ds.accent2 || '#B71518',
scrollTrigger: parseInt(ds.scrollTrigger || '400', 10)
};
/* ── Bail-out se desktop o se siamo in un iframe ── */
if (window.innerWidth > 768) return;
/* ── Inject CSS variables override (se accent custom) ── */
if (CFG.accent !== '#E31E24' || CFG.accent2 !== '#B71518') {
var st = document.createElement('style');
st.textContent = ':root{--mobconv-accent:'+CFG.accent+';--mobconv-accent2:'+CFG.accent2+'}';
document.head.appendChild(st);
}
/* ── Parse trust items ── */
var trustList = null;
if (CFG.trustItems) {
try { trustList = JSON.parse(CFG.trustItems); } catch (e) { trustList = null; }
}
/* ── Costruisci WhatsApp URL ── */
function waUrl() {
if (!CFG.whatsapp) return null;
var msg = CFG.waMessage ? '?text=' + encodeURIComponent(CFG.waMessage) : '';
return 'https://wa.me/' + CFG.whatsapp + msg;
}
/* ── Render sticky bar ── */
function buildSticky() {
var html = ['<div class="mobconv-sticky" id="mobconv-sticky"><div class="mobconv-sticky-row">'];
if (CFG.phone) {
html.push('<a class="mobconv-pill" href="tel:' + CFG.phone.replace(/\s/g,'') + '" aria-label="Telefono">📞</a>');
}
var wa = waUrl();
if (wa) {
html.push('<a class="mobconv-pill wa" href="' + wa + '" target="_blank" rel="noopener" aria-label="WhatsApp">💬</a>');
}
// Primary CTA
var ctaAttrs = 'class="mobconv-cta" id="mobconv-primary"';
if (CFG.primaryAction === 'agilehub-open') {
ctaAttrs += ' href="#" data-agilehub-open';
} else if (CFG.primaryAction === 'external') {
ctaAttrs += ' href="' + CFG.primaryHref + '" target="_blank" rel="noopener"';
} else {
ctaAttrs += ' href="' + CFG.primaryHref + '"';
}
html.push('<a ' + ctaAttrs + '>' + CFG.primaryText + ' <span style="opacity:.85">' + CFG.primaryIcon + '</span></a>');
html.push('</div></div>');
return html.join('');
}
/* ── Render WhatsApp FAB ── */
function buildFab() {
if (!CFG.showWaFab || !CFG.whatsapp) return '';
return '<a class="mobconv-wa-fab" data-enabled="true" href="' + waUrl() + '" target="_blank" rel="noopener" aria-label="Scrivici su WhatsApp">' +
'<i class="fab fa-whatsapp"></i>' +
'</a>';
}
/* ── Render trust ribbon ── */
function buildTrust() {
if (!trustList || !trustList.length) return null;
var dark = CFG.trustTheme === 'dark' ? ' dark' : '';
var items = trustList.map(function (t) {
return '<span class="mobconv-trust-item">' + t + '</span>';
}).join('');
return '<div class="mobconv-trust' + dark + '"><div class="mobconv-trust-row">' + items + '</div></div>';
}
/* ── Mount on DOM ready ── */
function mount() {
// Inject sticky bar at end of body
var stickyHtml = buildSticky();
var fabHtml = buildFab();
var wrap = document.createElement('div');
wrap.innerHTML = stickyHtml + fabHtml;
while (wrap.firstChild) document.body.appendChild(wrap.firstChild);
// Inject trust ribbon after hero (or specified selector)
var trustHtml = buildTrust();
if (trustHtml && CFG.trustAfter) {
var anchor = document.querySelector(CFG.trustAfter);
if (anchor) {
var t = document.createElement('div');
t.innerHTML = trustHtml;
anchor.parentNode.insertBefore(t.firstChild, anchor.nextSibling);
}
}
// Padding body bottom per non coprire CTA esistenti in fondo pagina
document.body.classList.add('mobconv-padded');
// Smooth scroll per anchor primary
if (CFG.primaryAction === 'anchor') {
var btn = document.getElementById('mobconv-primary');
if (btn) {
btn.addEventListener('click', function (e) {
var href = btn.getAttribute('href');
if (href && href.charAt(0) === '#') {
var tgt = document.querySelector(href);
if (tgt) {
e.preventDefault();
tgt.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
});
}
}
// agilehub-open: il modulo agilehub-applet.js auto-binda [data-agilehub-open]
// quindi nessun bind manuale serve qui.
// Scroll listener: show sticky bar dopo X px
var stickyEl = document.getElementById('mobconv-sticky');
var triggered = false;
function onScroll() {
var y = window.scrollY || document.documentElement.scrollTop;
if (!triggered && y >= CFG.scrollTrigger) {
stickyEl.classList.add('show');
triggered = true;
} else if (triggered && y < CFG.scrollTrigger - 60) {
// Reset solo se scrolla MOLTO in alto (-60 hysteresis)
stickyEl.classList.remove('show');
triggered = false;
}
}
window.addEventListener('scroll', onScroll, { passive: true });
onScroll(); // check iniziale
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', mount);
} else {
mount();
}
})();

View File

@ -479,7 +479,7 @@
try {
const result = await api.listRisks(params);
if (result.success) {
risksData = result.data || [];
risksData = (result.data && result.data.items) || [];
renderRisksTable(risksData);
} else {
showNotification(result.message || 'Errore nel caricamento rischi', 'error');

View File

@ -0,0 +1,209 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Standard Continuita di Servizio — NIS2 Agile</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Segoe UI',-apple-system,sans-serif;font-size:14.5px;line-height:1.7;color:#1A1A2E;background:#F5F7FA}
.page{max-width:960px;margin:0 auto;background:#fff;box-shadow:0 4px 40px rgba(0,0,0,.08)}
.doc-header{background:linear-gradient(135deg,#0D1B2A 0%,#1565C0 100%);color:#fff;padding:40px 48px}
.header-top{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;flex-wrap:wrap;margin-bottom:22px}
.brand{display:flex;align-items:center;gap:10px}
.brand-icon{width:44px;height:44px;border-radius:10px;background:rgba(255,255,255,.12);display:flex;align-items:center;justify-content:center;font-size:1.2rem;color:rgba(255,255,255,.8)}
.brand-name{font-size:1rem;font-weight:800}
.brand-sub{font-size:.68rem;opacity:.5}
.doc-meta{text-align:right;font-size:.75rem;opacity:.5}
.doc-meta strong{display:block;color:rgba(255,255,255,.8);opacity:1;font-size:.85rem}
.doc-title{font-size:1.65rem;font-weight:800;letter-spacing:-.02em;line-height:1.2;margin-bottom:6px}
.doc-title span{color:rgba(255,255,255,.8)}
.doc-sub{font-size:.84rem;opacity:.6}
.tag-row{display:flex;gap:8px;flex-wrap:wrap;margin-top:18px}
.tag{display:inline-flex;align-items:center;gap:5px;padding:4px 12px;border-radius:20px;font-size:.7rem;font-weight:600;background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.18);color:rgba(255,255,255,.85)}
.content{padding:36px 48px}
h2{font-size:1.25rem;font-weight:800;color:#1565C0;margin:36px 0 14px;padding-bottom:6px;border-bottom:2px solid #eee}
h2:first-child{margin-top:0}
h2 i{margin-right:8px}
h3{font-size:1rem;font-weight:700;color:#1565C0;margin:22px 0 8px}
p{margin:8px 0}ul,ol{margin:8px 0 8px 20px}li{margin:4px 0}
.info-box{background:#E8F5E9;border-left:4px solid #2E7D32;padding:14px 18px;border-radius:0 8px 8px 0;margin:16px 0;font-size:.88rem}
.info-box i{color:#2E7D32;margin-right:6px}
.warn-box{background:#FFF3E0;border-left:4px solid #F57F17;padding:14px 18px;border-radius:0 8px 8px 0;margin:16px 0;font-size:.88rem}
.warn-box i{color:#F57F17;margin-right:6px}
table{width:100%;border-collapse:collapse;margin:14px 0;font-size:.82rem}
th{background:#f5f5f5;padding:10px 12px;text-align:left;font-weight:700;border-bottom:2px solid #ddd;font-size:.75rem;text-transform:uppercase;letter-spacing:.5px;color:#333}
td{padding:9px 12px;border-bottom:1px solid #F0F2F5;vertical-align:top}
tr:hover td{background:#FAFBFC}
code{background:#f5f5f5;padding:2px 6px;border-radius:4px;font-size:.8rem;font-family:'Courier New',monospace;color:#333}
pre{background:#1A1A2E;color:#E0E4E8;padding:16px 20px;border-radius:8px;overflow-x:auto;font-size:.78rem;line-height:1.5;margin:12px 0}
.layer{display:grid;grid-template-columns:60px 1fr;gap:0;margin:16px 0}
.layer-num{background:#1565C0;color:#fff;font-weight:800;font-size:1.1rem;display:flex;align-items:center;justify-content:center;border-radius:8px 0 0 8px}
.layer-body{background:#f9f9f9;padding:16px 20px;border-radius:0 8px 8px 0;border:1px solid #eee;border-left:none}
.layer-body h4{margin:0 0 6px;color:#1565C0;font-size:.95rem}
.badge-ok{display:inline-block;background:#2E7D32;color:#fff;padding:2px 8px;border-radius:10px;font-size:.68rem;font-weight:700}
.badge-warn{display:inline-block;background:#F57F17;color:#fff;padding:2px 8px;border-radius:10px;font-size:.68rem;font-weight:700}
.badge-crit{display:inline-block;background:#C62828;color:#fff;padding:2px 8px;border-radius:10px;font-size:.68rem;font-weight:700}
.doc-footer{background:#f5f5f5;padding:24px 48px;border-top:1px solid #ddd;font-size:.78rem;color:#5F6B7A;display:flex;justify-content:space-between;flex-wrap:wrap;gap:12px}
@media print{body{background:#fff}.page{box-shadow:none}}
@media(max-width:700px){.content,.doc-header,.doc-footer{padding:20px}.layer{grid-template-columns:1fr}.layer-num{border-radius:8px 8px 0 0;padding:8px}.layer-body{border-radius:0 0 8px 8px;border-left:1px solid #eee}}
</style>
</head>
<body>
<div class="page">
<div class="doc-header">
<div class="header-top">
<div class="brand">
<div class="brand-icon"><i class="fas fa-shield-halved"></i></div>
<div><div class="brand-name">NIS2 Agile</div><div class="brand-sub">Cybersecurity Compliance NIS2</div></div>
</div>
<div class="doc-meta"><strong>Standard Operativo</strong>Continuita di Servizio</div>
</div>
<div class="doc-title">Strategia di <span>Continuita di Servizio</span></div>
<div class="doc-sub">Architettura di resilienza, monitoraggio, auto-recovery e protezione dei dati</div>
<div class="tag-row">
<span class="tag"><i class="fas fa-calendar"></i> Aprile 2026</span>
<span class="tag"><i class="fas fa-microchip"></i> PHP+MySQL+Qdrant</span>
<span class="tag"><i class="fas fa-server"></i> Hetzner CPX31</span>
<span class="tag"><i class="fas fa-heartbeat"></i> 99.9% SLA Target</span>
</div>
</div>
<div class="content">
<h2><i class="fas fa-sitemap"></i> 1. Architettura di Servizio</h2>
<p><strong>NIS2 Agile</strong> e composto dai seguenti servizi gestiti sulla piattaforma Hetzner condivisa con gli altri prodotti della suite Agile Technology.</p>
<table><tr><th>Servizio</th><th>Stack</th><th>Healthcheck</th></tr><tr><td><code>nis2-app</code></td><td>PHP+MySQL+Qdrant</td><td><span class="badge-ok">/health</span></td></tr><tr><td><code>MySQL/PostgreSQL</code></td><td>Database condiviso</td><td><span class="badge-ok">ping</span></td></tr><tr><td><code>Qdrant</code></td><td>Vector DB (embeddings)</td><td><span class="badge-ok">HTTP 200</span></td></tr><tr><td><code>AgileHub (NEXUS)</code></td><td>Ticket + AI + Notifiche</td><td><span class="badge-ok">/health</span></td></tr></table>
<div class="info-box">
<i class="fas fa-info-circle"></i>
<strong>Deploy a caldo</strong>: le modifiche al codice sorgente vengono applicate via bind mount Docker o PM2 reload. Nessun rebuild necessario per aggiornamenti ordinari.
</div>
<h2><i class="fas fa-layer-group"></i> 2. Strategia di Resilienza a 7 Livelli</h2>
<div class="layer"><div class="layer-num">L1</div><div class="layer-body">
<h4><i class="fas fa-rotate"></i> Auto-Restart Container / PM2</h4>
<p>Ogni servizio ha <code>restart: unless-stopped</code> (Docker) o <code>pm2 --watch</code> (Node.js). Se un processo crasha, viene riavviato automaticamente entro 10-30 secondi.</p>
<p><strong>Tempo di recovery</strong>: &lt; 30 secondi</p>
</div></div>
<div class="layer"><div class="layer-num">L2</div><div class="layer-body">
<h4><i class="fas fa-heartbeat"></i> Healthcheck Nativo</h4>
<p>Ogni servizio espone un endpoint <code>/health</code> o <code>/api/health</code> verificato ogni 30 secondi. Se il check fallisce 3 volte consecutive, il container/processo viene ricreato automaticamente.</p>
<p><strong>Rileva</strong>: crash applicativo, memoria esaurita, deadlock, loop infinito.</p>
</div></div>
<div class="layer"><div class="layer-num">L3</div><div class="layer-body">
<h4><i class="fas fa-database"></i> Database Retry con Reconnect</h4>
<p>Tutti i servizi implementano retry automatico su errori transitori del database (MySQL/PostgreSQL):</p>
<ul>
<li><strong>2006</strong> — Server has gone away (reconnect)</li>
<li><strong>2013</strong> — Lost connection during query (retry)</li>
<li><strong>1213</strong> — Deadlock found (retry con backoff)</li>
</ul>
<p>Strategia: 2 retry con backoff esponenziale + reconnect automatico.</p>
</div></div>
<div class="layer"><div class="layer-num">L4</div><div class="layer-body">
<h4><i class="fas fa-cloud"></i> Resilienza API Esterne</h4>
<p>Le chiamate ad API esterne (Claude AI, ElevenLabs TTS, Voyage embeddings) hanno protezione a 3 livelli:</p>
<ol>
<li><strong>Retry con backoff</strong>: 3 tentativi su errori transitori (429, 500, 502, 503)</li>
<li><strong>Degradazione graceful</strong>: se l'AI non risponde, il prodotto funziona con i dati locali</li>
<li><strong>Frontend fallback</strong>: messaggio "AI temporaneamente non disponibile" con suggerimenti alternativi</li>
</ol>
</div></div>
<div class="layer"><div class="layer-num">L5</div><div class="layer-body">
<h4><i class="fas fa-bell"></i> Error Alerting in Tempo Reale</h4>
<p>Ogni errore critico genera una notifica AgileHub (campanella) per l'admin e, se configurato, un'email di alert. L'admin vede l'errore entro 30 secondi via polling badge.</p>
</div></div>
<div class="layer"><div class="layer-num">L6</div><div class="layer-body">
<h4><i class="fas fa-eye"></i> Monitoraggio Esterno AgileHub</h4>
<p>Il cron AgileHub (<code>ticket-agent-cron.sh</code>) verifica lo stato di tutti i container ogni 2 minuti. Se un prodotto non e raggiungibile, viene segnalato nel log e inviata notifica.</p>
<p>Complementare ai healthcheck interni: rileva problemi di rete, proxy Apache, DNS.</p>
</div></div>
<div class="layer"><div class="layer-num">L7</div><div class="layer-body">
<h4><i class="fas fa-broom"></i> Manutenzione Preventiva</h4>
<p>Script automatici prevengono il degrado:</p>
<table>
<tr><th>Operazione</th><th>Frequenza</th><th>Funzione</th></tr>
<tr><td>Disk monitor</td><td>Ogni 6 ore</td><td>Alert email se disco &gt; 85%</td></tr>
<tr><td>Cleanup</td><td>Settimanale</td><td>Elimina file temporanei, Docker prune</td></tr>
<tr><td>SSL renewal</td><td>Settimanale</td><td>Rinnovo automatico Let's Encrypt</td></tr>
<tr><td>Claude auth</td><td>Orario</td><td>Refresh token OAuth Claude Code</td></tr>
</table>
</div></div>
<h2><i class="fas fa-shield-halved"></i> 3. Protezione da Abuso</h2>
<h3>Rate Limiting</h3>
<table>
<tr><th>Endpoint</th><th>Limite</th><th>Finestra</th><th>Tipo</th></tr>
<tr><td>Login</td><td>5 tentativi</td><td>15 min</td><td>Per IP + lockout</td></tr>
<tr><td>API generiche</td><td>100 req</td><td>1 ora</td><td>Per chiave/utente</td></tr>
<tr><td>Password reset</td><td>3 req</td><td>1 ora</td><td>Per email</td></tr>
<tr><td>Webhook esterni</td><td>50 req</td><td>1 min</td><td>Per IP</td></tr>
</table>
<h2><i class="fas fa-wrench"></i> 4. Modalita Manutenzione</h2>
<p>Quando il sistema AgileHub applica un fix approvato, il <strong>maintenance mode</strong> si attiva automaticamente:</p>
<ol>
<li>Il cron agent crea il flag <code>/tmp/maintenance-NIS2.flag</code></li>
<li>Apache rileva il flag e mostra la pagina di manutenzione a tutti gli utenti</li>
<li>Il fix viene applicato (Claude nel container DevEnv)</li>
<li>Il flag viene rimosso — il prodotto torna online</li>
<li>La pagina di manutenzione si auto-aggiorna e reindirizza gli utenti</li>
</ol>
<div class="warn-box">
<i class="fas fa-exclamation-triangle"></i>
<strong>Nessun intervento manuale richiesto</strong>: il maintenance mode e completamente automatico. L'utente vede un messaggio professionale e il sistema torna online da solo.
</div>
<h2><i class="fas fa-exclamation-triangle"></i> 5. Matrice Rischi e Mitigazione</h2>
<table>
<tr><th>Scenario</th><th>Probabilita</th><th>Impatto</th><th>Mitigazione</th><th>Recovery</th></tr>
<tr><td>Crash singolo servizio</td><td><span class="badge-warn">Media</span></td><td><span class="badge-warn">Medio</span></td><td>Auto-restart Docker/PM2</td><td>&lt; 30s</td></tr>
<tr><td>Database restart</td><td><span class="badge-ok">Bassa</span></td><td><span class="badge-crit">Alto</span></td><td>DB retry + reconnect</td><td>&lt; 5s</td></tr>
<tr><td>AI non disponibile</td><td><span class="badge-warn">Media</span></td><td><span class="badge-ok">Basso</span></td><td>3 retry + graceful</td><td>Funzioni base intatte</td></tr>
<tr><td>Disco pieno</td><td><span class="badge-ok">Bassa</span></td><td><span class="badge-crit">Alto</span></td><td>Monitor 6h + cleanup</td><td>Alert prima del 85%</td></tr>
<tr><td>SSL scaduto</td><td><span class="badge-ok">Bassa</span></td><td><span class="badge-crit">Alto</span></td><td>Auto-renewal certbot</td><td>Rinnovo 30gg prima</td></tr>
<tr><td>Attacco brute force</td><td><span class="badge-warn">Media</span></td><td><span class="badge-warn">Medio</span></td><td>Rate limit + lockout</td><td>Blocco 15 min</td></tr>
<tr><td>Errore deployment</td><td><span class="badge-warn">Media</span></td><td><span class="badge-crit">Alto</span></td><td>Maintenance mode + git revert</td><td>&lt; 2 min rollback</td></tr>
</table>
<h2><i class="fas fa-handshake"></i> 6. Service Level Agreement</h2>
<table>
<tr><th>Metrica</th><th>Target</th><th>Misurazione</th></tr>
<tr><td>Uptime servizio</td><td><strong>99.9%</strong> (8.7h downtime/anno)</td><td>Health monitor + log</td></tr>
<tr><td>Recovery da crash</td><td>&lt; 30 secondi</td><td>Docker restart + healthcheck</td></tr>
<tr><td>Recovery da aggiornamento</td><td>&lt; 5 minuti</td><td>Maintenance mode + deploy</td></tr>
<tr><td>Rilevamento downtime</td><td>&lt; 5 minuti</td><td>Cron monitor + email alert</td></tr>
<tr><td>Tempo risposta API (p95)</td><td>&lt; 500ms</td><td>Health endpoint response time</td></tr>
</table>
<h2><i class="fas fa-clipboard-check"></i> 7. Checklist Operativa</h2>
<h3>Verifica automatica (quotidiana)</h3>
<ul>
<li>Tutti i container/processi in stato healthy/online</li>
<li>Campanella AgileHub — nessuna notifica di errore sistema</li>
<li>Email — nessun alert da health monitor</li>
</ul>
<h3>Prima di un aggiornamento manuale</h3>
<ol>
<li>Verificare che non ci siano utenti attivi (o attivare maintenance mode)</li>
<li>Eseguire l'aggiornamento</li>
<li>Verificare tutti i servizi: <code>docker compose ps</code> o <code>pm2 list</code></li>
<li>Controllare i log per errori</li>
</ol>
</div>
<div class="doc-footer">
<div>&copy; 2026 NIS2 Agile &mdash; <a href="https://nis2.agile.software" style="color:#1565C0">nis2.agile.software</a></div>
<div>Standard Continuita di Servizio v1.0 &bull; Aprile 2026 &bull; Agile Technology SRL</div>
</div>
</div>
</body>
</html>

View File

@ -1 +1 @@
{"version":"1.6.1","build":"20260529g","date":"2026-05-29T14:55:00+02:00","changelog":"Doc: aggiornati help.js (sezione Impostazioni con Sessioni/Preferenze/Branding/Reset/Tenant), i18n.js (chiavi IT/EN per Fasi 2-5), product knowledge AI AgileHub (card NIS2 id=914)"}
{"version":"1.7.0","build":"20260529h","date":"2026-05-29T16:30:00+02:00","changelog":"FEAT integrazione analisi docs/nis2: (1) Asset Relevance Scoring NIS2 0-100 a 6 criteri (GV.OC-04) + registro formale stampabile; (2) Tassonomia incidenti Determina ACN 164179/2025 (IS-1..4, regime essenziale/importante Allegati 3-4); (3) Post-Incident Review strutturato 5-Whys + metriche TTD/TTC/TTR; (4) Layer mapping NIST CSF 2.0 (43 controlli); (5) Fonti normative certe: registry citabile + grounding AI + citazioni help + ingest PDF normativi nella KB RAG."}

View File

@ -0,0 +1,398 @@
<?php
/**
* =============================================================================
* Import storico segnalazioni NIS2 AgileHub/Nexus
* =============================================================================
*
* Migra le righe di `nis2_agile_db.feedback_reports` (e relative
* attachments inline base64) verso `nexus_ticket_db.tickets` /
* `ticket_attachments`, preservando created_at originale e idempotente
* rispetto a re-run grazie a `tickets.external_ref`.
*
* Mapping:
* feedback_reports.id tickets.external_ref ('nis2:<id>')
* feedback_reports.tipo tickets.kind (vedi $KIND_MAP)
* feedback_reports.priorita tickets.priority (alta→HIGH, media→MEDIUM, bassa→LOW)
* feedback_reports.status tickets.status (vedi $STATUS_MAP)
* feedback_reports.descrizione tickets.operative_summary (+ subject = first 200)
* feedback_reports.user_email tickets.caller_email
* feedback_reports.user_role tickets.reporter_role
* feedback_reports.page_url tickets.page_url
* feedback_reports.ai_* tickets.ai_*
* feedback_reports.attachment ticket_attachments.data (base64, LONGTEXT)
* + sempre: requested_product='NIS2', requested_product_label='NIS2 Agile',
* channel='FORM', tenant_id=$NEXUS_TENANT_ID
*
* Tabella `feedback_audit_log` (se presente) ticket_messages role='SYSTEM'
* Tabella `notifications`: NON migrata (decisione esplicita per non flooddare
* la bell di AgileHub con notifiche storiche già lette).
*
* Uso:
* php import-feedback-to-nexus.php # dry-run di default
* php import-feedback-to-nexus.php --commit # esecuzione vera
* php import-feedback-to-nexus.php --rollback # cancella tutto ciò che è stato importato (filtro external_ref LIKE 'nis2:%')
* php import-feedback-to-nexus.php --limit=10 # solo prime 10 righe (utile per smoke test)
*
* Variabili d'ambiente:
* SRC_DB_HOST default 172.25.0.1
* SRC_DB_USER default nis2_user
* SRC_DB_PASS default Nis2Dev2026!
* SRC_DB_NAME default nis2_agile_db
*
* NEXUS_DB_HOST default 172.25.0.1
* NEXUS_DB_USER default agile_services
* NEXUS_DB_PASS *required*
* NEXUS_DB_NAME default nexus_ticket_db
*
* NEXUS_TENANT_ID *required* (id numerico del tenant 'alltax' in agile_auth_db.tenants)
* =============================================================================
*/
declare(strict_types=1);
// ----------------------------- CLI ARGS -----------------------------------
$args = array_slice($argv, 1);
$dryRun = !in_array('--commit', $args, true);
$rollback = in_array('--rollback', $args, true);
$limit = null;
foreach ($args as $a) {
if (preg_match('/^--limit=(\d+)$/', $a, $m)) { $limit = (int)$m[1]; }
}
// ----------------------------- ENV ----------------------------------------
$SRC = [
'host' => getenv('SRC_DB_HOST') ?: '172.25.0.1',
'user' => getenv('SRC_DB_USER') ?: 'nis2_user',
'pass' => getenv('SRC_DB_PASS') ?: 'Nis2Dev2026!',
'db' => getenv('SRC_DB_NAME') ?: 'nis2_agile_db',
];
$DST = [
'host' => getenv('NEXUS_DB_HOST') ?: '172.25.0.1',
'user' => getenv('NEXUS_DB_USER') ?: 'agile_services',
'pass' => getenv('NEXUS_DB_PASS') ?: '',
'db' => getenv('NEXUS_DB_NAME') ?: 'nexus_ticket_db',
];
$NEXUS_TENANT_ID = (int) (getenv('NEXUS_TENANT_ID') ?: 0);
if (!$rollback && $NEXUS_TENANT_ID <= 0) {
fwrite(STDERR, "ERROR: env NEXUS_TENANT_ID mancante. Esegui prima seed-nexus-tenant.sql e annota l'id.\n");
exit(2);
}
if (!$rollback && empty($DST['pass'])) {
fwrite(STDERR, "ERROR: env NEXUS_DB_PASS mancante.\n");
exit(2);
}
// ----------------------------- LOG ----------------------------------------
$logDir = __DIR__ . '/../tmp';
if (!is_dir($logDir)) { mkdir($logDir, 0775, true); }
$logPath = $logDir . '/import-feedback-' . date('Ymd-His') . '.log';
$logFp = fopen($logPath, 'w');
function logLine(string $line): void {
global $logFp;
$stamp = date('H:i:s');
fwrite($logFp, "[$stamp] $line\n");
echo "[$stamp] $line\n";
}
logLine('=== Import feedback NIS2 → Nexus ===');
logLine('Mode: ' . ($rollback ? 'ROLLBACK' : ($dryRun ? 'DRY-RUN' : 'COMMIT')));
logLine('Tenant id: ' . $NEXUS_TENANT_ID);
logLine('Limit: ' . ($limit ?? 'none'));
logLine('Source: ' . "{$SRC['user']}@{$SRC['host']}/{$SRC['db']}");
logLine('Target: ' . "{$DST['user']}@{$DST['host']}/{$DST['db']}");
// ----------------------------- PDO ----------------------------------------
$pdoOpts = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$src = new PDO("mysql:host={$SRC['host']};dbname={$SRC['db']};charset=utf8mb4",
$SRC['user'], $SRC['pass'], $pdoOpts);
$dst = new PDO("mysql:host={$DST['host']};dbname={$DST['db']};charset=utf8mb4",
$DST['user'], $DST['pass'], $pdoOpts);
} catch (PDOException $e) {
logLine('FATAL DB connect: ' . $e->getMessage());
exit(3);
}
// ----------------------------- ROLLBACK -----------------------------------
if ($rollback) {
logLine('Rollback: cancello tutti i ticket con external_ref LIKE "nis2:%"');
if ($dryRun) {
$n = (int) $dst->query(
"SELECT COUNT(*) FROM tickets WHERE external_ref LIKE 'nis2:%'"
)->fetchColumn();
logLine("[DRY-RUN] eliminerebbe $n ticket (e relativi attachments / messages via FK CASCADE)");
logLine("Per eseguire davvero: aggiungi --commit");
exit(0);
}
$dst->beginTransaction();
$deleted = $dst->exec("DELETE FROM tickets WHERE external_ref LIKE 'nis2:%'");
$dst->commit();
logLine("Rollback: $deleted ticket eliminati");
exit(0);
}
// ----------------------------- DETECT OPTIONAL COLUMNS --------------------
function tableHasColumn(PDO $pdo, string $table, string $col): bool {
$stmt = $pdo->prepare(
"SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?"
);
$stmt->execute([$table, $col]);
return (bool) $stmt->fetchColumn();
}
function tableExists(PDO $pdo, string $table): bool {
$stmt = $pdo->prepare(
"SELECT 1 FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?"
);
$stmt->execute([$table]);
return (bool) $stmt->fetchColumn();
}
$hasAuditLog = tableExists($src, 'feedback_audit_log');
$hasRiskScore = tableHasColumn($src, 'feedback_reports', 'risk_score');
$hasNotaAdmin = tableHasColumn($src, 'feedback_reports', 'nota_admin');
$hasStatusVerified = false;
try {
$row = $src->query(
"SELECT COLUMN_TYPE FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'feedback_reports'
AND COLUMN_NAME = 'status'"
)->fetch();
$hasStatusVerified = $row && stripos($row['COLUMN_TYPE'], "'verificato'") !== false;
} catch (PDOException $e) {}
logLine('Source schema: feedback_audit_log=' . ($hasAuditLog ? 'yes' : 'no')
. ', risk_score=' . ($hasRiskScore ? 'yes' : 'no')
. ', nota_admin=' . ($hasNotaAdmin ? 'yes' : 'no')
. ', status verificato=' . ($hasStatusVerified ? 'yes' : 'no'));
// ----------------------------- MAPPING TABLES ------------------------------
$KIND_MAP = [
'bug' => 'COMPLAINT',
'ux' => 'SUPPORT_REQUEST',
'funzionalita' => 'INFO_REQUEST',
'domanda' => 'INFO_REQUEST',
'altro' => 'OTHER',
];
$PRIORITY_MAP = [
'alta' => 'HIGH',
'media' => 'MEDIUM',
'bassa' => 'LOW',
];
$STATUS_MAP = [
'aperto' => 'OPEN',
'in_lavorazione' => 'IN_PROGRESS',
'risolto' => 'RESOLVED',
'chiuso' => 'CLOSED',
'verificato' => 'CLOSED',
];
// ----------------------------- READ SOURCE --------------------------------
$sql = "SELECT * FROM feedback_reports ORDER BY id ASC";
if ($limit) { $sql .= " LIMIT " . (int)$limit; }
$rows = $src->query($sql)->fetchAll();
logLine('Source rows fetched: ' . count($rows));
if (empty($rows)) {
logLine('Niente da migrare. Esco.');
exit(0);
}
// Pre-load existing external_refs in target (single roundtrip)
$existing = [];
foreach ($dst->query(
"SELECT external_ref FROM tickets WHERE external_ref LIKE 'nis2:%'"
)->fetchAll(PDO::FETCH_COLUMN) as $ref) {
$existing[$ref] = true;
}
logLine('Already imported in target: ' . count($existing));
// ----------------------------- INSERT PREP --------------------------------
$insTicket = $dst->prepare("
INSERT INTO tickets
(tenant_id, caller_phone, caller_name, caller_email,
requested_product, requested_product_label,
subject, operative_summary,
status, queue, priority, channel, kind,
external_ref, page_url, reporter_role,
ai_categoria, ai_priorita, ai_suggerimento, ai_response, ai_processed,
lang,
created_at, updated_at)
VALUES
(:tenant_id, :caller_phone, :caller_name, :caller_email,
:requested_product, :requested_product_label,
:subject, :operative_summary,
:status, :queue, :priority, :channel, :kind,
:external_ref, :page_url, :reporter_role,
:ai_categoria, :ai_priorita, :ai_suggerimento, :ai_response, :ai_processed,
:lang,
:created_at, :updated_at)
");
$insAttachment = $dst->prepare("
INSERT INTO ticket_attachments
(ticket_id, tenant_id, filename, mime_type, size, data, created_at)
VALUES
(:ticket_id, :tenant_id, :filename, :mime_type, :size, :data, :created_at)
");
$insMessage = $dst->prepare("
INSERT INTO ticket_messages
(ticket_id, tenant_id, role, content, metadata, created_at)
VALUES
(:ticket_id, :tenant_id, 'SYSTEM', :content, :metadata, :created_at)
");
// ----------------------------- LOOP ---------------------------------------
$counters = ['ok' => 0, 'skip' => 0, 'err' => 0, 'attach' => 0, 'audit' => 0];
if (!$dryRun) { $dst->beginTransaction(); }
foreach ($rows as $r) {
$extRef = 'nis2:' . (int)$r['id'];
if (isset($existing[$extRef])) {
$counters['skip']++;
logLine("[SKIP duplicate] $extRef");
continue;
}
$tipo = $r['tipo'] ?? 'altro';
$priorita = $r['priorita'] ?? 'media';
$status = $r['status'] ?? 'aperto';
$kind = $KIND_MAP[$tipo] ?? 'OTHER';
$priority = $PRIORITY_MAP[$priorita] ?? 'MEDIUM';
$statusN = $STATUS_MAP[$status] ?? 'OPEN';
$descr = (string) ($r['descrizione'] ?? '');
$subject = mb_substr(trim(preg_replace('/\s+/', ' ', $descr)), 0, 200);
if ($subject === '') { $subject = "[NIS2 legacy #{$r['id']}]"; }
// Append nota_admin alla operative_summary se presente
$opSummary = $descr;
if ($hasNotaAdmin && !empty($r['nota_admin'])) {
$opSummary .= "\n\n--- Nota admin originale ---\n" . $r['nota_admin'];
}
if ($hasRiskScore && isset($r['risk_score']) && $r['risk_score'] !== null) {
$opSummary .= "\n\n[risk_score originale: " . $r['risk_score'] . "]";
}
// Map ai_priorita italiano → ENUM Nexus
$aiPrio = null;
if (!empty($r['ai_priorita'])) {
$aiPrio = $PRIORITY_MAP[$r['ai_priorita']] ?? null;
}
$params = [
':tenant_id' => $NEXUS_TENANT_ID,
':caller_phone' => '', // non disponibile in feedback_reports
':caller_name' => $r['user_email'] ?? null, // fallback: nessun caller_name in source
':caller_email' => $r['user_email'] ?? null,
':requested_product' => 'NIS2',
':requested_product_label' => 'NIS2 Agile',
':subject' => $subject,
':operative_summary' => mb_substr($opSummary, 0, 1990),
':status' => $statusN,
':queue' => 'SUPPORT',
':priority' => $priority,
':channel' => 'FORM',
':kind' => $kind,
':external_ref' => $extRef,
':page_url' => $r['page_url'] ?? null,
':reporter_role' => $r['user_role'] ?? null,
':ai_categoria' => $r['ai_categoria'] ?? null,
':ai_priorita' => $aiPrio,
':ai_suggerimento' => $r['ai_suggerimento'] ?? null,
':ai_response' => $r['ai_risposta'] ?? null,
':ai_processed' => isset($r['ai_processed']) ? (int)$r['ai_processed'] : 0,
':lang' => 'it',
':created_at' => $r['created_at'] ?? date('Y-m-d H:i:s'),
':updated_at' => $r['updated_at'] ?? ($r['created_at'] ?? date('Y-m-d H:i:s')),
];
if ($dryRun) {
$counters['ok']++;
logLine("[DRY-RUN OK] $extRef kind=$kind status=$statusN created={$params[':created_at']}");
continue;
}
try {
$insTicket->execute($params);
$newId = (int) $dst->lastInsertId();
$counters['ok']++;
logLine("[OK] $extRef → ticket #$newId");
// Attachment inline base64
if (!empty($r['attachment'])) {
$att = (string) $r['attachment'];
$mime = 'image/png';
$rawB64 = $att;
if (preg_match('/^data:([^;]+);base64,(.*)$/', $att, $m)) {
$mime = $m[1];
$rawB64 = $m[2];
}
$size = (int) (strlen($rawB64) * 3 / 4);
$insAttachment->execute([
':ticket_id' => $newId,
':tenant_id' => $NEXUS_TENANT_ID,
':filename' => 'legacy_' . (int)$r['id'] . '.png',
':mime_type' => $mime,
':size' => $size,
':data' => $att,
':created_at' => $params[':created_at'],
]);
$counters['attach']++;
}
// feedback_audit_log → ticket_messages role=SYSTEM
if ($hasAuditLog) {
$stmt = $src->prepare(
"SELECT * FROM feedback_audit_log WHERE report_id = ? ORDER BY id ASC"
);
$stmt->execute([(int)$r['id']]);
foreach ($stmt->fetchAll() as $ev) {
$content = sprintf(
"[legacy audit] action=%s to_status=%s actor=%s",
$ev['action'] ?? '',
$ev['to_status'] ?? '',
$ev['actor_type']?? ''
);
$insMessage->execute([
':ticket_id' => $newId,
':tenant_id' => $NEXUS_TENANT_ID,
':content' => $content,
':metadata' => $ev['details'] ?? '{}',
':created_at' => $ev['created_at'] ?? $params[':created_at'],
]);
$counters['audit']++;
}
}
} catch (PDOException $e) {
$counters['err']++;
logLine("[ERR] $extRef" . $e->getMessage());
// continua: gli altri possono passare. Fail-safe: log e via.
}
}
if (!$dryRun) {
if ($counters['err'] > 0) {
$dst->rollBack();
logLine('Errori rilevati: ROLLBACK transazione, nessuna riga scritta.');
logLine("Counters: " . json_encode($counters));
exit(1);
}
$dst->commit();
}
logLine('=== Riepilogo ===');
logLine("OK: {$counters['ok']}");
logLine("SKIP: {$counters['skip']}");
logLine("ERR: {$counters['err']}");
logLine("Attachments: {$counters['attach']}");
logLine("Audit msgs: {$counters['audit']}");
logLine("Log file: $logPath");
exit($counters['err'] > 0 ? 1 : 0);

View File

@ -0,0 +1,213 @@
<?php
/**
* NIS2 Agile - Ingest Fonti Normative Certe nella Knowledge Base (RAG)
* ----------------------------------------------------------------------------
* Indicizza i PDF normativi ufficiali (docs/nis2/*.pdf, registrati in
* application/config/nis2_sources.php) nella collection Qdrant `nis2_kb` con
* scope SYSTEM, cosi' che AIService::askWithRag() possa citare le fonti certe.
*
* ESEGUIRE SU HETZNER (richiede accesso a Qdrant + Voyage), es:
* docker exec -i nis2-app php /var/www/nis2-agile/scripts/ingest-nis2-sources.php
* # oppure dalla root del progetto:
* php scripts/ingest-nis2-sources.php
*
* Estrazione testo: usa `pdftotext` (poppler-utils) se disponibile, altrimenti
* ricade sull'API document di Claude. Idempotente: cancella i chunk SYSTEM del
* documento (per `source` stabile) prima del re-upsert.
*
* Opzioni:
* --only=determina_164179_2025 ingerisce una sola fonte (key del registry)
* --dry-run estrae e mostra le statistiche senza upsert
* ============================================================================
*/
if (PHP_SAPI !== 'cli') { fwrite(STDERR, "Solo CLI\n"); exit(1); }
if (!defined('BASE_PATH')) define('BASE_PATH', dirname(__DIR__));
if (!defined('APP_PATH')) define('APP_PATH', BASE_PATH . '/application');
require_once APP_PATH . '/config/env.php';
require_once APP_PATH . '/config/config.php';
require_once APP_PATH . '/config/database.php';
require_once APP_PATH . '/services/EmbedService.php';
require_once APP_PATH . '/services/VectorService.php';
$opts = getopt('', ['only::', 'dry-run']);
$only = $opts['only'] ?? null;
$dryRun = isset($opts['dry-run']);
$sources = require APP_PATH . '/config/nis2_sources.php';
function logln(string $m): void { echo '[' . date('Y-m-d H:i:s') . "] $m\n"; }
/** Estrae testo da un PDF: pdftotext -> fallback Claude document API. */
function extractPdfText(string $absPath): string
{
// 0) Cache di testo pre-estratto accanto al PDF (<file>.pdf.txt).
// Utile quando l'ingest gira in un container privo di pdftotext:
// si estrae prima sull'host e si rilegge il .txt qui.
$cache = $absPath . '.txt';
if (is_file($cache)) {
$t = (string) file_get_contents($cache);
if (strlen(trim($t)) > 200) { logln(' uso cache testo: ' . basename($cache)); return $t; }
}
// 1) pdftotext (veloce, gratuito)
$bin = trim((string)@shell_exec('command -v pdftotext 2>/dev/null'));
if ($bin !== '') {
$tmp = tempnam(sys_get_temp_dir(), 'nis2pdf') . '.txt';
@shell_exec(escapeshellcmd($bin) . ' -enc UTF-8 -nopgbrk ' . escapeshellarg($absPath) . ' ' . escapeshellarg($tmp) . ' 2>/dev/null');
$txt = is_file($tmp) ? (string)file_get_contents($tmp) : '';
@unlink($tmp);
if (strlen(trim($txt)) > 200) return $txt;
}
// 2) Fallback: Claude document API
logln(' pdftotext non disponibile/insufficiente -> uso Claude document API');
$data = base64_encode((string)file_get_contents($absPath));
$body = [
'model' => defined('ANTHROPIC_MODEL') ? ANTHROPIC_MODEL : 'claude-sonnet-4-5-20250929',
'max_tokens' => 8000,
'messages' => [[
'role' => 'user',
'content' => [
['type' => 'document', 'source' => ['type' => 'base64', 'media_type' => 'application/pdf', 'data' => $data]],
['type' => 'text', 'text' => 'Estrai integralmente il testo di questo documento normativo in testo semplice, mantenendo numeri di articolo, commi, allegati e tabelle. Non riassumere, non commentare.'],
],
]],
];
$ch = curl_init('https://api.anthropic.com/v1/messages');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'content-type: application/json',
'x-api-key: ' . ANTHROPIC_API_KEY,
'anthropic-version: 2023-06-01',
],
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_TIMEOUT => 180,
]);
$res = curl_exec($ch);
if ($res === false) { logln(' ERRORE curl: ' . curl_error($ch)); curl_close($ch); return ''; }
curl_close($ch);
$j = json_decode($res, true);
return $j['content'][0]['text'] ?? '';
}
function chunkText(string $text, int $size = 2000, int $overlap = 200): array
{
// Multibyte-safe: usa mb_* per non spezzare caratteri UTF-8 a meta'
// (altrimenti json_encode produce body non valido -> Voyage HTTP 400).
$text = mb_convert_encoding($text, 'UTF-8', 'UTF-8'); // bonifica sequenze invalide
$chunks = []; $len = mb_strlen($text, 'UTF-8'); $start = 0;
while ($start < $len) {
$take = min($size, $len - $start);
$piece = mb_substr($text, $start, $take, 'UTF-8');
if (trim($piece) !== '') $chunks[] = $piece;
if ($start + $take >= $len) break;
$start += ($size - $overlap);
}
return $chunks;
}
function uuid(): string
{
$b = random_bytes(16);
$b[6] = chr((ord($b[6]) & 0x0f) | 0x40);
$b[8] = chr((ord($b[8]) & 0x3f) | 0x80);
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($b), 4));
}
logln('=== Ingest fonti normative NIS2 nella KB (scope SYSTEM) ===');
if ($dryRun) logln('MODALITA DRY-RUN: nessun upsert.');
$embed = null; $vector = null;
if (!$dryRun) {
$embed = new EmbedService();
$vector = new VectorService();
$vector->ensureCollection($embed->dims);
}
$totalChunks = 0; $done = 0;
foreach ($sources as $key => $src) {
if ($only && $key !== $only) continue;
if (empty($src['file'])) { logln("SKIP {$key}: nessun file PDF associato"); continue; }
$abs = BASE_PATH . '/' . $src['file'];
if (!is_file($abs)) { logln("SKIP {$key}: file non trovato {$abs}"); continue; }
logln("Fonte: {$src['short']} ({$src['file']})");
$text = extractPdfText($abs);
$text = preg_replace('/[ \t]+/', ' ', $text);
$text = preg_replace('/\n{3,}/', "\n\n", trim($text));
if (strlen($text) < 200) { logln(" ERRORE: testo estratto troppo breve, salto."); continue; }
// Prefisso citazione su ogni documento: aiuta il modello a citare correttamente
$header = "FONTE NORMATIVA: {$src['citation']}\nAUTORITA: {$src['authority']}\n\n";
$chunks = chunkText($header . $text, 2000, 200);
logln(' testo: ' . strlen($text) . ' char -> ' . count($chunks) . ' chunk');
$totalChunks += count($chunks);
if ($dryRun) { $done++; continue; }
// Idempotenza: rimuovi i chunk SYSTEM esistenti per questa fonte
try {
$vector->deleteByFilter(['must' => [
['key' => 'scope', 'match' => ['value' => 'SYSTEM']],
['key' => 'source', 'match' => ['value' => $src['citation']]],
]]);
} catch (Exception $e) { logln(' (warning) delete precedente: ' . $e->getMessage()); }
$docUuid = uuid();
$points = [];
foreach ($chunks as $i => $chunk) {
// Embedding con retry/backoff: Voyage puo' restituire errori transitori
// (HTTP 0 timeout / 429 rate limit) su grandi volumi di chunk.
$vec = null;
for ($try = 1; $try <= 5; $try++) {
try { $vec = $embed->embed($chunk); break; }
catch (Throwable $e) {
if ($try === 5) { logln(" ERRORE embed chunk {$i} dopo 5 tentativi: " . $e->getMessage()); throw $e; }
logln(" retry embed chunk {$i} (tentativo {$try}): " . substr($e->getMessage(), 0, 60));
sleep($try); // backoff lineare 1s,2s,3s,4s
}
}
$points[] = [
'id' => uuid(),
'vector' => $vec,
'payload' => [
'doc_uuid' => $docUuid,
'title' => $src['short'] . ($i > 0 ? ' (parte ' . ($i + 1) . ')' : ''),
'chunk' => $chunk,
'entity_type' => 'normativa',
'source' => $src['citation'],
'lang' => 'it',
'scope' => 'SYSTEM',
'consulting_firm_id' => null,
'organization_id' => null,
'shared_with_orgs' => [],
'uploaded_by' => 0,
],
];
}
// Upsert a batch (per non superare i limiti di payload)
foreach (array_chunk($points, 64) as $batch) {
$vector->upsertBatch($batch);
}
// Tracking MySQL (best-effort)
try {
$stmt = Database::getInstance()->prepare(
"INSERT INTO kb_uploaded_documents
(qdrant_doc_uuid, scope, consulting_firm_id, organization_id, uploaded_by, title, entity_type, source, lang, chunk_count, shared_with_orgs, status)
VALUES (?, 'SYSTEM', NULL, NULL, 0, ?, 'normativa', ?, 'it', ?, '[]', 'ready')"
);
$stmt->execute([$docUuid, $src['short'], $src['citation'], count($chunks)]);
} catch (Exception $e) { logln(' (warning) tracking insert: ' . $e->getMessage()); }
logln(" OK indicizzato (doc_uuid={$docUuid})");
$done++;
}
logln("=== Completato: {$done} fonti, {$totalChunks} chunk totali ===");