Files
nis2-agile/public/sw.js
T
DevEnv nis2-agileandClaude Opus 4.8 b917d2da14 [FIX] Epic C / C5 hardening — esiti flotta di verifica multi-agente (mig.054)
Corretti i finding confermati dalla verifica multi-agente (5 major + minori):

SICUREZZA
- Stored XSS allegati: da blocklist a ALLOWLIST di estensioni innocue (no html/svg/js
  renderizzabili same-origin) in StakeholderPortalController::attachment e
  StakeholderActivityController::storeUpload; nome file randomizzato (random_bytes).
- Magic-link: scadenza (mig.054 stk_activity_targets.token_expires_at; send() imposta
  scadenza attività+30gg o +90gg; resolveTarget() → 410 TOKEN_EXPIRED se scaduto);
  rate-limit per-IP sugli endpoint del portale; comment/attachment bloccati su attività chiusa.

CORRETTEZZA
- send(): NON rigenera token né azzera lo stato dei destinatari già responded/acknowledged
  (prima ne perdeva l'esito); imposta token_expires_at.
- assign individuale: semantica "replace" (rimuove i deselezionati non ancora conclusi) +
  guard su lista vuota (evita 'IN ()').
- update(): conserva assign_mode esistente quando si modifica solo stak_code.

UI/UX/A11Y
- Editor opzioni per domande a scelta singola/multipla (prima degradavano a testo nel portale).
- Etichette stato/tipo localizzate; risposte mostrate inline (no alert()); escAttr nel portale
  (escape virgolette negli attributi); ARIA su modali/tab; voce sidebar anche in common.js.

OPEN ITEM (NON regressione C5, pre-esistente e ambientale): l'upload allegati restituisce
UPLOAD_ERROR in prod — move_uploaded_file/is_uploaded_file fallisce nella topologia
proxy→fastcgi (stesso pattern di evidence_files/AuditController, mai funzionato: la dir
uploads/evidence non esiste). rename/copy come www-data funzionano. Da investigare lato infra.
Il resto di C5 (questionari, firma-lettura, commenti, calendario, portale) è pienamente operativo.

Smoke prod OK: allowlist (.html→422, struttura ok), opzioni scelta nel portale, send no-reset,
replace individuale, token_expires_at presente. Additivo. v1.21.1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 22:52:31 +02:00

97 lines
3.4 KiB
JavaScript

/*
* NIS2 Agile — Service Worker (PWA)
* -----------------------------------------------------------------------------
* Strategia (SaaS autenticato multi-tenant — sicurezza prima di tutto):
* - /api/* -> NETWORK-ONLY, MAI in cache. Nessun dato autenticato
* viene cacheato => zero leak cross-utente su un
* dispositivo condiviso. La sessione resta gestita da
* JWT / auth-gate.js, fuori dal service worker.
* - navigazioni HTML -> NETWORK-FIRST, fallback alla cache e infine /offline.html.
* - asset statici -> STALE-WHILE-REVALIDATE (risposta immediata dalla cache,
* aggiornamento in background).
*
* Il nome cache include la release: va BUMPATO ad ogni rilascio (vedi version.json),
* cosi' activate elimina automaticamente la shell vecchia.
*/
const CACHE = 'nis2-shell-v1.21.1';
const PRECACHE = [
'/offline.html',
'/manifest.webmanifest',
'/css/style.css',
'/js/common.js',
'/js/common-bi.js',
'/js/api.js',
'/js/i18n.js',
'/js/pwa.js',
'/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css',
'/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js',
'/vendor/bootstrap-italia/dist/svg/sprites.svg',
'/assets/icons/icon-192.png',
'/assets/icons/icon-512.png'
];
self.addEventListener('install', (event) => {
event.waitUntil((async () => {
const cache = await caches.open(CACHE);
// add uno-a-uno: un singolo asset mancante non deve far fallire l'install
await Promise.all(PRECACHE.map((url) =>
cache.add(new Request(url, { cache: 'reload' })).catch((err) =>
console.warn('[sw] precache skip', url, err && err.message))
));
await self.skipWaiting();
})());
});
self.addEventListener('activate', (event) => {
event.waitUntil((async () => {
const keys = await caches.keys();
await Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)));
await self.clients.claim();
})());
});
self.addEventListener('fetch', (event) => {
const req = event.request;
if (req.method !== 'GET') return; // non-GET: rete normale
const url = new URL(req.url);
if (url.origin !== self.location.origin) return; // cross-origin: rete normale
// /api/* -> network-only, mai in cache
if (url.pathname.startsWith('/api/')) {
event.respondWith(
fetch(req).catch(() => new Response(
JSON.stringify({ error: 'offline', message: 'Connessione di rete assente' }),
{ status: 503, headers: { 'Content-Type': 'application/json' } }
))
);
return;
}
// navigazioni HTML -> network-first, fallback cache -> offline
if (req.mode === 'navigate') {
event.respondWith((async () => {
try {
return await fetch(req);
} catch (e) {
const cached = await caches.match(req);
return cached || (await caches.match('/offline.html'));
}
})());
return;
}
// asset statici stesso-origin -> stale-while-revalidate
event.respondWith((async () => {
const cache = await caches.open(CACHE);
const cached = await cache.match(req);
const network = fetch(req).then((res) => {
if (res && res.status === 200 && res.type === 'basic') {
cache.put(req, res.clone());
}
return res;
}).catch(() => null);
return cached || (await network) || (await caches.match('/offline.html'));
})());
});