/* ============================================================ FLOWSHIELD — Normativa (compliance frameworks) ============================================================ */ /* (Se eliminó FrameworkCard: eran las tarjetas de marcos ENS/ISO/NIST/PCI con cifras estáticas falsas, ya retiradas. Hoy Normativa solo pinta sitios REALES analizados con SiteCard.) */ function _compHost(t) { try { const u = (t||'').includes('://') ? new URL(t) : new URL('https://'+t); return u.hostname || t; } catch { return t || '—'; } } // Real per-site card (reuses the tool-card chrome; honest data from /posture). function SiteCard({ s, onClick, active }) { const ref = useRefL(null); const onMove = (e)=>{ const el=ref.current; if(!el)return; const r=el.getBoundingClientRect(); el.style.setProperty('--mx',((e.clientX-r.left)/r.width)*100+'%'); el.style.setProperty('--my',((e.clientY-r.top)/r.height)*100+'%'); }; const col = s.score>=80?'var(--ok)':s.score>=60?'var(--amber)':'var(--red)'; return (
{s.score}%
CUMPLIMIENTO WEB
{_compHost(s.target)}
{s.rating || '—'}{s.jurisdiction?` · ${s.jurisdiction}`:''}
{window.rptDate?window.rptDate(s.date):s.date} {s.gaps?`${s.gaps} brecha${s.gaps===1?'':'s'}`:'sin brechas'}
); } function Normativa() { const [mode, setMode] = useStateL('panel'); const [post, setPost] = useStateL(null); // null = cargando const [sel, setSel] = useStateL(null); // sitio seleccionado const [detail, setDetail] = useStateL(null); // {groups,...} | 'na' | null const [gaps, setGaps] = useStateL([]); // brechas (findings) del sitio const load = () => { window.fsApi('/api/compliance/posture', { method:'GET' }) .then(d => { setPost(d); if (d.sites && d.sites.length && !sel) setSel(d.sites[0]); }) .catch(() => setPost({ sites:[], score:0, counts:{}, total_scans:0 })); }; useEffectL(() => { if (mode==='panel') load(); }, [mode]); // Cargar detalle (checks reales) + brechas del sitio seleccionado. useEffectL(() => { if (!sel) { setDetail(null); setGaps([]); return; } if (sel.scan_id) { window.fsApi('/api/compliance/scans/'+sel.scan_id, { method:'GET' }) .then(d => setDetail(d)).catch(() => setDetail('na')); } else { setDetail('na'); } // brechas reales del objetivo desde el store de hallazgos window.fsApi('/api/results/findings', { method:'GET' }) .then(d => setGaps(((d&&d.findings)||[]).filter(f => f.module==='compliance' && f.target && _compHost(f.target)===_compHost(sel.target)))) .catch(() => setGaps([])); }, [sel && sel.scan_id, sel && sel.target]); if (mode === 'scan') return setMode('panel')} />; const sites = (post && post.sites) || []; const avg = post ? post.score : 0; const counts = (post && post.counts) || { pass:0, partial:0, fail:0 }; const hasVerdicts = (counts.pass||0)+(counts.partial||0)+(counts.fail||0) > 0; const totalGaps = sites.reduce((a,s)=>a+(s.gaps||0),0); const empty = post && post.total_scans === 0; return (
setMode('scan')} glitch>Nuevo scan web} /> {empty && }
Alcance: comprobaciones automáticas de la superficie web. No verifica el contenido legal de las políticas ni las obligaciones internas del RGPD (RoPA, contratos de encargado, bases jurídicas, DPIA, plazos de conservación). No sustituye una auditoría legal.
{/* global posture */}
Postura de cumplimiento web
{sites.length ? <>Cumplimiento medio del {avg}% sobre {sites.length} sitio(s) analizado(s){totalGaps?<> · {totalGaps} brecha(s) abiertas:null}. : <>Aún no hay análisis de cumplimiento. Lanza un Nuevo scan web y la postura aparecerá aquí.}
{hasVerdicts && (
{counts.pass} cumplen {counts.partial} parciales {counts.fail} brechas
)}
{sites.length > 0 && <>
Sitios analizados
{sites.map((s,i) => setSel(s)} />)}
} {/* detail */} {sel && (
{_compHost(sel.target)}
{sel.jurisdiction||sel.framework} · {sel.rating||'—'} · {sel.score}%
{detail && detail!=='na' && detail.meta && detail.meta.screenshot_pre && window.open('/api/compliance/screenshot/'+sel.scan_id+'/pre','_blank')}>Exportar evidencias}
{detail && detail!=='na' && (detail.groups||[]).map(g => (
{g.g}
{(g.checks||[]).map(c => (
{c.t}
{c.note &&
{c.note}
}
))}
))} {detail === 'na' && (
El detalle por comprobación no está guardado para este análisis (escaneo anterior). Vuelve a lanzar el scan para ver el desglose completo.{gaps.length?' Brechas registradas:':''}
{gaps.map((f,i) => (
{f.name}
))}
)}
)}
); } /* ============================================================ Compliance Scanner — escaneo de cumplimiento web ============================================================ */ // Scope groups for the selection panel. The real verdicts/notes come from the // live Playwright engine; these labels + check counts mirror what it returns. const COMP_GROUPS = [ { g:'RGPD / LOPDGDD', n:4 }, { g:'Cookies', n:5 }, { g:'Políticas y avisos legales', n:4 }, { g:'Contenido de la política de privacidad', n:8 }, { g:'Seguridad técnica', n:5 }, ]; // UI jurisdiction label -> backend jurisdiction code. const REGION_MAP = { 'UE · España (RGPD + LOPDGDD)':'ES', 'UE · General (RGPD)':'EU', 'Reino Unido (UK-GDPR)':'UK', 'EE.UU. · California (CCPA/CPRA)':'US-CA', 'Brasil (LGPD)':'BR', 'Global (best practices)':'GLOBAL', }; function CompVerdict({ v }) { const m = { pass:{c:'var(--ok)',t:'Cumple',i:'check'}, partial:{c:'var(--amber)',t:'Parcial',i:'alert'}, fail:{c:'var(--red)',t:'Incumple',i:'x'} }[v]; return {m.t}; } function ComplianceScanner({ onBack }) { const [url, setUrl] = useStateL('https://www.flowia.es'); const [region, setRegion] = useStateL('UE · España (RGPD + LOPDGDD)'); const [groups, setGroups] = useStateL(() => COMP_GROUPS.map(g=>g.g)); const [opts, setOpts] = useStateL({ crawl:true, subpages:false, screenshot:true }); const [phase, setPhase] = useStateL('idle'); // idle | running | done const [lines, setLines] = useStateL([]); const [prog, setProg] = useStateL(0); const [result, setResult] = useStateL(null); const termRef = useRefL(null); const toggleGroup = (g) => setGroups(s => s.includes(g) ? s.filter(x=>x!==g) : [...s, g]); const scan = async () => { if (!groups.length || phase==='running') return; setPhase('running'); setResult(null); setProg(8); setLines([ { t:'> compliance-engine · Playwright headless', c:'acc' }, { t:`> objetivo: ${url}`, c:'dim' }, { t:`> jurisdicción: ${region}`, c:'dim' }, { t:'[*] lanzando navegador headless (Chromium)…', c:'' }, { t:'[*] cargando página e interceptando red…', c:'' }, { t:'[*] capturando cookies previas al consentimiento…', c:'' }, ]); let p=8; const creep=setInterval(()=>{ p=Math.min(p+4,88); setProg(p); }, 350); try { const data = await window.fsApi('/api/compliance/scan', { body:{ url, jurisdiction: REGION_MAP[region] || 'EU', interact_consent: true, screenshot: opts.screenshot, groups, max_pages: opts.subpages ? 6 : 0, }}); clearInterval(creep); const raw = data.raw || {}; const meta = raw.meta || {}; const real = []; real.push(meta.cmp ? { t:`[+] CMP detectado: ${meta.cmp}`, c:'ok' } : { t:'[!] sin banner de consentimiento detectado', c:'warn' }); real.push(meta.non_essential_pre ? { t:`[!] ${meta.non_essential_pre} cookie(s) no esenciales antes de aceptar`, c:'warn' } : { t:'[+] sin cookies no esenciales en la carga inicial', c:'ok' }); real.push((meta.trackers_pre && meta.trackers_pre.length) ? { t:`[!] terceros sin consentimiento: ${meta.trackers_pre.join(', ')}`, c:'err' } : { t:'[+] sin terceros antes del consentimiento', c:'ok' }); if (meta.cmp_clicked) real.push({ t:`[*] 'Aceptar' pulsado · cookies ${meta.cookies_pre}→${meta.cookies_post} · terceros ${(meta.third_parties_pre||[]).length}→${(meta.third_parties_post||[]).length}`, c:'' }); if (meta.pages_scanned > 1) real.push({ t:`[*] crawl: ${meta.pages_scanned} páginas analizadas · ${meta.forms_personal||0} formulario(s) con datos personales`, c:'' }); if (groups.includes('Contenido de la política de privacidad')) real.push(meta.policy_analyzed ? { t:'[*] contenido de la política de privacidad analizado (deber de información RGPD)', c:'' } : { t:'[!] no se pudo leer el contenido de la política de privacidad', c:'warn' }); if (meta.screenshot_pre) real.push({ t:'[*] evidencias (screenshots) capturadas', c:'' }); real.push({ t:`> evaluación completada · cumplimiento ${raw.compliance_score}%`, c:'ok' }); setLines(l => [...l, ...real]); setProg(100); setResult(raw); setPhase('done'); } catch (e) { clearInterval(creep); setLines(l => [...l, { t:`[x] error: ${e.message || e}`, c:'err' }]); setProg(100); setPhase('idle'); } }; useEffectL(()=>{ if(termRef.current) termRef.current.scrollTop=termRef.current.scrollHeight; }, [lines]); const resultGroups = result ? (result.groups||[]).filter(g=>groups.includes(g.g)) : []; const activeChecks = resultGroups.flatMap(g=>g.checks); const counts = activeChecks.reduce((a,c)=>{a[c.verdict]=(a[c.verdict]||0)+1;return a;},{}); const score = result ? result.compliance_score : (activeChecks.length ? Math.round(((counts.pass||0)+(counts.partial||0)*0.5)/activeChecks.length*100) : 0); const exportEvidence = () => { if (result && result.scan_id && result.meta && result.meta.screenshot_pre) window.open('/api/compliance/screenshot/'+result.scan_id+'/pre', '_blank'); }; const [reporting, setReporting] = useStateL(false); const downloadReport = async () => { if (!result || reporting) return; setReporting(true); try { await window.fsDownloadPdf('/api/report/compliance', result); } catch (e) { setLines(l => [...l, { t:'[x] no se pudo generar el informe: ' + (e.message || e), c:'err' }]); } finally { setReporting(false); } }; return (
{/* config */}
Objetivo y alcance
setUrl(e.target.value)} disabled={phase==='running'} />
{COMP_GROUPS.map(g => ( ))}
setOpts(o=>({...o,subpages:!o.subpages}))} disabled={phase==='running'} /> setOpts(o=>({...o,screenshot:!o.screenshot}))} disabled={phase==='running'} />
{phase==='done' ?
Re-escanear{reporting ? 'Generando…' : 'Informe PDF'}
: {phase==='running'?'ESCANEANDO…':'ANALIZAR CUMPLIMIENTO WEB'}}
{/* terminal + score */}
flowshield@normativa: ~/compliance
{phase==='idle' &&
// selecciona ámbitos y pulsa ANALIZAR CUMPLIMIENTO…
} {lines.map((l,i)=>l &&
{l.t}
)} {phase==='running' &&
}
{phase==='done' && (
=80?'var(--ok)':score>=60?'var(--amber)':'var(--red)'} sub="CUMPLIMIENTO WEB" />
)}
{phase==='done' && (
Resultados por comprobación · {url}
Exportar evidencias
{resultGroups.map(g => (
{g.g}
{g.checks.map(c => (
{c.t}
{c.note &&
{c.note}
}
))}
))}
)}
); } Object.assign(window, { Normativa, ComplianceScanner });