Compare commits
8 Commits
4a85abeb3b
...
94d7867cea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94d7867cea | ||
|
|
0330bcf29d | ||
|
|
5c545ea3d0 | ||
|
|
a7a21faa82 | ||
|
|
9b53ca3ba1 | ||
|
|
1d934e4e63 | ||
|
|
c0bf7b6c15 | ||
|
|
1d13166d7a |
8
.gitignore
vendored
8
.gitignore
vendored
@ -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
35
AGENT_CHANGES.md
Normal 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
550
CLAUDE.md
@ -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**.
|
||||
|
||||
75
application/config/nis2_sources.php
Normal file
75
application/config/nis2_sources.php
Normal 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/',
|
||||
],
|
||||
];
|
||||
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
329
application/controllers/KnowledgeBaseController.php
Normal file
329
application/controllers/KnowledgeBaseController.php
Normal 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)
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
202
application/services/AssetScoringService.php
Normal file
202
application/services/AssetScoringService.php
Normal 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] ?? [];
|
||||
}
|
||||
}
|
||||
66
application/services/EmbedService.php
Normal file
66
application/services/EmbedService.php
Normal 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;
|
||||
}
|
||||
}
|
||||
64
application/services/RagService.php
Normal file
64
application/services/RagService.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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 ≥ 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">⚠ DOCUMENTO RISERVATO - DISTRIBUZIONE LIMITATA ⚠</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 — Requisito GV.OC-04 (NIST CSF 2.0)<br>
|
||||
Settore: {$sector} | Tipo soggetto: {$entity} | Data emissione: {$date} | 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 ≥ 40.</strong> I sistemi con punteggio ≥ 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 (≥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>
|
||||
• Direttiva (UE) 2022/2555 (NIS2) — Parlamento europeo e Consiglio UE<br>
|
||||
• D.Lgs. 4 settembre 2024, n. 138 — recepimento NIS2 (artt. 23, 24)<br>
|
||||
• NIST Cybersecurity Framework 2.0 — 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);
|
||||
|
||||
158
application/services/VectorService.php
Normal file
158
application/services/VectorService.php
Normal 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) ?? []) : [],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
263
docs/EVIX_ANALISI_CONCORRENZA.html
Normal file
263
docs/EVIX_ANALISI_CONCORRENZA.html
Normal 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 & 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 & 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 & 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">Sì <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">Sì <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">Sì <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 & 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 & 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 & 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
6
docs/OPEN_TICKETS.md
Normal file
@ -0,0 +1,6 @@
|
||||
# OPEN TICKETS — NIS2
|
||||
|
||||
Nessun ticket aperto.
|
||||
|
||||
---
|
||||
_Ultimo sync: 2026-05-29 15:40:02_
|
||||
982
docs/STANDARD_AI_PRODOTTO.md
Normal file
982
docs/STANDARD_AI_PRODOTTO.md
Normal 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
|
||||
246
docs/STANDARD_EMAIL_RELAY.md
Normal file
246
docs/STANDARD_EMAIL_RELAY.md
Normal 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'`.
|
||||
298
docs/STANDARD_INSTALLER_INTEGRATION.md
Normal file
298
docs/STANDARD_INSTALLER_INTEGRATION.md
Normal 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).
|
||||
237
docs/STANDARD_MARKETING_TENANT_PROVISIONING.md
Normal file
237
docs/STANDARD_MARKETING_TENANT_PROVISIONING.md
Normal 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
|
||||
```
|
||||
160
docs/STANDARD_MULTITENANT_ARCHITECTURE.md
Normal file
160
docs/STANDARD_MULTITENANT_ARCHITECTURE.md
Normal 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)
|
||||
BIN
docs/nis2/AmbitiNIS2_ITA.pdf
Normal file
BIN
docs/nis2/AmbitiNIS2_ITA.pdf
Normal file
Binary file not shown.
233
docs/nis2/AmbitiNIS2_ITA.pdf.txt
Normal file
233
docs/nis2/AmbitiNIS2_ITA.pdf.txt
Normal 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 dell’Autorità
|
||||
|
||||
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 dell’Autorità come essenziali
|
||||
2
|
||||
Possibile individuazione dell’Autorità come importanti o essenziali
|
||||
1
|
||||
|
||||
BIN
docs/nis2/Determina164179_apr2025.pdf
Normal file
BIN
docs/nis2/Determina164179_apr2025.pdf
Normal file
Binary file not shown.
241
docs/nis2/Determina164179_apr2025.pdf.txt
Normal file
241
docs/nis2/Determina164179_apr2025.pdf.txt
Normal file
@ -0,0 +1,241 @@
|
||||
Agenzia per la Cybersicurezza Nazionale
|
||||
Determinazione del Direttore Generale dell’Agenzia per la
|
||||
cybersicurezza nazionale
|
||||
di cui all’articolo 31, commi 1 e 2, del decreto legislativo 4 settembre 2024, n. 138, adottata
|
||||
secondo le modalità di cui all’articolo 40, comma 5, lettera l), che, ai sensi dell’articolo 42,
|
||||
comma 1, lettera c), in fase di prima applicazione, stabilisce le modalità e le specifiche di base
|
||||
per l’adempimento 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
|
||||
dell’architettura nazionale di cybersicurezza e istituzione dell’Agenzia 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 nell’Unione, 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 l’articolo 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 l’articolo 40, comma 5, lettera l), del decreto NIS che prevede che tali obblighi sono stabiliti
|
||||
con una o più Determinazioni dell’Agenzia per la cybersicurezza nazionale, sentito il Tavolo per
|
||||
l’attuazione della disciplina NIS;
|
||||
VISTO altresì l’articolo 42, comma 1, lettera c), del decreto NIS, che prevede, in fase di prima
|
||||
applicazione, che l’Autorità nazionale competente NIS stabilisce le modalità e le specifiche di base
|
||||
per l’adempimento 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 dell’Agenzia 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 l’Agenzia 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 l’attuazione 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 all’articolo 11, comma 4, lettera f), del decreto NIS;
|
||||
PRESSO ATTO dei riscontri pervenuti;
|
||||
SENTITO il Tavolo per l’attuazione 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”, l’Agenzia per la cybersicurezza nazionale
|
||||
di cui all’articolo 5, comma 1, del decreto-legge 14 giugno 2021, n. 82;
|
||||
c) “Autorità nazionale competente NIS”, l’Autorità nazionale competente di cui
|
||||
all’articolo 10, comma 1, del decreto NIS;
|
||||
d) “Autorità di settore NIS”, le Amministrazioni di cui all’articolo 11, commi 1 e 2, del
|
||||
decreto NIS;
|
||||
e) “soggetto NIS”, un soggetto, di cui all’articolo 2, comma 1, lettera hhh), del decreto
|
||||
NIS, di natura giuridica pubblica o privata che rientra nell’ambito 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 nell’elenco dei soggetti NIS”, la comunicazione di cui
|
||||
all’articolo 7, comma 3, lettera a), del decreto NIS;
|
||||
i) “organi di amministrazione e direttivi”, gli organi di amministrazione e direttivi di cui
|
||||
all’articolo 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 all’articolo 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 nell’ambito di
|
||||
applicazione del decreto NIS;
|
||||
m) “fornitori di servizi di registrazione dei nomi di dominio”, i fornitori di cui all’articolo
|
||||
2, comma 1, lettera oo), del decreto NIS;
|
||||
n) “gestori di registri dei nomi di dominio di primo livello”, i gestori di cui all’articolo 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 dell’operatore di servizi
|
||||
essenziali che abilitano i servizi essenziali per i quali l’operatore 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) all’1% della base di utenti nazionale, calcolato sulla base dei dati pubblicati
|
||||
dall’Osservatorio trimestrale delle comunicazioni a cura dell’Autorità per le
|
||||
garanzie nelle comunicazioni;
|
||||
2) a un milione;
|
||||
t) “sistemi informativi e di rete telco”, sistemi informativi e di rete per l’accesso alla rete
|
||||
fissa o mobile, da postazione o da terminale mobile, individuati come critici
|
||||
dall’operatore telco in quanto potenzialmente in grado di servire, per ciascun servizio
|
||||
indicato:
|
||||
1) una percentuale dell’utenza pari o superiore all’1% della base di utenti nazionale
|
||||
per quel servizio, sulla base dei dati pubblicati dall’Osservatorio trimestrale delle
|
||||
comunicazioni a cura dell’Autorità per le garanzie nelle comunicazioni;
|
||||
2) un’utenza 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, nell’allegato 1;
|
||||
b) per i soggetti essenziali, nell’allegato 2.
|
||||
3 di 6
|
||||
|
||||
Agenzia per la Cybersicurezza Nazionale
|
||||
3. Gli incidenti significativi di base sono stabiliti:
|
||||
a) per i soggetti importanti, nell’allegato 3;
|
||||
b) per i soggetti essenziali, nell’allegato 4.
|
||||
Articolo 3
|
||||
(Termini per l’adozione delle specifiche di base)
|
||||
1. Il termine per l’adozione 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 nell’elenco dei soggetti NIS.
|
||||
2. Il termine per l’adempimento dell’obbligo 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 nell’elenco dei soggetti NIS.
|
||||
Articolo 4
|
||||
(Sicurezza, stabilità e resilienza dei sistemi di nomi di dominio)
|
||||
1. Fermo restando quanto previsto dall’articolo 29 del decreto NIS, entro diciotto mesi dalla
|
||||
ricezione della comunicazione di inserimento nell’elenco 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 all’articolo 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 dell’articolo 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 all’allegato 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 dall’articolo 33 del decreto NIS, i soggetti PSNC-NIS
|
||||
notificano gli incidenti significativi di base di cui all’allegato 4, ai sensi dell’articolo 25 del
|
||||
decreto NIS, limitatamente ai sistemi informativi e di rete diversi da quelli PSNC.
|
||||
2. Il termine per l’obbligo 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 dall’articolo 2, comma 2, e dall’articolo 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 dell’entrata in vigore del decreto NIS ai sensi del
|
||||
decreto legislativo 18 maggio 2018, n. 65.
|
||||
2. Al fine di assicurare la continuità dell’obbligo di notifica di incidente di cui all’articolo 12,
|
||||
comma 5, del decreto legislativo 18 maggio 2018, n. 65, dall’entrata in vigore della presente
|
||||
determinazione, ai sensi dell’articolo 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) all’allegato 3, qualora siano soggetti importanti;
|
||||
b) all’allegato 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 dall’articolo 2, comma 2, e dall’articolo 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 dell’entrata 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à dell’obbligo di notifica di incidente di cui all’articolo 40,
|
||||
comma 3, lettera b), del decreto legislativo 1° agosto 2003, n. 259, ai sensi dell’articolo 25
|
||||
del decreto NIS, dall’entrata 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 all’allegato 3, qualora siano soggetti importanti;
|
||||
b) di cui all’allegato 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 un’ora 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 all’uno 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 dell’articolo 12, comma 6, del decreto NIS.
|
||||
Articolo 9
|
||||
(Pubblicità)
|
||||
1. La presente determinazione è pubblicata sui siti web istituzionali dell’Agenzia 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. L’articolo 2, commi 2 e 3, e l’articolo 3 entrano in vigore il giorno successivo
|
||||
all’esperimento 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
|
||||
|
||||
BIN
docs/nis2/Determina333017_sett2025.pdf
Normal file
BIN
docs/nis2/Determina333017_sett2025.pdf
Normal file
Binary file not shown.
678
docs/nis2/Determina333017_sett2025.pdf.txt
Normal file
678
docs/nis2/Determina333017_sett2025.pdf.txt
Normal file
@ -0,0 +1,678 @@
|
||||
Agenzia per la Cybersicurezza Nazionale
|
||||
Determinazione del Direttore generale dell’Agenzia per la
|
||||
cybersicurezza nazionale
|
||||
di cui all’articolo 7, comma 6, del decreto legislativo 4 settembre 2024, n. 138, adottata
|
||||
secondo le modalità di cui all’articolo 40, comma 5, recante termini, modalità e procedimenti
|
||||
di utilizzo e accesso alla piattaforma digitale nonché ulteriori informazioni che i soggetti
|
||||
devono fornire all’Autorità 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
|
||||
dell’architettura nazionale di cybersicurezza e istituzione dell’Agenzia 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 nell’Unione, 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, l’articolo 7 e l’articolo 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 l’applicazione del
|
||||
regolamento n. 85/2137/CEE relativo all’istituzione di un Gruppo europeo di interesse economico GEIE, ai sensi dell’art. 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, l’articolo 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 dell’articolo 7, comma 6, e dell’articolo 40, comma 5,
|
||||
lettera b), del decreto legislativo 4 settembre 2024, n. 138, con determinazione dell’Agenzia per la
|
||||
cybersicurezza nazionale, sentito il Tavolo per l’attuazione della disciplina NIS, sono stabiliti i
|
||||
termini, le modalità nonché i procedimenti di utilizzo e accesso alla piattaforma digitale di cui
|
||||
all’articolo 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
|
||||
all’articolo 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 l’attuazione della disciplina NIS di cui all’articolo 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”, l’Agenzia per la cybersicurezza nazionale
|
||||
di cui all’articolo 5, comma 1, del decreto-legge 14 giugno 2021, n. 82;
|
||||
c) “Autorità nazionale competente NIS”, l’Autorità nazionale competente di cui
|
||||
all’articolo 10, comma 1, del decreto NIS;
|
||||
d) “Autorità di settore NIS”, le Amministrazioni di cui all’articolo 11, commi 1 e 2, del
|
||||
decreto NIS;
|
||||
e) “organi di amministrazione e direttivi”, gli organi di amministrazione e direttivi di cui
|
||||
all’articolo 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
|
||||
l’Agenzia per la cybersicurezza nazionale mette a disposizione dei suoi interlocutori e
|
||||
dei soggetti, pubblici e privati, che rientrano nell’ambito di applicazione della disciplina
|
||||
cyber o con i quali l’Agenzia deve interagire ai sensi della stessa;
|
||||
2 di 16
|
||||
|
||||
Agenzia per la Cybersicurezza Nazionale
|
||||
g) “SPID”, il Sistema pubblico dell’identità digitale, istituito ai sensi dell'art. 64 del CAD,
|
||||
modificato dall’art. 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 d’identità personale garantita dallo
|
||||
Stato e rilasciata dal Ministero dell’Interno che permette l’accertamento dell’identità del
|
||||
possessore e l’accesso ai servizi online delle Pubbliche Amministrazioni;
|
||||
i) “Servizi NIS”, i servizi, accessibili tramite il Portale ACN, necessari per supportare
|
||||
l’espletamento degli adempimenti previsti dal decreto NIS e le interlocuzioni tra
|
||||
l’Autorità nazionale compente NIS e i soggetti;
|
||||
j) “Servizio NIS/Dichiarazione”, il Servizio NIS reso disponibile dall’Autorità nazionale
|
||||
competente NIS ai soggetti ai fini della registrazione di cui all’articolo 7, comma 1, del
|
||||
decreto NIS;
|
||||
k) “Servizio NIS/Aggiornamento annuale informazioni”, il Servizio NIS reso disponibile
|
||||
dall’Autorità nazionale competente NIS ai soggetti NIS per l’aggiornamento annuale,
|
||||
delle informazioni di cui all’articolo 7, commi 4 e 5, del decreto NIS;
|
||||
l) “Servizio NIS/Aggiornamento continuo informazioni”, il Servizio NIS reso disponibile
|
||||
dall’Autorità nazionale competente NIS ai soggetti NIS per l’aggiornamento continuo,
|
||||
delle informazioni trasmesse ai sensi dell’articolo 7, comma 7, del decreto NIS;
|
||||
m) “sito web”, il sito web istituzionale dell’Agenzia per la cybersicurezza nazionale
|
||||
(acn.gov.it);
|
||||
n) “piattaforma digitale”, la piattaforma digitale di cui all’articolo 7, comma 1, del decreto
|
||||
NIS, accessibile tramite il Portale ACN per l’erogazione dei Servizi NIS;
|
||||
o) “soggetto”, un soggetto, di cui all’articolo 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 nell’ambito di applicazione del decreto NIS;
|
||||
q) “punto di contatto”, la persona fisica designata dal soggetto NIS ai sensi dell’articolo 7,
|
||||
comma 1, lettera c), del decreto NIS;
|
||||
r) “sostituto punto di contatto”, la persona fisica designata dal soggetto NIS ai sensi
|
||||
dell’articolo 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 l’efficace interlocuzione con l’Autorità
|
||||
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 all’articolo 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 all’articolo 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 dell’associazione
|
||||
dell’utenza con un soggetto che consente all’utente stesso di accedere al Portale ACN
|
||||
e, in particolare, ai Servizi NIS;
|
||||
z) “registrazione”, il processo di cui all’articolo 7, comma 1, del decreto NIS;
|
||||
aa) “dichiarazione”, la dichiarazione resa dal punto di contatto ai fini della registrazione;
|
||||
bb) “elenco dei soggetti NIS”, l’elenco dei soggetti essenziali e dei soggetti importanti di
|
||||
cui all’articolo 7, comma 3, del decreto NIS;
|
||||
cc) “impresa collegata”, un soggetto che soddisfa i criteri di cui all’articolo 3, paragrafi 2 e
|
||||
3, dell’allegato 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 dell’articolo 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 all’articolo 7, commi 4 e 5,
|
||||
del decreto NIS;
|
||||
hh) “aggiornamento continuo delle informazioni”, il processo tramite il quale, ai sensi
|
||||
dell’articolo 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 all’articolo 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 all’Autorità 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 nell’Unione.
|
||||
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 l’associazione 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 l’aggiornamento continuo delle informazioni, tramite il “Servizio
|
||||
NIS/Aggiornamento continuo informazioni”.
|
||||
3. Ai sensi dell’articolo 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 all’articolo 7 del medesimo decreto e sono
|
||||
responsabili delle eventuali violazioni.
|
||||
4. La mancata registrazione, comunicazione o aggiornamento delle informazioni di cui
|
||||
all’articolo 7 del decreto NIS, con le modalità sopra indicate, è punita ai sensi dell’articolo
|
||||
38 del medesimo decreto.
|
||||
Articolo 3
|
||||
(Termini di uso del Portale ACN e dei Servizi NIS)
|
||||
1. I soggetti comunicano con l’Autorità nazionale competente NIS, anche ai fini del
|
||||
censimento, dell’associazione e della registrazione, esclusivamente tramite i Servizi NIS o
|
||||
tramite la sezione dedicata nell’area NIS del sito web, salvo in caso di diversa espressa
|
||||
specifica indicazione dell’Autorità nazionale competente o per cause di forza maggiore,
|
||||
fermo restando quanto previsto dal decreto NIS.
|
||||
2. Le istruttorie dell’Autorità 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 dell’Autorità 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 dell’articolo 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 nell’area 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 all’originatore e ai destinatari
|
||||
dell’informazione, 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 all’articolo 7 del decreto NIS, e interloquisce, per conto del soggetto
|
||||
NIS, con l’Autorità 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 all’articolo 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,
|
||||
nell’espletamento 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 un’altra impresa del gruppo che rientra
|
||||
nell’ambito 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 un’altra pubblica
|
||||
amministrazione che rientra nell’ambito 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 dell’articolo 23 del decreto NIS e delle persone fisiche ai sensi
|
||||
dell’articolo 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 all’articolo 1, comma 1,
|
||||
della legge 28 giugno 2024, n. 90, che rientrano nell’ambito di applicazione del decreto NIS,
|
||||
può soddisfare l’obbligo di nomina e comunicazione del referente per la cybersicurezza di
|
||||
cui all’articolo 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 quest’ultimo ai sensi dell’articolo 4, a cui si applicano le
|
||||
previsioni del citato articolo.
|
||||
2. Il sostituto punto di contatto supporta il punto di contatto nell’esercizio delle proprie
|
||||
funzioni, può interloquire direttamente con l’Autorità nazionale competente NIS e può
|
||||
effettuare sulla piattaforma digitale le medesime azioni del punto di contatto, ad eccezione
|
||||
della registrazione di cui all’articolo 7 del decreto NIS.
|
||||
3. Il sostituto punto di contatto è designato entro il 31 maggio dell’anno in cui il soggetto NIS
|
||||
ha ricevuto comunicazione di inserimento nell’elenco dei soggetti NIS.
|
||||
4. L’obbligo di designazione del sostituto punto di contatto non si applica ai soggetti NIS che
|
||||
versino nell’impossibilità materiale di effettuare tale adempimento, in quanto il punto di
|
||||
contatto è l’unica persona fisica operante nell’organizzazione.
|
||||
Articolo 6
|
||||
(Rappresentante nell’Unione)
|
||||
1. Per designare il proprio rappresentante NIS in Italia, i soggetti NIS di cui all’articolo 5,
|
||||
comma 1, lettera b), del decreto NIS, trasmettono e aggiornano, dal primo settembre al trenta
|
||||
novembre di ogni anno, al domicilio digitale dell’Agenzia 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
|
||||
l’Autorità nazionale competente NIS.
|
||||
2. L’Autorità nazionale competente NIS comunica al domicilio digitale del soggetto
|
||||
l’autorizzazione, 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 dall’articolo 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 quest’ultimo 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 all’articolo 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 all’articolo 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 nell’esercizio 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 dell’organizzazione 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 dell’utenza del punto di contatto e del sostituto al soggetto NIS)
|
||||
1. Il punto di contatto, censito sul Portale ACN, effettua l’associazione della sua utenza con il
|
||||
soggetto che lo ha designato attraverso la digitazione del suo codice fiscale o del codice
|
||||
dell’indice dei domicili digitali delle pubbliche amministrazioni e dei gestori di pubblici
|
||||
servizi (IPA).
|
||||
2. L’utente:
|
||||
a) verifica la denominazione nonché l’indirizzo, 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 dell’organizzazione 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, l’utente 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. L’associazione dell’utenza del punto di contatto è sottoposta alla convalida del soggetto
|
||||
NIS, secondo la procedura telematica indicata nella richiesta inviata al domicilio digitale di
|
||||
quest’ultimo.
|
||||
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 dell’associazione dell’utenza del punto di contatto ad un soggetto NIS
|
||||
determina la dissociazione, se presente, dell’utenza del punto di contatto precedentemente
|
||||
associata.
|
||||
7. Il sostituto punto di contatto effettua, su invito del punto di contatto, l’associazione 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 all’Autorità 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 dell’elenco 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, l’utente:
|
||||
a) qualora il soggetto non sia un’impresa 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 un’impresa autonoma, elenca i soggetti NIS di cui è a
|
||||
conoscenza che sono imprese collegate nei confronti delle quali soddisfi almeno uno
|
||||
dei criteri di cui all’articolo 3, comma 10, del decreto NIS, indicando il codice fiscale
|
||||
di tali imprese e quale dei criteri è soddisfatto;
|
||||
c) qualora il soggetto non sia un’impresa autonoma, elenca le imprese collegate che
|
||||
soddisfano nei suoi confronti almeno uno dei criteri di cui all’articolo 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 l’attività del soggetto;
|
||||
e) indica le normative settoriali dell’Unione europea citate negli allegati I e II del decreto
|
||||
NIS per definire le tipologie di soggetto che rientrano nell’ambito 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 l’appartenenza 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 l’obbligo di registrazione per i soggetti di cui all’articolo 3, comma 10, del
|
||||
decreto NIS, l’Autorità 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 dell’articolo 6, paragrafo 2, dell’allegato alla raccomandazione
|
||||
2003/361/CE.
|
||||
5. Qualora in fase di compilazione della dichiarazione siano rilevate incongruenze, queste sono
|
||||
segnalate all’utente che deve procedere a:
|
||||
a) modificare la dichiarazione correggendo le informazioni errate o incomplete;
|
||||
b) fornire ulteriori elementi informativi per giustificare l’incongruenza rilevata.
|
||||
6. Al termine della compilazione della dichiarazione, all’utente è 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” all’Autorità nazionale competente
|
||||
NIS. Copia di tali informazioni, per ricevuta, è inviata al domicilio digitale del soggetto con
|
||||
l’avvertenza che tale dichiarazione potrà essere sottoposta alle verifiche di coerenza di cui
|
||||
all’articolo 14.
|
||||
8. Decorsi dieci giorni solari dalla sottomissione della dichiarazione questa si intende
|
||||
definitivamente acquisita e non ulteriormente modificabile dall’utente.
|
||||
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 all’utente.
|
||||
10. Ai soggetti già inseriti nell’elenco dei soggetti NIS, all’avvio della registrazione per l’anno
|
||||
2026, viene presentata una bozza di dichiarazione precompilata sulla base delle
|
||||
informazioni trasmesse nel corso dell’anno solare precedente tramite i Servizi NIS.
|
||||
Articolo 12
|
||||
(Clausola di salvaguardia)
|
||||
1. Nel caso in cui l’utente ritenga che il calcolo effettuato ai sensi dell’articolo 11, comma 4,
|
||||
non sia proporzionato tenuto conto dei criteri di cui al decreto del Presidente del Consiglio
|
||||
dei ministri di cui all’articolo 40, comma 1, lettera a), del decreto NIS, nel corso della
|
||||
11 di 16
|
||||
|
||||
Agenzia per la Cybersicurezza Nazionale
|
||||
registrazione può chiedere l’applicazione della clausola di salvaguardia di cui all’articolo 3,
|
||||
comma 12, del decreto NIS.
|
||||
2. Ai fini della richiesta di cui al comma 1, tramite il “Servizio NIS/Dichiarazione”, l’utente
|
||||
fornisce gli elementi di valutazione corrispondenti ai criteri di cui al decreto del Presidente
|
||||
del Consiglio dei ministri di cui all’articolo 40, comma 1, lettera a), del decreto NIS
|
||||
all’Autorità nazionale competente NIS, che li condivide con le Autorità di settore interessate
|
||||
ai fini delle valutazioni di cui all’articolo 11, comma 4, lettera c), del medesimo decreto.
|
||||
3. Al soggetto NIS è fornito riscontro con la comunicazione dell’Autorità nazionale
|
||||
competente NIS ai sensi dell’articolo 7, comma 3, del decreto NIS.
|
||||
|
||||
Articolo 13
|
||||
(Individuazione da parte dell’Autorità nazionale competente NIS)
|
||||
1. I soggetti che ricevono una notifica di individuazione da parte dell’Autorità nazionale
|
||||
competente NIS, su proposta delle Autorità di settore, ai sensi dell’articolo 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, dall’Autorità nazionale competente NIS d’intesa con le Autorità di settore e non
|
||||
sollevano il soggetto NIS dall’obbligo del rispetto dei termini d’uso di cui all’articolo 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, l’Autorità 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 dall’Autorità 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,
|
||||
l’Autorità nazionale competente comunica, tramite i Servizi NIS al domicilio digitale del
|
||||
soggetto NIS:
|
||||
a) l’esito positivo della verifica;
|
||||
12 di 16
|
||||
|
||||
Agenzia per la Cybersicurezza Nazionale
|
||||
b) l’esito negativo della verifica.
|
||||
5. La comunicazione di cui al comma 4, lettera b), non solleva il soggetto NIS dall’obbligo di
|
||||
registrazione di cui all’articolo 7, comma 1, del decreto NIS.
|
||||
Articolo 15
|
||||
(Elaborazione dell’elenco dei soggetti NIS e comunicazioni)
|
||||
1. L’elenco dei soggetti NIS di cui all’articolo 7, comma 2, del decreto NIS è elaborato
|
||||
dall’Autorità 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 dell’articolo 11, comma 4, lettera a), del medesimo decreto che, ove
|
||||
necessario, possono proseguire anche successivamente all’elaborazione dell’elenco 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 dell’elenco dei
|
||||
soggetti NIS.
|
||||
3. Ai sensi dell’articolo 7, comma 3, del decreto NIS, l’Autorità nazionale competente NIS
|
||||
comunica ai soggetti registrati l'inserimento, o meno, nell'elenco dei soggetti NIS. Ai
|
||||
soggetti inseriti nell’elenco 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 l’Autorità nazionale competente NIS.
|
||||
|
||||
Capo IV
|
||||
Aggiornamento delle informazioni
|
||||
Articolo 16
|
||||
(Processo per l’aggiornamento 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 dall’articolo 4, il punto di contatto si assicura che la delega
|
||||
conferitagli dal rappresentante legale del soggetto sia corretta, aggiornata e conforme a
|
||||
quanto previsto dall’articolo 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 dall’articolo 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 l’aggiornamento:
|
||||
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, l’indirizzo della sede legale, l’indicazione del
|
||||
rappresentante legale, l’elenco dei procuratori generali, il numero di telefono, il
|
||||
domicilio digitale e un indirizzo di posta elettronica ordinaria funzionale;
|
||||
b) dell’elenco dei componenti degli organi di amministrazione e direttivi, quali persone
|
||||
fisiche responsabili ai sensi dell’articolo 38, comma 5, del decreto NIS;
|
||||
c) ove applicabile, dell’elenco dei servizi che rientrano nell’ambito di applicazione della
|
||||
direttiva 2022/2555 che il soggetto NIS offre nell’UE 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) dell’elenco degli accordi di condivisione delle informazioni
|
||||
f) dei dati identificativi del referente CSIRT e degli eventuali sostituti.
|
||||
4. Per i soggetti NIS di cui all’articolo 7, comma 5, del decreto NIS gli utenti verificano la
|
||||
correttezza e l’aggiornamento dell’elenco delle sedi del soggetto NIS nell’Unione,
|
||||
indicandone l’indirizzo.
|
||||
5. Per i soggetti NIS di cui all’articolo 5, comma 1, lettera b), del decreto NIS, che hanno
|
||||
designato il proprio rappresentante NIS in Italia ai sensi dell’articolo 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
|
||||
dell’organizzazione, indicandone la suddivisione in articolazioni di primo livello. Tale
|
||||
descrizione della struttura organizzativa potrà essere impiegata per l’eventuale 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”
|
||||
all’Autorità nazionale competente NIS. Copia di tali informazioni, per ricevuta, è inviata al
|
||||
domicilio digitale del soggetto.
|
||||
8. La modifica, confermata da punto di contatto, dell’indicazione del rappresentante legale o
|
||||
dell’elenco dei procuratori generali del soggetto NIS è sottoposta alla convalida del soggetto
|
||||
medesimo, secondo la procedura telematica indicata nella richiesta inviata al domicilio
|
||||
digitale di quest’ultimo.
|
||||
9. Le modifiche dei dati anagrafici e di contatto del soggetto NIS sono trasmesse, per ricevuta,
|
||||
al domicilio digitale di quest’ultimo.
|
||||
10. Le modifiche dei dati anagrafici e di contatto degli utenti sono trasmesse, per ricevuta,
|
||||
all’indirizzo di posta elettronica certificata indicata dall’utente stesso o, in subordine,
|
||||
all’indirizzo di posta elettronica ordinaria indicata dall’utente stesso.
|
||||
11. In fase di prima applicazione, fermo restando quanto previsto dall’articolo 35, comma 3,
|
||||
lettera c), e dall’articolo 42, comma 1, lettera c), del decreto NIS, in caso di registrazione
|
||||
tardiva, il termine per completare l’aggiornamento annuale è comunicato di volta in volta
|
||||
dall’Autorità nazionale competente NIS.
|
||||
|
||||
14 di 16
|
||||
|
||||
Agenzia per la Cybersicurezza Nazionale
|
||||
12. Il comma 3, lettera b), del presente articolo e l’articolo 17 non si applicano ai soggetti che
|
||||
rientrano nell’ambito di applicazione del decreto NIS e del Regolamento UE 2022/2554
|
||||
(DORA).
|
||||
Articolo 17
|
||||
(Elencazione degli organi di amministrazione e direttivi)
|
||||
1. Ai fini dell’articolo 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 l’indirizzo di posta
|
||||
elettronica certificata.
|
||||
2. Le informazioni di cui al comma 1 sono confermate dal punto di contatto.
|
||||
3. Ai fini dell’articolo 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 l’aggiornamento continuo delle informazioni)
|
||||
1. A seguito del perfezionamento dell’aggiornamento annuale, laddove siano sopravvenute
|
||||
modifiche alle informazioni trasmesse ai sensi dell’articolo 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. L’aggiornamento continuo delle informazioni è possibile fino al 14 aprile di ogni anno
|
||||
successivo alla ricezione della comunicazione di cui all’articolo 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”
|
||||
all’Autorità nazionale competente NIS. Copia di tali informazioni, per ricevuta, è inviata al
|
||||
domicilio digitale del soggetto.
|
||||
4. La modifica, confermata da punto di contatto, dell’indicazione del rappresentante legale o
|
||||
dell’elenco dei procuratori generali del soggetto NIS è sottoposta alla convalida del soggetto
|
||||
medesimo, secondo la procedura telematica indicata nella richiesta inviata al domicilio
|
||||
digitale di quest’ultimo.
|
||||
5. Le modifiche dei dati anagrafici e di contatto del soggetto NIS sono trasmesse, per ricevuta,
|
||||
al domicilio digitale di quest’ultimo.
|
||||
6. Le modifiche dei dati anagrafici e di contatto degli utenti sono trasmesse, per ricevuta,
|
||||
all’indirizzo di posta elettronica certificata indicata dall’utente stesso o, in subordine,
|
||||
all’indirizzo di posta elettronica ordinaria indicata dall’utente 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 dell’Autorità 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
|
||||
|
||||
BIN
docs/nis2/Dir2022_2555_UE_NIS2_ITA.pdf
Normal file
BIN
docs/nis2/Dir2022_2555_UE_NIS2_ITA.pdf
Normal file
Binary file not shown.
4772
docs/nis2/Dir2022_2555_UE_NIS2_ITA.pdf.txt
Normal file
4772
docs/nis2/Dir2022_2555_UE_NIS2_ITA.pdf.txt
Normal file
File diff suppressed because it is too large
Load Diff
BIN
docs/nis2/Dir2022_2557_UE_ITA.pdf
Normal file
BIN
docs/nis2/Dir2022_2557_UE_ITA.pdf
Normal file
Binary file not shown.
2266
docs/nis2/Dir2022_2557_UE_ITA.pdf.txt
Normal file
2266
docs/nis2/Dir2022_2557_UE_ITA.pdf.txt
Normal file
File diff suppressed because it is too large
Load Diff
88
docs/nis2/INTEGRAZIONE_COMPLETATA.md
Normal file
88
docs/nis2/INTEGRAZIONE_COMPLETATA.md
Normal 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
2015
docs/nis2/assets.html
Normal file
File diff suppressed because it is too large
Load Diff
1481
docs/nis2/dashboard.html
Normal file
1481
docs/nis2/dashboard.html
Normal file
File diff suppressed because it is too large
Load Diff
450
docs/nis2/doc-network-physical.html
Normal file
450
docs/nis2/doc-network-physical.html
Normal 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>
|
||||
482
docs/nis2/doc-relevant-systems.html
Normal file
482
docs/nis2/doc-relevant-systems.html
Normal 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>
|
||||
1402
docs/nis2/incidente_r01/incident-dashboard.html
Normal file
1402
docs/nis2/incidente_r01/incident-dashboard.html
Normal file
File diff suppressed because it is too large
Load Diff
1805
docs/nis2/incidente_r01/incident-detail.html
Normal file
1805
docs/nis2/incidente_r01/incident-detail.html
Normal file
File diff suppressed because it is too large
Load Diff
353
docs/nis2/incidente_r01/incident-gate.html
Normal file
353
docs/nis2/incidente_r01/incident-gate.html
Normal 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>
|
||||
956
docs/nis2/incidente_r01/incident-new.html
Normal file
956
docs/nis2/incidente_r01/incident-new.html
Normal 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">Sì</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>
|
||||
758
docs/nis2/incidente_r01/incident-notification.html
Normal file
758
docs/nis2/incidente_r01/incident-notification.html
Normal 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">Sì</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>
|
||||
803
docs/nis2/incidente_r01/incident-pir.html
Normal file
803
docs/nis2/incidente_r01/incident-pir.html
Normal 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>
|
||||
660
docs/nis2/incidente_r01/incident-recovery.html
Normal file
660
docs/nis2/incidente_r01/incident-recovery.html
Normal 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>
|
||||
|
||||
61
docs/sql/012_consulting_firms.sql
Normal file
61
docs/sql/012_consulting_firms.sql
Normal 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;
|
||||
41
docs/sql/013_firm_assignments.sql
Normal file
41
docs/sql/013_firm_assignments.sql
Normal 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;
|
||||
66
docs/sql/020_asset_relevance.sql
Normal file
66
docs/sql/020_asset_relevance.sql
Normal 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;
|
||||
36
docs/sql/021_incident_nis2_taxonomy.sql
Normal file
36
docs/sql/021_incident_nis2_taxonomy.sql
Normal 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;
|
||||
73
docs/sql/022_incident_metrics_pir.sql
Normal file
73
docs/sql/022_incident_metrics_pir.sql
Normal 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;
|
||||
128
docs/sql/seed_agile_tech_demo.sql
Normal file
128
docs/sql/seed_agile_tech_demo.sql
Normal 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;
|
||||
1432
docs/standards/STANDARD_PERSONA_CONVERSATIONAL_RULES.md
Normal file
1432
docs/standards/STANDARD_PERSONA_CONVERSATIONAL_RULES.md
Normal file
File diff suppressed because it is too large
Load Diff
@ -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>≥${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
775
public/guida.html
Normal 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 & 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> = 50–249 dipendenti <em>oppure</em> fatturato tra 10 e 50 milioni €.</li>
|
||||
<li><strong>Grande impresa</strong> = ≥250 dipendenti <em>oppure</em> fatturato >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 6–8 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> (1–5).</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à (1–5)</strong>: 1 = quasi mai, 5 = quasi certo.</li>
|
||||
<li><strong>Impatto (1–5)</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, 9–15 alto, 4–8 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 (1–5 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>> 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 > <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 4–5 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 (0–10).</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> → "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 & 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 (0–100%).</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
1268
public/index-en.html
Normal file
File diff suppressed because it is too large
Load Diff
@ -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>
|
||||
|
||||
@ -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
548
public/js/ai-assistant.js
Normal 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 ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[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">👍 ' + 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">👎 ' + 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">'
|
||||
+ '🔄 ' + 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">'
|
||||
+ '👨‍🏫 ' + 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">'
|
||||
+ '🐛 ' + 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">×</button>'
|
||||
+ '</div>'
|
||||
|
||||
// Context bar
|
||||
+ (page ? '<div style="padding:8px 16px;background:#EDE9FE;font-size:11px;color:#6D28D9">'
|
||||
+ '📍 ' + 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">'
|
||||
+ '💡 ' + 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 ? '⏹' : '🎤') + '</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();
|
||||
}
|
||||
})();
|
||||
@ -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
870
public/js/bug-reporter.js
Normal 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();
|
||||
}
|
||||
})();
|
||||
@ -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>` },
|
||||
|
||||
@ -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 ≥ 40</strong>. Classi: ≥80 Critico, 60-79 Alto, 40-59 Medio, 20-39 Basso, <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
230
public/js/kb.js
Normal 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 || '') + ' · ' + (d.chunk_count || 0) + ' chunk · ' + 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
134
public/kb.html
Normal 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–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>
|
||||
189
public/mobile-conversion.css
Normal file
189
public/mobile-conversion.css
Normal 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
192
public/mobile-conversion.js
Normal 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();
|
||||
}
|
||||
})();
|
||||
@ -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');
|
||||
|
||||
209
public/service-continuity.html
Normal file
209
public/service-continuity.html
Normal 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>: < 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 > 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>< 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>< 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>< 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>< 30 secondi</td><td>Docker restart + healthcheck</td></tr>
|
||||
<tr><td>Recovery da aggiornamento</td><td>< 5 minuti</td><td>Maintenance mode + deploy</td></tr>
|
||||
<tr><td>Rilevamento downtime</td><td>< 5 minuti</td><td>Cron monitor + email alert</td></tr>
|
||||
<tr><td>Tempo risposta API (p95)</td><td>< 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>© 2026 NIS2 Agile — <a href="https://nis2.agile.software" style="color:#1565C0">nis2.agile.software</a></div>
|
||||
<div>Standard Continuita di Servizio v1.0 • Aprile 2026 • Agile Technology SRL</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -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."}
|
||||
|
||||
398
scripts/import-feedback-to-nexus.php
Normal file
398
scripts/import-feedback-to-nexus.php
Normal 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);
|
||||
213
scripts/ingest-nis2-sources.php
Normal file
213
scripts/ingest-nis2-sources.php
Normal 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 ===");
|
||||
Loading…
Reference in New Issue
Block a user