/* ============================================================ FLOWSHIELD — Normativa (compliance frameworks) ============================================================ */ function FrameworkCard({ f, 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 = f.score>=80?'var(--ok)':f.score>=70?'var(--amber)':'var(--red)'; return (
{f.score}%
CUMPLIMIENTO
{f.name}
{f.full}
✓ {f.met} cumple ◐ {f.partial} parcial ✕ {f.gap} brecha
); } 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
{_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 && } {/* global posture */}
Postura de cumplimiento
{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:'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', '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 (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'}}
{/* 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" />
)}
{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 });