[DOCS] Design modulo questionari fornitori + portale OTP + AI consulente normativo

Pacchetto di design completo (nessun codice applicato, nessuna migrazione eseguita):
- DESIGN aggiornato con 5 review agenti + 3 decisioni utente + pilastro AI consulente (sez. 12-14)
- docs/supplier-portal/template-nis2-base.questions.json: 26 domande GV.SC (Allegato 2 ACN) con nis2_ref/vuln_flag e fonti certe verbatim
- docs/supplier-portal/AI_CONSULENTE_NORMATIVO.md: corpus normativo aggiornato + persona consulente (modello TRPG)
- docs/supplier-portal/UX_MINI_SPEC.md: mini-spec portale fornitore (stati/copy/autosave/mobile/a11y/editor no-code)
- docs/sql/032-035: migrazioni idempotenti proposte (modulo, suppliers, portale auth, migrazione 027->campaigns)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile 2026-05-31 09:53:07 +02:00
parent dce53a0d0c
commit aa2db4c6c2
8 changed files with 749 additions and 2 deletions

View File

@ -48,6 +48,33 @@ Valore: due-diligence continua dei fornitori (Art. 21.2(d) NIS2; coerente con EN
---
## 3.5 Caricamento anagrafica fornitori (per-azienda-cliente: CSV + API key) — requisito utente 2026-05-31
**Principio**: i fornitori NON si auto-registrano. L'**anagrafica fornitori è di proprietà dell'azienda cliente** ed è **diversa per ogni org**. L'azienda cliente popola la propria lista fornitori; solo dopo, il singolo referente fornitore accede via OTP/magic-link (§3) per compilare i questionari a lui assegnati.
### Tre canali di ingestion (lato azienda cliente)
1. **Manuale** — form esistente sulla scheda fornitore (`supply-chain.html`). Già presente.
2. **Import CSV/Excel** — upload file → preview/mapping colonne → `bulkUpsert` idempotente. **Riuso del pattern già implementato** `AssetController::import()` + `bulkUpsert()` (mig. 025 asset `external_ref`/`discovery_source`). Dedup per `(organization_id, vat_number)` o `(organization_id, external_ref)`.
3. **API key all'azienda cliente** — integrazione programmatica (es. il gestionale/ERP del cliente spinge i fornitori). **Riuso dell'infra esistente** `api_keys` + pattern `X-API-Key` di `ServicesController` (come gli endpoint `ingestAssets`/`ingestIncident` già scritti). Nuovo scope dedicato **`write:suppliers`**. Endpoint upsert idempotente per `external_ref`.
### Impatto schema (additivo su `suppliers`)
- `external_ref VARCHAR NULL` — chiave di upsert idempotente dal sistema sorgente del cliente (come `assets.external_ref`).
- `source ENUM('manual','csv','api') DEFAULT 'manual'` — provenienza del record (tracciabilità/audit).
- `UNIQUE(organization_id, external_ref)` per evitare doppioni in re-import/re-push (NULL multipli ammessi → i record manuali senza ref non confliggono).
### API (lato azienda)
- `POST /api/supply-chain/import` (JWT interno) — body CSV/JSON, `bulkUpsert` con report righe ok/scartate (pattern asset import).
- `POST /api/services/suppliers` (X-API-Key, scope `write:suppliers`) — upsert idempotente per `external_ref`; un'API key è **scoped alla singola org** (come tutte le `api_keys`), hashed, revocabile, con audit.
### Sicurezza
- API key per-org, hashed in DB, scope minimo `write:suppliers`, revocabile, scadenza; ogni write logga `logAudit`. **Mai** scope cross-org.
- CSV: validazione tipo/encoding, dimensione massima, sanitizzazione campi (no formula-injection in celle `=/+/-/@`), dedup, nessuna esecuzione lato server.
- `external_ref` è opaco dal lato cliente: non usarlo mai per costruire path/query non parametrizzate.
> **Distinzione chiave**: questo §3.5 è il caricamento **dell'anagrafica** (lato azienda, API key/CSV/manuale). Il §3 è l'**accesso del referente fornitore** (OTP/magic-link) per compilare i questionari. Due piani separati, due meccanismi di auth separati.
---
## 4. Schema dati (migrazioni nuove, additive)
```
@ -124,7 +151,8 @@ supplier_otp
| Fase | Contenuto | Rischio |
|---|---|---|
| **1** | Categorie (seed + CRUD) + template/domande configurabili in DB (migra le 8 domande) + render dinamico. Riusa il link-token attuale. | Basso |
| **0 (PREREQUISITO)** | `EmailService::sendViaRelay()` via relay AgileHub (`X-Internal-Key`, cURL come `SsoHelper::postInternal`, env multi-source per clear_env FPM). Ripuntare `sendQuestionnaire`/`forgotPassword` (oggi usano `mail()` → falliscono in silenzio). **Bloccante: senza, OTP/reminder/inviti non recapitano.** | Basso ma bloccante |
| **1** | Categorie (seed + CRUD) + template/domande configurabili in DB (migra le 8 domande) + render dinamico. Scrivere già le risposte in `questionnaire_answers` (campaign-stub) per non migrare in Fase 2. | Basso |
| **2** | Campagne con scadenze/ricorrenze configurabili + reminder (cron) + cruscotto. | Medio |
| **3** | **Portale fornitore + auth OTP/magic-link** (`supplier_users`, `supplier_otp`, `/api/supplier-portal/*`, supplier-portal.html). | Alto (auth esterna) |
| **4** | Tipi domanda avanzati (allegati/scale/multi) + scoring configurabile per peso. | Medio |
@ -151,7 +179,12 @@ Tutto additivo; nessuna modifica distruttiva ai moduli esistenti.
1. ✅ Set categorie predefinite confermato (Cloud/IaaS, MSP/MSSP, Software/SaaS, Hardware/Manutenzione, Consulenza, Logistica, Telecomunicazioni, Altro) — personalizzabili.
2. ✅ Auth: **OTP a 6 cifre + magic-link, ENTRAMBI** (il fornitore può usare l'uno o l'altro).
3. ✅ **Sessione fornitore = 4 ore**; **validità OTP/magic-link = 15 minuti**, single-use, max 5 tentativi, rate-limit.
4. ✅ Processo: prima **review del design da 5 agenti**, poi implementazione partendo dalla **Fase 1**.
4. ✅ Processo: prima **review del design da 5 agenti** (COMPLETATA 2026-05-31, esiti §12), poi implementazione partendo dalla **Fase 0** (prerequisito email relay) → **Fase 1**.
5. ✅ **DECISO (utente, 2026-05-31, post-review)**:
- (a) **Migrazione + sostituzione**: `questionnaire_campaigns` sostituisce `supplier_questionnaires` (027). Migrazione dati `INSERT…SELECT`, retrocompat dei link `sq_` già inviati, 027 deprecata. Una sola fonte di verità.
- (b) **Template "NIS2 base" completo GV.SC** già in Fase 1: 8 domande attuali + domande mappate ai 5 fattori GV.SC-07 e agli ambiti GV.SC-01 §2 (con `nis2_ref` e fonti certe). Modulo "compliance-grade" dall'inizio.
- (c) **Anagrafica fornitori via manuale + CSV + API key cliente** (§3.5) inclusa nel piano.
6. ✅ **Contesto normativo + AI consulente (come TRPG)** — vedi §13. Pilastro trasversale: corpus normativo NIS2 incluso e mantenuto aggiornato nella KB, AI istruita a rispondere come consulente NIS2 reale, sempre ancorata alle fonti certe.
## 11. Promemoria post-implementazione (regola progetto — VINCOLANTE per ogni fase)
Ad ogni fase che cambia funzionalità, aggiornare SEMPRE:
@ -160,3 +193,79 @@ Ad ogni fase che cambia funzionalità, aggiornare SEMPRE:
- **Traduzioni** (`public/js/i18n.js`): chiavi IT + EN per le nuove UI (categorie, template, campagne, portale, OTP).
- **KB AI / product_knowledge**: aggiornare la knowledge base AI (RAG + product_knowledge AgileHub) con la nuova funzionalità, così l'assistente la conosce.
- `version.json` bump, commit per fase, push da host.
---
## 12. Esiti review 5 agenti (2026-05-31) + correzioni al design
Verdetto comune dei 5 reviewer (sicurezza OTP, UX, architettura/DB, fattibilità stack, GDPR/supply-chain): **impianto solido, NON implementabile "as-is"**. Le correzioni 🔴 qui sotto sono da chiudere prima della Fase 1; le ⚠️ in corso d'opera. Convergenze forti tra più agenti = priorità massima.
### 🔴 Bloccanti (recepiti nel design)
1. **Fase 0 — Email relay** *(fattibilità)*: `EmailService::send()` usa `mail()` (rotto nel container). Aggiungere `sendViaRelay()` via AgileHub `POST /api/emails/send` con `X-Internal-Key`, env multi-source (`getenv()?:$_SERVER?:$_ENV?:default` per clear_env FPM). **L'OTP va inviato via template `/api/emails/send` (NON `send-raw`)** così il codice non finisce nei log `email_log` *(convergenza sicurezza+fattibilità)*.
2. **Auth fornitore = libreria SEPARATA** *(sicurezza+architettura+fattibilità, 3 agenti)*: `SUPPLIER_JWT_SECRET` dedicato (da vault/.env), claim `aud:"supplier-portal"` SENZA `user_id`, `requireSupplierSession()` proprio. **MAI** passare per `BaseController::requireAuth()`/`active_sessions`. Sessione revocabile via tabella `supplier_sessions` (jti verificato a DB). Lockout persistente su `supplier_otp.attempts` + rate-limit DB (non solo `/tmp`), invalidazione OTP precedenti, `hash_equals`.
3. **`questionnaire_answers` con `organization_id`** denormalizzato *(architettura)* + `UNIQUE(campaign_id, question_id)` + FK `ON DELETE CASCADE` → no leak cross-org/IDOR.
4. **Schema → DDL completa** *(architettura)*: PK/FK/indici/tipi reali prima di codare. Indici sargable cron: `(status, next_reminder_at)`, `(status, due_at)`. `UNIQUE(organization_id, slug)` categorie + sentinel `organization_id=0` per i preset.
5. **Coesistenza vs migrazione `supplier_questionnaires` (027)** *(architettura+fattibilità)*: `questionnaire_campaigns` è superset di 027. **DECISIONE NEL DESIGN** (vedi §10 punto 5). Migrazioni nuove numerate **032+**, idempotenti via `information_schema` (no `ADD COLUMN IF NOT EXISTS`), applicate `mysql -h localhost` da Hetzner. Retrocompat token `sq_` + endpoint `public-questionnaire` in produzione.
6. **Versioning template = snapshot dedicato** *(architettura+UX, F17)*: tabella `questionnaire_template_versions(template_id, organization_id, version, questions_snapshot JSON incl. weight, change_note, created_by, UNIQUE(template_id,version))` come `policy_versions`. Le campagne in corso restano congelate alla loro versione.
7. **Contenuto normativo del template "NIS2 base"** *(GDPR/supply-chain)*: coprire i **5 fattori GV.SC-07** + ambiti **GV.SC-01 §2 a-q** applicabili (cancellazione dati a fine fornitura, subfornitura, secure development, ruoli personale GV.SC-02); campo `nis2_ref` per mapping; scoring **per-vulnerabilità** (flag "no MFA"/"no patch"/…) oltre al totale (Art.21.3), modulato per `criticality`. Fonti da aggiungere a `nis2_sources.php`: ENISA Good Practices SC + NIST SP 800-161r1.
8. **GDPR** *(GDPR/supply-chain)*: base giuridica = legittimo interesse Art.6(1)(f) + obbligo legale (c) (LIA); **informativa Art.13/14** in email OTP + landing portale (titolare=cliente, Agile=responsabile Art.28 + sub-processor disclosure); retention quantificata (purge `supplier_otp` 24-48h via cron; risposte/`supplier_users` per durata relazione + ≥5 anni audit); endpoint cancellazione/anonimizzazione Art.17 con tombstone (no distruzione evidenze).
9. **Immutabilità evidenze** *(GDPR/supply-chain)*: `questionnaire_answers` append-only + log submit/scoring in `AuditService` (hash-chain); ogni campagna conserva il proprio snapshot risposte+score (no sovrascrittura); **soft-delete** fornitore (`deleted_at`, no hard `Database::delete`).
10. **Salvataggio parziale (autosave)** *(UX, F11 — priorità assoluta)*: stato `in_progress` + endpoint `PATCH .../answers` (autosave per-domanda con `debounce`), "salva bozza" vs "invia". Le risposte parziali sopravvivono a logout/scadenza sessione.
### 🔴/⚠️ Routing (architettura+fattibilità)
Aggiungere `'supplier-portal' => 'SupplierPortalController'` a `controllerMap`. Modellare gli URL sulle forme che il router **produce davvero** (id numerico primo segmento): `POST:requestOtp`, `POST:verifyOtp`, `GET:me`, `GET:{id}` (campagna), `POST:{id}/submit`, `PATCH:{id}/answers`. La notazione `{campaignId}` del doc NON è riconosciuta.
### ⚠️ UX da chiudere nel design (mini-spec §6)
- **Email invito progettata** (F6): nome committente in oggetto/corpo, motivo, ~tempo, scadenza in chiaro, CTA singola, branding firm.
- **Gestione "email non arriva" + errori OTP umani** (F2/F3): countdown reinvio, "controlla spam", messaggi specifici allo step verifica (errato/scaduto/lockout/già usato) ognuno con via d'uscita; preservare rigenera-link manuale lato azienda.
- **Magic-link primario, OTP fallback** (F1) + niente doppio-consumo da prefetch mail mobile (F4): consumo solo all'apertura sessione esplicita.
- **Editor template no-code** (F16): nascondere slug/code/order_index/options JSON/reminder_offsets/recurrence_months/weight dietro chip/dropdown/drag + preview "come lo vede il fornitore".
- **i18n portale** (F20): lingua IT/EN impostabile dal cliente o selezionabile (fornitori esteri).
- **Ricevuta** (F22): email conferma + PDF risposte scaricabile. **Multi-referente/delega** (F24). **Score al fornitore configurabile, default OFF** (F23). **Progress bar** (F13). **Scadenza sempre visibile** (F7). **Mobile-first + a11y** (F10/F21): elementi nativi, ARIA, touch ≥44px, `autocomplete="one-time-code"`.
### ⚠️ Operativo (fattibilità)
Cron `supplier-questionnaire-cron.sh` (primo `.sh` del progetto): wrapper sottile + runner PHP CLI, `TZ=Europe/Rome`, fuori finestra DST 02:00-03:00, idempotente (UPDATE condizionato), entry in `CRON_REGISTRY.md`, crontab via richiesta utente. Disciplina: `kill -USR2 1` dopo ogni edit PHP, semaforo `/tmp/agent-working.lock`, commit per fase, push da host.
### Stima realistica rivista
~**6-7 sessioni** (vs 4): Fase 0 ~0.5 · Fase 1 ~1-1.5 · Fase 2 ~1.5 · Fase 3 ~2-2.5 · Fase 4 ~0.5-1.
---
## 13. Pilastro trasversale — Contesto normativo incluso/aggiornato + AI "vero consulente" (richiesta utente 2026-05-31, modello TRPG)
**Obiettivo**: NIS2 Agile deve comportarsi come un **consulente NIS2 reale** — conoscere e citare la normativa vigente (NIS2, D.Lgs. 138/2024, Determine/Allegati ACN, ENISA, NIST), tenerla **aggiornata**, e rispondere **sempre ancorata a fonti certe** (mai inventate). Allineamento al pattern già in uso su TRPG.
**Cosa esiste già in NIS2** (da censire e potenziare):
- RAG completo: Qdrant `nis2_kb` (512-dim Voyage), `VectorService`/`EmbedService`/`RagService`, `AIService::askWithRag()`.
- Registro fonti certe `application/config/nis2_sources.php` + `AIService::authoritativeSourcesBlock()` (vieta riferimenti inventati).
- Ingest corpus normativo: `scripts/ingest-nis2-sources.php` (PDF normativi → `nis2_kb` scope SYSTEM).
- Corpus locale: `docs/nis2/` (direttiva ITA, allegati ACN 1-4, ecc.).
**Gap da colmare** (output dell'agente di design dedicato):
1. **Completezza corpus**: verificare che TUTTE le fonti di `nis2_sources.php` siano effettivamente ingerite in `nis2_kb`; aggiungere ENISA Good Practices SC e NIST SP 800-161r1 (citati nel design ma assenti dal registro — F-12).
2. **Aggiornamento**: meccanismo per mantenere il corpus aggiornato (nuove Determine ACN, feed normativo già presente come `normative_updates`) → ingest incrementale + tracciamento versione fonte.
3. **AI "consulente"**: rafforzare i system prompt (persona consulente NIS2, tono, scope, citazione obbligatoria fonti) coerente con lo standard `persona-conversational-rules v2.0` (persona ARIA_SUPPORT_NIS2 già esistente). Ogni risposta normativa cita §/articolo della fonte.
4. **Copertura supply-chain**: il consulente deve saper rispondere su GV.SC-01..07, art.21.2(d)/21.3, e guidare la compilazione dei questionari fornitori (collega §7 fasi a questo pilastro).
**Nota**: questo pilastro è parzialmente indipendente dal modulo questionari ma li potenzia (help in-context durante la compilazione, spiegazione dei `nis2_ref`). Sequenziabile in parallelo alle Fasi.
---
## 14. Stato lavorazione (2026-05-31)
- ✅ Design + 5 review + decisioni utente consolidate (§10, §12, §13).
- ✅ 4 agenti di produzione-design completati e **materializzati come artefatti** (nessun codice applicato, nessun commit):
- (A) Contenuto template GV.SC → `docs/supplier-portal/template-nis2-base.questions.json` (26 domande, `nis2_ref`/`vuln_flag`, fonti certe verbatim).
- (B) Pilastro AI consulente + corpus → `docs/supplier-portal/AI_CONSULENTE_NORMATIVO.md`.
- (C) DDL migrazioni → `docs/sql/032_supplier_questionnaires_module.sql`, `033_suppliers_category_source.sql`, `034_supplier_portal_auth.sql`, `035_migrate_027_to_campaigns.sql` (idempotenti, NON applicate).
- (D) Mini-spec UX §6 → `docs/supplier-portal/UX_MINI_SPEC.md`.
- ⏳ Prossimo (su conferma utente): commit del pacchetto design su Gitea → implementazione **Fase 0** (email relay) → **Fase 1** (categorie + template GV.SC + import CSV/API + render dinamico), con `kill -USR2 1`/smoke/commit per fase e aggiornamento guida/help.js/i18n/KB AI.
### Roadmap implementazione consolidata (ordine)
| # | Fase | Output | Migrazioni | Rischio |
|---|---|---|---|---|
| 0 | Email relay | `EmailService::sendViaRelay()` + ripuntare sendQuestionnaire/forgotPassword | — | Basso (sblocca + fixa bug prod) |
| 1 | Categorie + template GV.SC + anagrafica (CSV/API) + render | seed 10 categorie + template NIS2 base (26 dom.) + import suppliers | 032, 033 | Basso |
| 2 | Campagne + scadenze/ricorrenze + reminder cron + cruscotto | — | (035 migrazione 027) | Medio |
| 3 | Portale fornitore + auth OTP/magic-link separata + sessioni revocabili | — | 034 | Alto (auth esterna) |
| 4 | Tipi domanda avanzati (file/scale/multi) + scoring per-vulnerabilità + ricevuta PDF | — | — | Medio |
| ‖ | Pilastro AI consulente (parallelo): corpus completo + persona + feed→KB + AI in-context | — | (KB su Hetzner) | Medio |

View File

@ -0,0 +1,209 @@
-- ============================================================================
-- Migration 032 - Modulo Questionari Fornitori (supply chain Art.21.2.d)
-- ----------------------------------------------------------------------------
-- PROPOSTA DI DESIGN (NON ancora applicata). Da eseguire su host MySQL.
--
-- SOSTITUISCE supplier_questionnaires (mig 027, da qui DEPRECATA/read-only) con
-- un modello completo a template versionati + campagne + domande + risposte:
--
-- supplier_categories = tassonomia fornitori (preset di sistema + per-org)
-- questionnaire_templates = modelli di questionario (per-org, opz. per categoria)
-- questionnaire_template_versions= snapshot versionato (questions_snapshot JSON, weight)
-- questionnaire_questions = domande "live" del template (ordinabili, pesate)
-- questionnaire_campaigns = invio di un template a un fornitore (sostituisce 027)
-- questionnaire_answers = risposte puntuali (answer_value JSON-ready)
--
-- Retrocompat: i token sq_ gia inviati (027) restano validi grazie alla
-- colonna access_token_hash (stesso SHA-256 di supplier_questionnaires.token_hash),
-- migrata da mig 035 (migrazione dati). 027 NON viene droppata: resta read-only.
--
-- IMPORTANTE (CLAUDE.md / memoria): MySQL 8 Ubuntu NON supporta
-- "ADD COLUMN IF NOT EXISTS" / "CREATE INDEX IF NOT EXISTS". Le ALTER usano
-- procedure idempotenti su information_schema. CREATE TABLE IF NOT EXISTS ok.
-- DELIMITER non funziona via pipe -> usare "source".
--
-- Eseguire con:
-- mysql -h localhost nis2_agile_db -e "source docs/sql/032_supplier_questionnaires_module.sql"
--
-- Ordine di applicazione: 032 -> 033 -> 034 -> 035 (vedi nota in 035).
-- ============================================================================
-- 1. Categorie fornitori (preset di sistema = organization_id 0)
CREATE TABLE IF NOT EXISTS supplier_categories (
id INT NOT NULL AUTO_INCREMENT,
organization_id INT NOT NULL DEFAULT 0 COMMENT '0 = preset di sistema (visibile a tutte le org)',
slug VARCHAR(64) NOT NULL COMMENT 'Identificativo stabile, es. cloud_provider',
name VARCHAR(255) NOT NULL,
description TEXT NULL,
is_system TINYINT(1) NOT NULL DEFAULT 0 COMMENT '1 per i preset di sistema',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uq_supcat_org_slug (organization_id, slug),
KEY idx_supcat_org (organization_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='Tassonomia fornitori: preset di sistema (org 0) + categorie per-org';
-- 2. Template questionario (per-org, opz. legato a una categoria)
CREATE TABLE IF NOT EXISTS questionnaire_templates (
id INT NOT NULL AUTO_INCREMENT,
organization_id INT NOT NULL,
category_id INT NULL COMMENT 'FK supplier_categories: template specifico per categoria',
name VARCHAR(255) NOT NULL,
description TEXT NULL,
current_version VARCHAR(20) NOT NULL DEFAULT '1.0' COMMENT 'Versione live (vedi questionnaire_template_versions)',
status ENUM('draft','active','archived') NOT NULL DEFAULT 'draft',
pass_threshold TINYINT UNSIGNED NULL COMMENT 'Soglia score 0-100 per esito positivo',
is_default TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Template predefinito per la org/categoria',
created_by INT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_qt_org (organization_id),
KEY idx_qt_category (category_id),
KEY idx_qt_org_status (organization_id, status),
CONSTRAINT fk_qt_org FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
CONSTRAINT fk_qt_category FOREIGN KEY (category_id) REFERENCES supplier_categories(id) ON DELETE SET NULL,
CONSTRAINT fk_qt_creator FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='Modelli di questionario di sicurezza fornitori (per organizzazione)';
-- 3. Versioni template (pattern policy_versions, snapshot domande+weight)
CREATE TABLE IF NOT EXISTS questionnaire_template_versions (
id INT NOT NULL AUTO_INCREMENT,
template_id INT NOT NULL,
organization_id INT NOT NULL COMMENT 'Denormalizzato per filtri/authz',
version VARCHAR(20) NOT NULL,
questions_snapshot JSON NOT NULL COMMENT 'Snapshot completo domande incl. weight, opzioni, tipo',
change_note VARCHAR(255) NULL,
created_by INT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uq_qtv_template_version (template_id, version),
KEY idx_qtv_template (template_id),
KEY idx_qtv_org (organization_id),
CONSTRAINT fk_qtv_template FOREIGN KEY (template_id) REFERENCES questionnaire_templates(id) ON DELETE CASCADE,
CONSTRAINT fk_qtv_org FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
CONSTRAINT fk_qtv_creator FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='Storico versionato dei template questionario (snapshot domande+weight)';
-- 4. Domande "live" del template (ordinabili, pesate, multi-tipo)
CREATE TABLE IF NOT EXISTS questionnaire_questions (
id INT NOT NULL AUTO_INCREMENT,
template_id INT NOT NULL,
organization_id INT NOT NULL COMMENT 'Denormalizzato per filtri/authz',
question_code VARCHAR(50) NULL COMMENT 'Codice stabile opzionale, es. gvsc07_a_access_level',
question_text TEXT NOT NULL,
question_type ENUM('yes_no_partial','single_choice','multi_choice','scale_1_5','text','number','file') NOT NULL DEFAULT 'yes_no_partial',
options JSON NULL COMMENT 'Opzioni per single_choice/multi_choice/scale',
weight DECIMAL(6,2) NOT NULL DEFAULT 1.00 COMMENT 'Peso nello scoring',
is_required TINYINT(1) NOT NULL DEFAULT 1,
order_index INT NOT NULL DEFAULT 0 COMMENT 'Ordinamento di presentazione',
nis2_ref VARCHAR(40) NULL COMMENT 'Riferimento normativo es. GV.SC-07.a',
vuln_flag VARCHAR(64) NULL COMMENT 'Flag vulnerabilita acceso da risposta negativa',
high_criticality_only TINYINT(1) NOT NULL DEFAULT 0 COMMENT '1 = mostra solo a fornitori high/critical',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_qq_template_order (template_id, order_index),
KEY idx_qq_org (organization_id),
CONSTRAINT fk_qq_template FOREIGN KEY (template_id) REFERENCES questionnaire_templates(id) ON DELETE CASCADE,
CONSTRAINT fk_qq_org FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='Domande del template questionario fornitori (ordinabili e pesate)';
-- 5. Campagne (SOSTITUISCE supplier_questionnaires di mig 027)
CREATE TABLE IF NOT EXISTS questionnaire_campaigns (
id INT NOT NULL AUTO_INCREMENT,
organization_id INT NOT NULL,
supplier_id INT NOT NULL,
template_id INT NULL COMMENT 'Template usato (NULL se importata da 027 legacy)',
template_version VARCHAR(20) NULL COMMENT 'Versione template congelata all''invio',
access_token_hash CHAR(64) NOT NULL COMMENT 'SHA-256 del token sq_ inviato (retrocompat 027)',
status ENUM('draft','sent','in_progress','completed','expired','cancelled') NOT NULL DEFAULT 'sent',
score INT NULL COMMENT 'Punteggio 0-100 calcolato dalle risposte',
risk_level ENUM('low','medium','high','critical') NULL,
answers JSON NULL COMMENT 'Cache aggregata risposte (la verita e in questionnaire_answers)',
show_score_to_supplier TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'F23: default OFF',
language ENUM('it','en','auto') NOT NULL DEFAULT 'it' COMMENT 'Lingua portale/email',
sent_to_email VARCHAR(255) NULL,
sent_at DATETIME NULL,
due_at DATETIME NULL COMMENT 'Scadenza compilazione',
completed_at DATETIME NULL,
expires_at DATETIME NULL,
reminder_count INT NOT NULL DEFAULT 0,
last_reminder_at DATETIME NULL,
next_reminder_at DATETIME NULL COMMENT 'Prossimo invio reminder (cron)',
reminder_offsets JSON NULL COMMENT 'Offset reminder, es. [-14,-7,-1]',
is_recurring TINYINT(1) NOT NULL DEFAULT 0,
recurrence_months TINYINT UNSIGNED NULL COMMENT 'Periodicita in mesi, se ricorrente',
next_recurrence_at DATETIME NULL COMMENT 'Prossima riemissione (cron)',
created_by INT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uq_qc_token (access_token_hash),
KEY idx_qc_org (organization_id),
KEY idx_qc_supplier (supplier_id),
KEY idx_qc_template (template_id),
KEY idx_qc_status_reminder (status, next_reminder_at),
KEY idx_qc_status_due (status, due_at),
KEY idx_qc_status_recurrence (status, next_recurrence_at),
CONSTRAINT fk_qc_org FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
CONSTRAINT fk_qc_supplier FOREIGN KEY (supplier_id) REFERENCES suppliers(id) ON DELETE CASCADE,
CONSTRAINT fk_qc_template FOREIGN KEY (template_id) REFERENCES questionnaire_templates(id) ON DELETE SET NULL,
CONSTRAINT fk_qc_creator FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='Campagne questionario inviate ai fornitori (sostituisce supplier_questionnaires/027)';
-- 6. Risposte puntuali (answer_value JSON-ready per multi_choice/file)
CREATE TABLE IF NOT EXISTS questionnaire_answers (
id INT NOT NULL AUTO_INCREMENT,
campaign_id INT NOT NULL,
organization_id INT NOT NULL COMMENT 'Denormalizzato NOT NULL per filtri/authz',
question_id INT NULL COMMENT 'FK domanda live (NULL se domanda legacy/snapshot)',
question_code VARCHAR(50) NULL COMMENT 'Codice domanda dallo snapshot, per storicizzazione',
answer_value JSON NULL COMMENT 'Valore risposta JSON-ready (scalare/array per multi_choice/file)',
answer_text TEXT NULL COMMENT 'Testo libero',
file_ref VARCHAR(255) NULL COMMENT 'Riferimento allegato',
score_awarded DECIMAL(6,2) NULL COMMENT 'Punteggio attribuito alla risposta',
answered_by INT NULL COMMENT 'supplier_user_id che ha risposto (multi-referente)',
answered_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uq_qa_campaign_question (campaign_id, question_id),
KEY idx_qa_org (organization_id),
KEY idx_qa_question (question_id),
CONSTRAINT fk_qa_campaign FOREIGN KEY (campaign_id) REFERENCES questionnaire_campaigns(id) ON DELETE CASCADE,
CONSTRAINT fk_qa_org FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
CONSTRAINT fk_qa_question FOREIGN KEY (question_id) REFERENCES questionnaire_questions(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='Risposte puntuali alle campagne questionario (answer_value JSON-ready, append-only)';
-- 7. Seed idempotente preset di sistema (organization_id = 0) - 10 categorie
INSERT INTO supplier_categories (organization_id, slug, name, description, is_system)
VALUES
(0, 'cloud_provider', 'Cloud Provider (IaaS/PaaS/SaaS)', 'Fornitori di servizi cloud e hosting', 1),
(0, 'managed_security', 'Managed Security / MSSP', 'SOC, MDR, gestione sicurezza in outsourcing', 1),
(0, 'connectivity_telco', 'Connettivita / Telco', 'Operatori di rete, ISP, connettivita', 1),
(0, 'hardware_supplier', 'Fornitore Hardware', 'Apparati di rete, server, dispositivi', 1),
(0, 'software_vendor', 'Software Vendor / SaaS applicativo', 'Vendor di software applicativo e licenze', 1),
(0, 'professional_services', 'Servizi Professionali / Consulenza', 'Consulenza IT, legale, audit, compliance', 1),
(0, 'logistics', 'Logistica e Trasporti', 'Fornitori di logistica, magazzino, spedizioni', 1),
(0, 'facility_management', 'Facility Management', 'Gestione immobili, energia, impianti, sicurezza fisica',1),
(0, 'datacenter_colocation', 'Data center / Colocation', 'Data center, housing, colocation, infrastruttura fisica',1),
(0, 'tic_software_dev', 'TIC / Software Development', 'Sviluppo software su commessa, system integration, ICT', 1)
ON DUPLICATE KEY UPDATE
name = VALUES(name),
description = VALUES(description),
is_system = VALUES(is_system),
updated_at = CURRENT_TIMESTAMP;
-- ROLLBACK (manuale, ordine inverso per le FK):
-- DROP TABLE IF EXISTS questionnaire_answers;
-- DROP TABLE IF EXISTS questionnaire_campaigns;
-- DROP TABLE IF EXISTS questionnaire_questions;
-- DROP TABLE IF EXISTS questionnaire_template_versions;
-- DROP TABLE IF EXISTS questionnaire_templates;
-- DELETE FROM supplier_categories WHERE organization_id = 0 AND is_system = 1;
-- DROP TABLE IF EXISTS supplier_categories;

View File

@ -0,0 +1,79 @@
-- ============================================================================
-- Migration 033 - suppliers: categoria, provenienza import, soft-delete (guard)
-- ----------------------------------------------------------------------------
-- PROPOSTA DI DESIGN (NON ancora applicata). Da eseguire su host MySQL.
--
-- Estende suppliers per il nuovo modulo questionari/import:
-- - category_id : FK supplier_categories (classificazione fornitore)
-- - external_ref : ID nel sistema sorgente (dedup import API/CSV)
-- - source : ENUM('manual','csv','api') provenienza del record
-- - deleted_at : soft-delete -- GIA PRESENTE da mig 006; aggiunto solo se assente
-- + UNIQUE(organization_id, external_ref) per upsert idempotente.
--
-- NOTA REALE: suppliers.deleted_at e stato aggiunto dalla migration 006.
-- Questo script lo riaggiunge SOLO se mancante. Nessuna duplicazione.
--
-- IMPORTANTE: MySQL 8 Ubuntu NON supporta "ADD COLUMN IF NOT EXISTS".
-- Procedura idempotente su information_schema. DELIMITER -> usare "source".
-- mysql -h localhost nis2_agile_db -e "source docs/sql/033_suppliers_category_source.sql"
-- ============================================================================
DELIMITER //
DROP PROCEDURE IF EXISTS _mig033 //
CREATE PROCEDURE _mig033()
BEGIN
IF NOT EXISTS (SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='suppliers' AND COLUMN_NAME='category_id') THEN
ALTER TABLE suppliers ADD COLUMN category_id INT NULL
COMMENT 'FK supplier_categories: classificazione fornitore' AFTER service_type;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='suppliers' AND COLUMN_NAME='external_ref') THEN
ALTER TABLE suppliers ADD COLUMN external_ref VARCHAR(190) NULL
COMMENT 'ID fornitore nel sistema sorgente (dedup import API/CSV)' AFTER vat_number;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='suppliers' AND COLUMN_NAME='source') THEN
ALTER TABLE suppliers ADD COLUMN source ENUM('manual','csv','api') NOT NULL DEFAULT 'manual'
COMMENT 'Provenienza del record fornitore' AFTER external_ref;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='suppliers' AND COLUMN_NAME='deleted_at') THEN
ALTER TABLE suppliers ADD COLUMN deleted_at TIMESTAMP NULL DEFAULT NULL AFTER updated_at;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.TABLE_CONSTRAINTS
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='suppliers'
AND CONSTRAINT_NAME='fk_supplier_category' AND CONSTRAINT_TYPE='FOREIGN KEY') THEN
ALTER TABLE suppliers ADD CONSTRAINT fk_supplier_category
FOREIGN KEY (category_id) REFERENCES supplier_categories(id) ON DELETE SET NULL;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='suppliers'
AND INDEX_NAME='uq_supplier_external_ref') THEN
ALTER TABLE suppliers ADD UNIQUE KEY uq_supplier_external_ref (organization_id, external_ref);
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='suppliers'
AND INDEX_NAME='idx_suppliers_not_deleted') THEN
ALTER TABLE suppliers ADD INDEX idx_suppliers_not_deleted (organization_id, deleted_at);
END IF;
END //
DELIMITER ;
CALL _mig033();
DROP PROCEDURE IF EXISTS _mig033;
-- ROLLBACK (manuale):
-- ALTER TABLE suppliers
-- DROP FOREIGN KEY fk_supplier_category,
-- DROP INDEX uq_supplier_external_ref,
-- DROP INDEX idx_suppliers_not_deleted,
-- DROP COLUMN category_id,
-- DROP COLUMN external_ref,
-- DROP COLUMN source;
-- -- NB: NON droppare deleted_at (di proprieta della mig 006).

View File

@ -0,0 +1,85 @@
-- ============================================================================
-- Migration 034 - Portale fornitore: utenti, OTP/magic-link, sessioni revocabili
-- ----------------------------------------------------------------------------
-- PROPOSTA DI DESIGN (NON ancora applicata). Da eseguire su host MySQL.
--
-- Login passwordless del fornitore per compilare le campagne questionario:
-- supplier_users = referenti del fornitore abilitati al portale
-- supplier_otp = codici OTP + magic token (hash), tentativi, scadenza
-- supplier_sessions = sessioni con jti, revocabili (revoked_at)
--
-- Tutto NO-PASSWORD: l'accesso avviene via OTP/magic-link consegnato all'email.
-- Sicurezza: lockout persistente su supplier_otp.attempts; sessione revocabile.
--
-- CREATE TABLE IF NOT EXISTS (idempotente). DELIMITER -> usare "source".
-- mysql -h localhost nis2_agile_db -e "source docs/sql/034_supplier_portal_auth.sql"
-- ============================================================================
-- 1. Utenti del fornitore (referenti abilitati al portale)
CREATE TABLE IF NOT EXISTS supplier_users (
id INT NOT NULL AUTO_INCREMENT,
supplier_id INT NOT NULL,
organization_id INT NOT NULL COMMENT 'Denormalizzato: org committente proprietaria del fornitore',
email VARCHAR(255) NOT NULL,
full_name VARCHAR(255) NULL,
role ENUM('contact','compliance','signer') NOT NULL DEFAULT 'contact',
is_active TINYINT(1) NOT NULL DEFAULT 1,
last_login_at DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uq_supuser_supplier_email (supplier_id, email),
KEY idx_supuser_org (organization_id),
KEY idx_supuser_email (email),
CONSTRAINT fk_supuser_supplier FOREIGN KEY (supplier_id) REFERENCES suppliers(id) ON DELETE CASCADE,
CONSTRAINT fk_supuser_org FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='Referenti del fornitore abilitati al portale (login passwordless)';
-- 2. OTP + magic-link (hash, tentativi, scadenza)
CREATE TABLE IF NOT EXISTS supplier_otp (
id INT NOT NULL AUTO_INCREMENT,
supplier_user_id INT NOT NULL,
otp_hash CHAR(64) NULL COMMENT 'SHA-256 del codice OTP numerico',
magic_token_hash CHAR(64) NULL COMMENT 'SHA-256 del magic-link token',
purpose ENUM('login','questionnaire') NOT NULL DEFAULT 'login',
attempts INT NOT NULL DEFAULT 0 COMMENT 'Tentativi di verifica falliti (lockout persistente)',
max_attempts INT NOT NULL DEFAULT 5,
locked_until DATETIME NULL COMMENT 'Lockout temporaneo dopo max_attempts',
consumed_at DATETIME NULL COMMENT 'Quando usato (single-use)',
expires_at DATETIME NOT NULL,
ip_created VARCHAR(45) NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_sotp_user (supplier_user_id),
KEY idx_sotp_expires (expires_at),
KEY idx_sotp_magic (magic_token_hash),
CONSTRAINT fk_sotp_user FOREIGN KEY (supplier_user_id) REFERENCES supplier_users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='OTP e magic-link per autenticazione passwordless del fornitore';
-- 3. Sessioni revocabili (jti, scadenza, revoca)
CREATE TABLE IF NOT EXISTS supplier_sessions (
id INT NOT NULL AUTO_INCREMENT,
supplier_user_id INT NOT NULL,
organization_id INT NOT NULL COMMENT 'Denormalizzato per filtri/audit',
jti CHAR(36) NOT NULL COMMENT 'JWT ID (UUID) della sessione',
ip_address VARCHAR(45) NULL,
user_agent VARCHAR(500) NULL,
expires_at DATETIME NOT NULL,
revoked_at DATETIME NULL COMMENT 'NOT NULL => sessione revocata',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uq_ssess_jti (jti),
KEY idx_ssess_user (supplier_user_id),
KEY idx_ssess_org (organization_id),
KEY idx_ssess_expires (expires_at),
CONSTRAINT fk_ssess_user FOREIGN KEY (supplier_user_id) REFERENCES supplier_users(id) ON DELETE CASCADE,
CONSTRAINT fk_ssess_org FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='Sessioni portale fornitore, revocabili via revoked_at';
-- ROLLBACK (manuale, ordine inverso per le FK):
-- DROP TABLE IF EXISTS supplier_sessions;
-- DROP TABLE IF EXISTS supplier_otp;
-- DROP TABLE IF EXISTS supplier_users;

View File

@ -0,0 +1,69 @@
-- ============================================================================
-- Migration 035 - Migrazione dati supplier_questionnaires (027) -> campaigns
-- ----------------------------------------------------------------------------
-- PROPOSTA DI DESIGN (NON ancora applicata). Da eseguire PER ULTIMA su host MySQL.
--
-- Travasa le righe di supplier_questionnaires (mig 027) in
-- questionnaire_campaigns (mig 032), preservando i token sq_ gia inviati:
-- token_hash (027) -> access_token_hash (032) [stesso SHA-256]
-- status (027: sent|completed|expired) -> status (032 ENUM esteso)
-- answers/score/risk_level/email/date -> mappatura 1:1
--
-- Idempotente: INSERT ... SELECT con anti-join su access_token_hash (UNIQUE),
-- quindi rilanciabile senza duplicare. template_id resta NULL (legacy).
-- 027 NON viene droppata: resta read-only/deprecata per audit storico.
--
-- DIPENDE da: 032 (questionnaire_campaigns deve esistere) e 027.
-- mysql -h localhost nis2_agile_db -e "source docs/sql/035_migrate_027_to_campaigns.sql"
--
-- Ordine globale: 032 -> 033 -> 034 -> 035.
-- ============================================================================
DELIMITER //
DROP PROCEDURE IF EXISTS _mig035 //
CREATE PROCEDURE _mig035()
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.TABLES
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='supplier_questionnaires')
AND EXISTS (SELECT 1 FROM information_schema.TABLES
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='questionnaire_campaigns')
THEN
INSERT INTO questionnaire_campaigns
(organization_id, supplier_id, template_id, template_version,
access_token_hash, status, score, risk_level, answers,
sent_to_email, sent_at, completed_at, expires_at, created_at)
SELECT
sq.organization_id,
sq.supplier_id,
NULL AS template_id,
NULL AS template_version,
sq.token_hash AS access_token_hash,
CASE sq.status
WHEN 'sent' THEN 'sent'
WHEN 'completed' THEN 'completed'
WHEN 'expired' THEN 'expired'
ELSE 'sent'
END AS status,
sq.score,
sq.risk_level,
sq.answers,
sq.sent_to_email,
sq.sent_at,
sq.completed_at,
sq.expires_at,
sq.created_at
FROM supplier_questionnaires sq
LEFT JOIN questionnaire_campaigns qc
ON qc.access_token_hash = sq.token_hash
WHERE qc.id IS NULL; -- anti-join: salta quelle gia migrate (idempotenza)
END IF;
END //
DELIMITER ;
CALL _mig035();
DROP PROCEDURE IF EXISTS _mig035;
-- ROLLBACK (manuale): cancella SOLO le campagne legacy importate (template_id NULL,
-- token corrispondente in supplier_questionnaires). NON tocca le campagne native.
-- DELETE qc FROM questionnaire_campaigns qc
-- JOIN supplier_questionnaires sq ON sq.token_hash = qc.access_token_hash
-- WHERE qc.template_id IS NULL;

View File

@ -0,0 +1,91 @@
# Pilastro AI "vero consulente NIS2" — corpus normativo incluso/aggiornato + persona consulente
> Deliverable agente design 2026-05-31 (richiesta utente: "come TRPG, contesto normativo incluso e aggiornato + AI istruita, un vero consulente"). Sola analisi; nessun codice applicato.
> Riferimento incrociato: [[DESIGN_MODULO_QUESTIONARI_FORNITORI]] §13.
## 0. Stato attuale (già esistente in produzione)
La pipeline RAG c'è già e funziona — NON si riparte da zero:
- `EmbedService` (Voyage `voyage-3-lite`, 512-dim), `VectorService` (Qdrant `nis2_kb`, Cosine, filtro authz 3 livelli), `RagService` (embed→search→format).
- `AIService::askWithRag()` (top-5, minScore 0.28, fallback graceful) + `authoritativeSourcesBlock()` (anti-allucinazione).
- `AiController``POST /api/ai/ask` (rate-limited, audit). Persona **ARIA_SUPPORT_NIS2** (id=4, operativa).
- Registry fonti `application/config/nis2_sources.php` (7 fonti). Ingest `scripts/ingest-nis2-sources.php`**eseguito 2026-05-29: 287 chunk** (NIS2 171 + CER 77 + Det.333017 25 + Det.164179 9 + Ambiti 5).
- Feed `normative_updates` + ACK (mig.009) — ma **scollegato dalla KB**.
- Catalogo requisiti ACN granulari: mig.031 + `docs/nis2/allegati_acn/acn_requirements.json` (87 imp + 116 ess) — in DB/JSON ma **non in KB**.
## 1. Gap del corpus (da colmare — priorità)
1. **D.Lgs. 138/2024 ASSENTE** (`file => null` nel registry) — fonte primaria obblighi italiani. **Priorità massima.**
2. **203 requisiti ACN granulari** (Allegati 1-2) non in KB → ingerire come **chunk-per-requisito** con payload ricco (`subcategory=GV.SC-04`, `function`, `entity=essenziale/importante`, `source`). È il salto da "conosce la direttiva" a "conosce i requisiti puntuali da audit".
3. Assenti dal registro/KB: **Reg. UE 2024/2690**, **ENISA Good Practices Supply Chain**, **NIST SP 800-161r1 / CSF 2.0**, **DORA (Reg. UE 2022/2554)**.
4. Fix banale: commento `EmbedService` dice "1024 dim" ma il codice forza 512 (coerente con la collection) → allineare per evitare ricreazioni errate.
> Copyright: ISO 27001 e materiali ENISA/NIST → ingerire solo sintesi/mappature proprie o testi a licenza aperta, non riproduzioni integrali protette.
## 2. Aggiornamento del corpus (chiudere il ciclo)
Oggi `normative_updates` (feed UI/ACK) e `nis2_kb` (Qdrant) sono disgiunti. Design:
- **Versioning fonte** in `nis2_sources.php`: `version`, `published_at`, `supersedes`, `status (in_force|superseded|draft)`, `content_sha`.
- **Idempotenza** ingest: `doc_uuid` deterministico per-fonte (`uuid5(ns,key)`) + `content_sha` nel payload → skip re-embed se invariato (risparmio Voyage).
- **Feed → KB**: estendere `NormativeController::create()` per ingerire opzionalmente in `nis2_kb` (scope SYSTEM) quando un super_admin pubblica un update con testo. Aggiungere `normative_updates.kb_indexed`, `kb_doc_uuid`. Così l'ACK audit e la conoscenza AI nascono dallo stesso atto.
- **Cron** settimanale `ingest-nis2-sources.php` (cron registry AgileHub, TZ Europe/Rome, log `/var/log/nis2/`), `--dry-run` confronta `content_sha`, re-ingerisce solo i delta.
- **Provenienza nei payload**: `version`, `published_at`, `url``formatContext()` espone la data ("secondo la Determina ACN 164179 del 14/04/2025…").
- **Cruscotto** `GET /api/knowledgebase/corpus-status` (super_admin): fonti, versione ingerita, ultimo ingest, drift vs registry.
> Caveat: `kb_uploaded_documents` (mig.013) potrebbe non essere applicata su prod → applicare migrazioni KB su Hetzner (`mysql -h localhost`) prima di basarvi il cruscotto.
## 3. Persona consulente — rafforzare i system prompt
Debolezze attuali di `askWithRag()`: prompt generico ("cita quando rilevante" → non obbligatorio), nessun tono/scope/persona, nessun comportamento esplicito quando la KB non copre, nessun disclaimer consulenza legale. Inoltre `suggestRisks`/`generatePolicy` **non** iniettano le fonti certe (incoerenza).
### Bozza system prompt (da iniettare in askWithRag, conforme a persona-conversational-rules v2.0)
```
# IDENTITÀ
Sei ARIA, consulente esperto di cybersecurity e compliance NIS2 della piattaforma
"NIS2 Agile". Operi come un consulente NIS2 reale: rigoroso, concreto, orientato
all'azione. Rispondi sempre in italiano professionale. Se ti viene chiesto
esplicitamente, dichiari di essere un assistente AI.
# AMBITO
Direttiva (UE) 2022/2555 (NIS2), (UE) 2022/2557 (CER), D.Lgs. 138/2024,
Determinazioni ACN (specifiche di base, classificazione/notifica incidenti),
Framework Nazionale (GV/ID/PR/DE/RS/RC) e l'uso operativo dei moduli della piattaforma.
# REGOLE DI CITAZIONE (vincolanti)
1. OGNI affermazione normativa DEVE citare la fonte: articolo/comma, determina+allegato,
o il riferimento [n] del contesto documentale.
2. Col contesto presente, cita i riferimenti [1],[2]… dei documenti forniti.
3. NON inventare numeri di articolo, determine, allegati, date o requisiti. Se incerto,
dichiaralo e rimanda alla fonte ufficiale.
4. Preferisci D.Lgs.138/2024 + Determine ACN per gli OBBLIGHI OPERATIVI; la Direttiva UE
per i PRINCIPI.
# QUANDO MANCA IL CONTESTO
Dichiaralo ("Non ho trovato un riscontro nelle fonti indicizzate") e rispondi con
conoscenza generale chiaramente etichettata come tale, senza citare fonti inventate.
# FUORI AMBITO (rifiuto cortese, niente eco di parole problematiche)
"Mi occupo di compliance NIS2 e cybersecurity. Per [tema] ti consiglio di rivolgerti
alla funzione competente."
# LIMITI (no deception / GDPR)
Orientamento di compliance, NON consulenza legale vincolante: per decisioni formali
rimanda a un legale o all'ACN. Non includere mai dati identificativi dell'organizzazione.
# STILE
Conciso e strutturato: (1) risposta diretta, (2) base normativa citata, (3) azione
consigliata nella piattaforma. Niente preamboli.
```
Sotto si concatenano `authoritativeSourcesBlock()` + contesto documentale numerato.
### Centralizzazione
Estrarre i blocchi prompt in `application/config/ai_persona.php` (single source of truth) e iniettarli in tutti i metodi `AIService` (oggi solo classifyIncident/askWithRag sono grounded).
## 4. Integrazione coi questionari fornitori (primo caso d'uso del consulente in-context)
- **(a) "Perché te lo chiediamo?"** (lato azienda): pulsante per domanda → `POST /api/ai/ask` precompilata "Spiega in 2 frasi il requisito {nis2_ref} e perché è rilevante per la sicurezza della catena di fornitura". Grounded sul chunk-per-requisito.
- **(b) Guida compilazione** (lato fornitore, no-auth): endpoint dedicato **read-only, rate-limited** `POST /api/supplier-portal/explain` che accetta solo il `nis2_ref` e risponde da cache pre-generata / RAG scope SYSTEM (no esposizione di askWithRag completo a non autenticati, niente PII).
- **(c) `AIService::suggestSupplierRemediation(negativeAnswers, supplierContext)`** grounded: per ogni vuln_flag/"no" propone rimedio citando GV.SC/PR + Art.21.2(d) → alimenta piano trattamento fornitore o NCR/CAPA. Anonimizzare (no nome/P.IVA nel prompt).
## 5. Piano incrementale
- **Fase 0 (igiene, ~0.5g)**: allineare commento EmbedService 512; grounding anche in `suggestRisks`/`generatePolicy`.
- **Fase 1 (corpus, alto valore)**: procurare D.Lgs.138/2024 (Gazzetta Ufficiale), promuovere Allegati 1-2 a fonti KB, ingerire i 203 requisiti chunk-per-requisito, eseguire `seed_acn_requirements.php`. Su Hetzner (Qdrant+Voyage), non in devenv.
- **Fase 2 (persona, ~basso rischio)**: `ai_persona.php` + nuovo prompt askWithRag; aggiornare help.js + KB ARIA; smoke 10-15 domande note + 3 fuori scope + 1 senza copertura.
- **Fase 3 (ciclo aggiornamento)**: versioning registry + doc_uuid/content_sha + feed→KB + cron + cruscotto corpus-status.
- **Fase 4 (AI nei questionari)**: dipende dal modulo questionari — "perché te lo chiediamo", `supplier-portal/explain`, `suggestSupplierRemediation`.
## 6. Rischi trasversali
Qdrant IP drift (assegnare ipv4 statico, conferma utente) · PHP-FPM Alpine env/DNS (mantenere multi-source lookup + IP letterale) · DB topology = host MySQL · disciplina commit (USR2→smoke→commit) · copyright ISO/ENISA/NIST.

View File

@ -0,0 +1,68 @@
# Mini-spec UX — Portale fornitore + Editor lato azienda (§6)
> Deliverable agente design 2026-05-31. Chiude i finding UX F1F25 di [[DESIGN_MODULO_QUESTIONARI_FORNITORI]] §12. Copy + comportamento + acceptance criteria.
> Utente di riferimento del portale: referente PMI fornitrice, NON tecnico, da smartphone, 1 volta ogni 6-12 mesi, senza supporto IT. Ogni scelta privilegia chiarezza, target ≥44px, recupero da errore sempre possibile.
> Convenzioni riusate (verificate): `--primary:#06B6D4`; progress bar di `assessment.html` (`updateProgressBar`, id `progress-pct/-bar-fill/-answered/-total`, i18n `assessment.progress`); `debounce`, `showNotification`, `_loadFirmBranding()` (GET `/api/branding/current`), `initIdleTimeout()` overlay countdown — in `common.js`; badge `badge-success|warning|danger|neutral`; `sendSupplierQuestionnaire`+prompt rigenera-link in `supply-chain.html`; baseline + footer GDPR + `noindex,nofollow` in `supplier-assessment.html`.
> Endpoint (forma router §12): `POST /api/supplier-portal/request-otp`, `verify-otp`, `GET /me`, `GET /{id}`, `POST /{id}/submit`, `PATCH /{id}/answers`.
> i18n: namespace `sp.*` (+ `sp.editor.*`) in `i18n.js`; ogni stringa IT ha controparte EN.
## 1. Portale fornitore — supplier-portal.html
Principi: una colonna max 560px, branding del **committente** in header (NIS2 Agile solo footer), zero gergo ("codice"/"link"/"questionario", mai "token/OTP/JWT/campaign"), selettore lingua IT/EN in alto a destra, `noindex,nofollow`, nessun link all'app interna.
Stati:
1. **Loading**: spinner; se `GET /me`/branding >8s → stato errore rete.
2. **Richiesta accesso**: input email (`type=email inputmode=email autocomplete=email autofocus`, label visibile), CTA "Invia link di accesso", micro-copy GDPR con link informativa. **Risposta sempre opaca** (anti-enumerazione) → va sempre allo stato "email inviata".
3. **Email inviata**: "Controlla la tua email" + avviso spam + mittente; campo OTP fallback; **reinvio con countdown 60s** (raddoppia dal 3° invio); via d'uscita "Usa un'altra email".
4. **Verifica OTP — errori specifici** (F2/F3): messaggi umani con via d'uscita per: codice errato ("…hai ancora {n} tentativi", contatore reale da `supplier_otp.attempts`), scaduto ("→ Invia un nuovo codice"), lockout ("bloccato per {minuti} min", persistente lato DB), link già usato ("→ Invia un nuovo link"), errore rete ("Riprova"). Mai codici HTTP/"token".
5. **Gerarchia magic-link/OTP** (F1/F4): nel portale e nell'email il **pulsante magic-link è primario**, OTP fallback. **No doppio-consumo**: l'apertura del link (prefetch mail mobile) NON consuma il token; consumo solo al click esplicito "Entra nel portale". AC: aprire 2× e poi cliccare deve funzionare.
6. **Dashboard**: se **1 solo questionario aperto** → vai diretto alla compilazione. Se multipli → card con badge stato + scadenza in chiaro + CTA "Compila/Riprendi/Vedi ricevuta"; ordinamento da-compilare prima per scadenza.
7. **Compilazione**: header con badge scadenza sticky + progress bar + domande single-page (vedi §4).
8. **Vuoto**: "Nessun questionario in attesa" + suggerimento contattare committente.
9. **Completato/Ricevuta**: "Questionario inviato. Grazie!" + "Scarica PDF" + (Caso B) "Torna ai questionari". **Score mostrato solo se `show_score_to_supplier=true`** (default OFF, F23) → rimuovere/condizionare lo score sempre-mostrato dell'attuale supplier-assessment.html.
10. **Errore rete**: non distruttivo, risposte salvate localmente, "Riprova".
11. **Sessione in scadenza (4h)**: overlay countdown (riuso `initIdleTimeout`), "Continua" estende; alla scadenza "Le tue risposte sono salvate. Richiedi un nuovo link per riprendere". Ripresa dall'ultima domanda dopo nuovo OTP.
## 2. Autosave (F11 — priorità assoluta)
- Stato `in_progress` alla prima risposta. Salvataggio per-domanda via `PATCH /{id}/answers` con `debounce(fn,800)`.
- **Doppio livello**: localStorage immediato (resilienza offline) + PATCH server (cross-device). Server = source of truth; rinvia PATCH falliti.
- Indicatore discreto: "Salvataggio…" / "Bozza salvata {ora}" / "Salvataggio in sospeso — riproveremo".
- **Ripresa**: banner "Bentornato. Hai risposto a {x} di {y} domande" + "Riprendi" → scroll alla prima domanda senza risposta.
- **"Salva bozza"** (ghost, ridondante ma rassicurante) vs **"Invia definitivo"** (CTA primaria, conferma in modale "dopo l'invio non potrai modificare", disabilitata finché le obbligatorie non sono complete; se mancano → scroll alla prima + "{k} risposte obbligatorie mancanti").
- AC: chiudere/riaprire (anche altro device dopo nuovo OTP) → risposte presenti, cursore sull'ultima non risposta; submit blocca PATCH successivi (409 "già inviato").
## 3. Email invito/OTP (F6)
Inviata via template `/api/emails/send` (NON `send-raw` → OTP fuori da `email_log`). Branding committente, **una sola CTA**.
Contenuto minimo: (1) oggetto con nome committente; (2) motivo 1 frase ("Come fornitore di {Committente}…richiesto dalla normativa NIS2"); (3) tempo stimato (~{n} min); (4) scadenza in chiaro (solo se `due_at`); (5) CTA singola magic-link "Compila il questionario"; (6) OTP fallback "Oppure inserisci questo codice: 123 456 (15 min)"; (7) validità "15 minuti e una sola volta"; (8) footer GDPR Art.13/14 (titolare=committente, NIS2 Agile=responsabile Art.28, link informativa, "se non eri tu, ignora").
AC: OTP mai nei log; 1 sola CTA; nome committente in oggetto+corpo; scadenza solo se valorizzata.
## 4. Compilazione — ergonomia per tipo
Chrome: progress bar (riuso `updateProgressBar`) + "{answered} di {total}"; badge scadenza sticky (neutral >7gg / warning ≤7gg / danger scaduto).
Per tipo:
- `yes_no_partial`: 3 bottoni grandi (radio nativi sotto) "Sì/Parzialmente/No".
- `scale_1_5`: 5 bottoni con **etichette agli estremi sempre visibili** (da options min/max). Mai scala nuda.
- `single_choice`: radio nativi verticali. `multi_choice`: checkbox + hint "Puoi selezionare più opzioni".
- `text`: textarea + esempio placeholder. `number`: `inputmode=numeric`.
- `file`: formati/dimensioni dichiarati prima del tap; `accept` + `capture` (foto documenti); feedback upload (%/check/rimuovi); errori formato/dimensione con suggerimento; async, sopravvive ad autosave (`file_ref`).
- `help_text`: **sempre visibile** sotto la domanda (mai tooltip/hover — non funziona su touch).
- **"Non applicabile" / "Non so"** dove sensato: conta come risposta per la progress bar, distinto dal "No" nello scoring.
## 5. Ricevuta, multi-referente, score
- **Ricevuta (F22)**: email conferma al referente + **PDF risposte** scaricabile (da email e da §1.9). PDF = intestazione committente+fornitore, data, domande+risposte+allegati, riferimento normativo. Niente score se OFF. Snapshot immutabile → rigenerabile identico.
- **Multi-referente/delega (F24)**: "Inoltra a un collega" → nuovo accesso OTP per quell'email, stessa campagna/`supplier_id`, autosave condiviso, traccia `answered_by`. Delegato vede solo quella campagna (no IDOR), stessi limiti OTP.
- **Score (F23)**: `show_score_to_supplier` per template/campagna, **default false**. OFF = solo conferma invio. Toggle nell'editor "Mostra il punteggio al fornitore".
## 6. Editor lato azienda (F16) — no-code in supply-chain.html
Tab "Categorie" / "Template questionari" / "Campagne-scadenze". Mappatura tecnico→no-code:
- `slug`/`code`: auto-generati, nascosti. `order_index`: drag-and-drop (+ frecce ↑/↓ accessibili).
- `options` JSON: editor a righe ("+ Aggiungi opzione"); scale = 2 campi "Etichetta minimo/massimo".
- `reminder_offsets`: chip "14/7/1 giorni prima". `recurrence_months`: dropdown "Una tantum/Ogni 6/12/24 mesi/Personalizzato".
- `weight`: dropdown "Bassa/Media/Alta". `type`: etichette umane. `required`: toggle. `nis2_ref`: chip informativo non editabile. `show_score_to_supplier`: toggle (default OFF).
- **Anteprima "come lo vede il fornitore"**: stesso componente di rendering della compilazione (single source).
- **Versioning (F17)**: salvare un template con campagne attive → modale "crea nuova versione v{x+1}, si applica solo alle campagne future; le {n} in corso restano su v{x}". Snapshot in `questionnaire_template_versions`.
- **Cruscotto campagne**: semaforo (badge), colonne Fornitore/Questionario/Stato/Scadenza/Avanzamento/Azioni. Azioni rapide: Sollecita, Copia link (fallback manuale), Vedi risposte. **Bulk**: sollecita/avvia/esporta selezionati, con conferma e conteggio. Rispetta rate-limit reminder.
## 7. Accessibilità (F21) + mobile-first (F10)
Elementi nativi (`button`, `input radio/checkbox` con `label`); OTP `inputmode=numeric autocomplete=one-time-code pattern=[0-9]* maxlength=6`; email `inputmode=email autocomplete=email`. ARIA: `aria-live=polite` per autosave/progress, `role=alert` per errori; focus order logico, focus torna sul campo dopo errore OTP; modali con focus trap + Esc + `aria-modal`. Drag-drop con alternativa tastiera. Contrasto ≥WCAG AA, stato non solo da colore. Target ≥44×44px, single-column, no hover-only. AC: flusso completo navigabile da sola tastiera e da screen reader; OTP autofill; nessun target <44px a 360px.
## 8. i18n (F20)
Namespace `sp.*`. Lingua portale: (1) impostata dal cliente su campagna (`language it|en|auto`), (2) selezionabile dal fornitore (persistita in localStorage), (3) fallback `navigator.language` → IT. Email nella lingua della campagna. Chrome (header/footer/badge/errori/pulsanti) bilingue; i **contenuti domande** restano nella lingua in cui l'azienda li ha scritti (il selettore non auto-traduce i contenuti). AC: cambio lingua immediato senza reload; `campaign.language=en` → portale ed email in EN; nessuna stringa IT hardcoded in modalità EN.

View File

@ -0,0 +1,37 @@
{
"_meta": {
"template": "NIS2 base - Sicurezza catena di fornitura",
"description": "Template predefinito mappato alle misure ACN GV.SC (Allegato 2, Determina 164179/2025) e all'Art. 21.2(d)/21.3 della Direttiva (UE) 2022/2555. Tutte le help_text citano fonti CERTE presenti in application/config/nis2_sources.php (slug: acn_specifiche_base_2025, nis2_directive). Le fonti ENISA/NIST NON sono usate finche' non vengono aggiunte al registro fonti e ingerite in KB.",
"scoring": "yes=1.0 / partial=0.5 / no=0.0; scale_1_5 normalizzato (val-1)/4 (flag se <=2); number RTO flag se vuoto o oltre soglia per criticita; single/multi_choice non scorate ma generano flag se esposizione elevata. Output: score, risk_level, vulnerabilities[] (lista vuln_flag accesi) -> realizza Art. 21.3.",
"high_criticality_note": "Le domande con high_criticality_only=true si mostrano solo a suppliers.criticality in (high, critical).",
"source": "Prodotto da agente design 2026-05-31. Citazioni verbatim da docs/nis2/allegati_acn/Allegato2.txt (GV.SC, righe 111-189) e docs/nis2/Dir2022_2555_UE_NIS2_ITA.pdf.txt (Art.21)."
},
"questions": [
{ "order_index": 1, "code": "gvsc07_a_access_level", "text": "Qual e' il livello di accesso che la vostra fornitura richiede ai sistemi informativi e di rete del cliente?", "type": "single_choice", "options": ["nessun_accesso","accesso_applicativo_limitato","accesso_rete_segmentato","accesso_amministrativo_privilegiato"], "weight": 5, "required": true, "help_text": "ACN Allegato 2, GV.SC-07 §1.a: «il livello di accesso del fornitore ai sistemi informativi e di rete del soggetto NIS».", "nis2_ref": "GV.SC-07.a", "vuln_flag": "access_privileged_unmanaged", "high_criticality_only": false },
{ "order_index": 2, "code": "gvsc07_b_data_access", "text": "A quali tipologie di dati o proprieta' intellettuale del cliente avete accesso?", "type": "multi_choice", "options": ["nessun_dato","dati_aziendali_non_sensibili","dati_personali","dati_critici_o_segreti","proprieta_intellettuale"], "weight": 4, "required": true, "help_text": "ACN Allegato 2, GV.SC-07 §1.b: «l'accesso del fornitore alla proprieta' intellettuale e ai dati anche sulla base della loro criticita'».", "nis2_ref": "GV.SC-07.b", "vuln_flag": "sensitive_data_access", "high_criticality_only": false },
{ "order_index": 3, "code": "gvsc07_c_disruption_impact", "text": "Disponete di misure che limitano l'impatto sul cliente in caso di grave interruzione della vostra fornitura?", "type": "yes_no_partial", "options": null, "weight": 4, "required": true, "help_text": "ACN Allegato 2, GV.SC-07 §1.c: «l'impatto di una grave interruzione della fornitura».", "nis2_ref": "GV.SC-07.c", "vuln_flag": "no_continuity_for_client", "high_criticality_only": false },
{ "order_index": 4, "code": "gvsc07_d_rto", "text": "Qual e' il vostro RTO (Recovery Time Objective) dichiarato, in ore, per il ripristino del servizio in caso di indisponibilita'?", "type": "number", "options": null, "weight": 4, "required": true, "help_text": "ACN Allegato 2, GV.SC-07 §1.d: «i tempi e i costi di ripristino in caso di indisponibilita' dei servizi».", "nis2_ref": "GV.SC-07.d", "vuln_flag": "rto_undefined_or_high", "high_criticality_only": false },
{ "order_index": 5, "code": "gvsc07_d_recovery_cost", "text": "Avete documentato una stima dei tempi e dei costi di ripristino dei servizi forniti al cliente?", "type": "yes_no_partial", "options": null, "weight": 3, "required": true, "help_text": "ACN Allegato 2, GV.SC-07 §1.d: «i tempi e i costi di ripristino in caso di indisponibilita' dei servizi».", "nis2_ref": "GV.SC-07.d", "vuln_flag": "recovery_cost_unknown", "high_criticality_only": true },
{ "order_index": 6, "code": "gvsc07_e_roles", "text": "I ruoli e le responsabilita' del vostro personale nel governo dei sistemi informativi e di rete del cliente sono formalizzati (es. a contratto)?", "type": "yes_no_partial", "options": null, "weight": 4, "required": true, "help_text": "ACN Allegato 2, GV.SC-07 §1.e: «i ruoli e le responsabilita' del fornitore nel governo dei sistemi informativi e di rete».", "nis2_ref": "GV.SC-07.e", "vuln_flag": "no_security_roles_defined", "high_criticality_only": false },
{ "order_index": 7, "code": "iso27001", "text": "Disponete di una certificazione ISO/IEC 27001 (o equivalente) in corso di validita'?", "type": "yes_no_partial", "options": null, "weight": 4, "required": true, "help_text": "ACN Allegato 2, GV.SC-01 §2.d «conformita' e audit di sicurezza»; affidabilita' del fornitore ex GV.SC-01 §2.a.", "nis2_ref": "GV.SC-01.2.d", "vuln_flag": "no_iso27001", "high_criticality_only": false },
{ "order_index": 8, "code": "mfa", "text": "Imponete l'autenticazione a piu' fattori (MFA) per gli accessi ai sistemi che trattano dati del cliente?", "type": "yes_no_partial", "options": null, "weight": 5, "required": true, "help_text": "ACN Allegato 2, GV.SC-01 §2.g «gestione dell'autenticazione, delle identita' digitali e del controllo accessi»; cfr. Direttiva (UE) 2022/2555, Art. 21.2.j (autenticazione a piu' fattori).", "nis2_ref": "GV.SC-01.2.g", "vuln_flag": "no_mfa", "high_criticality_only": false },
{ "order_index": 9, "code": "patching", "text": "Avete un processo formale di patch management e gestione delle vulnerabilita'?", "type": "yes_no_partial", "options": null, "weight": 4, "required": true, "help_text": "ACN Allegato 2, GV.SC-01 §2.e «gestione delle vulnerabilita'»; cfr. Direttiva (UE) 2022/2555, Art. 21.2.e.", "nis2_ref": "GV.SC-01.2.e", "vuln_flag": "no_patch_mgmt", "high_criticality_only": false },
{ "order_index": 10, "code": "backup", "text": "Eseguite backup regolari con test di ripristino documentati?", "type": "yes_no_partial", "options": null, "weight": 4, "required": true, "help_text": "ACN Allegato 2, GV.SC-01 §2.f «continuita' operativa e ripristino in caso di disastro».", "nis2_ref": "GV.SC-01.2.f", "vuln_flag": "no_backup_tested", "high_criticality_only": false },
{ "order_index": 11, "code": "incident", "text": "Disponete di un piano di gestione degli incidenti che preveda la notifica al cliente?", "type": "yes_no_partial", "options": null, "weight": 5, "required": true, "help_text": "ACN Allegato 2, GV.SC-01 §2.m «gestione e segnalazione degli incidenti»; cfr. Direttiva (UE) 2022/2555, Art. 21.2.b.", "nis2_ref": "GV.SC-01.2.m", "vuln_flag": "no_incident_plan", "high_criticality_only": false },
{ "order_index": 12, "code": "access_review", "text": "Effettuate revisioni periodiche degli accessi con revoca tempestiva al cambio/cessazione di personale?", "type": "yes_no_partial", "options": null, "weight": 3, "required": true, "help_text": "ACN Allegato 2, GV.SC-01 §2.g «gestione dell'autenticazione, delle identita' digitali e del controllo accessi».", "nis2_ref": "GV.SC-01.2.g", "vuln_flag": "no_access_review", "high_criticality_only": false },
{ "order_index": 13, "code": "encryption", "text": "I dati del cliente sono cifrati a riposo e in transito con algoritmi allo stato dell'arte?", "type": "yes_no_partial", "options": null, "weight": 4, "required": true, "help_text": "ACN Allegato 2, GV.SC-01 §2.j «sicurezza dei dati»; cfr. Direttiva (UE) 2022/2555, Art. 21.2.h (crittografia).", "nis2_ref": "GV.SC-01.2.j", "vuln_flag": "no_encryption", "high_criticality_only": false },
{ "order_index": 14, "code": "subcontractor", "text": "Valutate la sicurezza dei vostri sub-fornitori (quarta parte) prima dell'affidamento?", "type": "yes_no_partial", "options": null, "weight": 3, "required": true, "help_text": "ACN Allegato 2, GV.SC-01 §2.q «subappalto, subfornitura o relativi potenziali requisiti di sicurezza lungo la catena di fornitura».", "nis2_ref": "GV.SC-01.2.q", "vuln_flag": "no_fourthparty_assessment", "high_criticality_only": false },
{ "order_index": 15, "code": "gvsc01_p_data_return", "text": "A fine fornitura garantite la restituzione e la cancellazione certificata dei dati del cliente?", "type": "yes_no_partial", "options": null, "weight": 4, "required": true, "help_text": "ACN Allegato 2, GV.SC-01 §2.p «dismissione della fornitura ivi compresa la restituzione e la cancellazione dei dati».", "nis2_ref": "GV.SC-01.2.p", "vuln_flag": "no_data_deletion_exit", "high_criticality_only": false },
{ "order_index": 16, "code": "gvsc01_q_subcontracting", "text": "In caso di subappalto/subfornitura, trasferite contrattualmente gli stessi requisiti di sicurezza lungo la catena di fornitura?", "type": "yes_no_partial", "options": null, "weight": 4, "required": true, "help_text": "ACN Allegato 2, GV.SC-01 §2.q «subappalto, subfornitura o relativi potenziali requisiti di sicurezza lungo la catena di fornitura».", "nis2_ref": "GV.SC-01.2.q", "vuln_flag": "no_security_flowdown", "high_criticality_only": false },
{ "order_index": 17, "code": "gvsc01_q_critical_subs", "text": "Elencate i sub-fornitori critici (quarta parte) coinvolti nella fornitura al cliente (ragione sociale e servizio).", "type": "text", "options": null, "weight": 3, "required": false, "help_text": "ACN Allegato 2, GV.SC-01 §2.q (catena di fornitura); cfr. Direttiva (UE) 2022/2555, Art. 21.3 (vulnerabilita' per ogni diretto fornitore e fornitore di servizi).", "nis2_ref": "GV.SC-01.2.q", "vuln_flag": "critical_subs_undisclosed", "high_criticality_only": true },
{ "order_index": 18, "code": "gvsc01_n_secure_dev", "text": "Adottate pratiche di sviluppo sicuro del codice e di sicurezza fin dalla progettazione e per impostazione predefinita?", "type": "yes_no_partial", "options": null, "weight": 4, "required": true, "help_text": "ACN Allegato 2, GV.SC-01 §2.n «sviluppo sicuro del codice e sicurezza fin dalla progettazione e per impostazione predefinita»; cfr. Direttiva (UE) 2022/2555, Art. 21.3 (procedure di sviluppo sicuro).", "nis2_ref": "GV.SC-01.2.n", "vuln_flag": "no_secure_dev", "high_criticality_only": false },
{ "order_index": 19, "code": "gvsc01_c_hr_reliability", "text": "Verificate l'affidabilita' del personale (es. background check) prima di assegnarlo ad attivita' con accesso ai dati/sistemi del cliente?", "type": "yes_no_partial", "options": null, "weight": 3, "required": true, "help_text": "ACN Allegato 2, GV.SC-01 §2.c «affidabilita' delle risorse umane».", "nis2_ref": "GV.SC-01.2.c", "vuln_flag": "no_hr_screening", "high_criticality_only": false },
{ "order_index": 20, "code": "gvsc01_f_dr_plan", "text": "Disponete di un piano di continuita' operativa e di disaster recovery documentato e testato?", "type": "yes_no_partial", "options": null, "weight": 4, "required": true, "help_text": "ACN Allegato 2, GV.SC-01 §2.f «continuita' operativa e ripristino in caso di disastro».", "nis2_ref": "GV.SC-01.2.f", "vuln_flag": "no_dr_plan", "high_criticality_only": false },
{ "order_index": 21, "code": "gvsc01_n_secdev_sdlc", "text": "In che misura il vostro ciclo di sviluppo prevede test di sicurezza pre-rilascio (SAST/DAST/code review)?", "type": "scale_1_5", "options": ["1_assenti","2_occasionali","3_su_progetti_chiave","4_sistematici","5_automatizzati_in_pipeline"], "weight": 2, "required": false, "help_text": "ACN Allegato 2, GV.SC-01 §2.n «sviluppo sicuro del codice e sicurezza fin dalla progettazione e per impostazione predefinita».", "nis2_ref": "GV.SC-01.2.n", "vuln_flag": "weak_sdlc", "high_criticality_only": true },
{ "order_index": 22, "code": "gvsc02_security_contact", "text": "Avete designato un referente/punto di contatto per la sicurezza informatica per questa fornitura?", "type": "yes_no_partial", "options": null, "weight": 3, "required": true, "help_text": "ACN Allegato 2, GV.SC-02 §1 (ruoli e responsabilita' di sicurezza per fornitori «stabiliti, comunicati e coordinati internamente ed esternamente»).", "nis2_ref": "GV.SC-02.1", "vuln_flag": "no_security_contact", "high_criticality_only": false },
{ "order_index": 23, "code": "gvsc02_staff_roles", "text": "I ruoli e le responsabilita' in materia di sicurezza del vostro personale che opera presso/per il cliente sono definiti e comunicati?", "type": "yes_no_partial", "options": null, "weight": 3, "required": true, "help_text": "ACN Allegato 2, GV.SC-02 §1: «i ruoli e responsabilita' in materia di sicurezza informatica assegnati al personale delle terze parti».", "nis2_ref": "GV.SC-02.1", "vuln_flag": "no_staff_security_roles", "high_criticality_only": false },
{ "order_index": 24, "code": "gvsc05_contract_clauses", "text": "Siete disponibili ad accettare clausole contrattuali di sicurezza informatica richieste dal cliente?", "type": "yes_no_partial", "options": null, "weight": 4, "required": true, "help_text": "ACN Allegato 2, GV.SC-05 §1 (requisiti di sicurezza «integrati nei contratti e in altri tipi di accordi con i fornitori»).", "nis2_ref": "GV.SC-05.1", "vuln_flag": "no_security_clauses", "high_criticality_only": false },
{ "order_index": 25, "code": "gvsc05_audit_right", "text": "Consentite al cliente di verificare periodicamente, anche tramite audit, la conformita' della fornitura ai requisiti di sicurezza?", "type": "yes_no_partial", "options": null, "weight": 3, "required": true, "help_text": "ACN Allegato 2, GV.SC-07 §2: «E' verificata periodicamente e documentata la conformita' delle forniture ai requisiti di cui alla misura GV.SC-05».", "nis2_ref": "GV.SC-07.2", "vuln_flag": "no_audit_right", "high_criticality_only": true },
{ "order_index": 26, "code": "gvsc05_breach_notify", "text": "Vi impegnate contrattualmente a notificare al cliente incidenti/violazioni di sicurezza entro termini definiti?", "type": "yes_no_partial", "options": null, "weight": 4, "required": true, "help_text": "ACN Allegato 2, GV.SC-05 §1 (requisiti integrati nei contratti) in combinato con GV.SC-01 §2.m «gestione e segnalazione degli incidenti».", "nis2_ref": "GV.SC-05.1", "vuln_flag": "no_contractual_breach_notify", "high_criticality_only": false }
]
}