From aa2db4c6c24dc748e1d74d16767bfb0652b83b3b Mon Sep 17 00:00:00 2001 From: DevEnv nis2-agile Date: Sun, 31 May 2026 09:53:07 +0200 Subject: [PATCH] [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 --- docs/DESIGN_MODULO_QUESTIONARI_FORNITORI.md | 113 +++++++++- .../032_supplier_questionnaires_module.sql | 209 ++++++++++++++++++ docs/sql/033_suppliers_category_source.sql | 79 +++++++ docs/sql/034_supplier_portal_auth.sql | 85 +++++++ docs/sql/035_migrate_027_to_campaigns.sql | 69 ++++++ .../AI_CONSULENTE_NORMATIVO.md | 91 ++++++++ docs/supplier-portal/UX_MINI_SPEC.md | 68 ++++++ .../template-nis2-base.questions.json | 37 ++++ 8 files changed, 749 insertions(+), 2 deletions(-) create mode 100644 docs/sql/032_supplier_questionnaires_module.sql create mode 100644 docs/sql/033_suppliers_category_source.sql create mode 100644 docs/sql/034_supplier_portal_auth.sql create mode 100644 docs/sql/035_migrate_027_to_campaigns.sql create mode 100644 docs/supplier-portal/AI_CONSULENTE_NORMATIVO.md create mode 100644 docs/supplier-portal/UX_MINI_SPEC.md create mode 100644 docs/supplier-portal/template-nis2-base.questions.json diff --git a/docs/DESIGN_MODULO_QUESTIONARI_FORNITORI.md b/docs/DESIGN_MODULO_QUESTIONARI_FORNITORI.md index 7d15c32..47e4436 100644 --- a/docs/DESIGN_MODULO_QUESTIONARI_FORNITORI.md +++ b/docs/DESIGN_MODULO_QUESTIONARI_FORNITORI.md @@ -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 | diff --git a/docs/sql/032_supplier_questionnaires_module.sql b/docs/sql/032_supplier_questionnaires_module.sql new file mode 100644 index 0000000..43db329 --- /dev/null +++ b/docs/sql/032_supplier_questionnaires_module.sql @@ -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; diff --git a/docs/sql/033_suppliers_category_source.sql b/docs/sql/033_suppliers_category_source.sql new file mode 100644 index 0000000..e272f08 --- /dev/null +++ b/docs/sql/033_suppliers_category_source.sql @@ -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). diff --git a/docs/sql/034_supplier_portal_auth.sql b/docs/sql/034_supplier_portal_auth.sql new file mode 100644 index 0000000..46b6b3e --- /dev/null +++ b/docs/sql/034_supplier_portal_auth.sql @@ -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; diff --git a/docs/sql/035_migrate_027_to_campaigns.sql b/docs/sql/035_migrate_027_to_campaigns.sql new file mode 100644 index 0000000..14c322e --- /dev/null +++ b/docs/sql/035_migrate_027_to_campaigns.sql @@ -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; diff --git a/docs/supplier-portal/AI_CONSULENTE_NORMATIVO.md b/docs/supplier-portal/AI_CONSULENTE_NORMATIVO.md new file mode 100644 index 0000000..4044967 --- /dev/null +++ b/docs/supplier-portal/AI_CONSULENTE_NORMATIVO.md @@ -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. diff --git a/docs/supplier-portal/UX_MINI_SPEC.md b/docs/supplier-portal/UX_MINI_SPEC.md new file mode 100644 index 0000000..a371b94 --- /dev/null +++ b/docs/supplier-portal/UX_MINI_SPEC.md @@ -0,0 +1,68 @@ +# Mini-spec UX — Portale fornitore + Editor lato azienda (§6) + +> Deliverable agente design 2026-05-31. Chiude i finding UX F1–F25 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. diff --git a/docs/supplier-portal/template-nis2-base.questions.json b/docs/supplier-portal/template-nis2-base.questions.json new file mode 100644 index 0000000..41a3f08 --- /dev/null +++ b/docs/supplier-portal/template-nis2-base.questions.json @@ -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 } + ] +}