/* ============================================================ FLOWSHIELD — Findings / Scans / Reports / Surface / misc ============================================================ */ function DemoBanner({ what }) { return (
DATOS DE EJEMPLO · este workspace aún no tiene {what} reales. Lanza un escaneo y aparecerán aquí automáticamente.
); } function csvDownload(name, header, rows) { const esc = (x) => '"' + String(x == null ? '' : x).replace(/"/g, '""') + '"'; const csv = [header, ...rows].map(r => r.map(esc).join(',')).join('\n'); const a = document.createElement('a'); a.href = URL.createObjectURL(new Blob([csv], { type: 'text/csv' })); a.download = name; document.body.appendChild(a); a.click(); a.remove(); } function Findings() { const [filter, setFilter] = useStateL('all'); const [sevF, setSevF] = useStateL('all'); const [raw, setRaw] = useStateL(null); // null = cargando const [counts, setCounts] = useStateL(null); const [demo, setDemo] = useStateL(false); const load = () => { window.fsApi('/api/results/findings', { method:'GET' }) .then(d => { const items = (d && d.findings) || []; if (items.length === 0) { setDemo(true); setRaw(window.SEED_FINDINGS); setCounts(null); } else { setDemo(false); setRaw(items); setCounts(d.counts); } }) .catch(() => { setDemo(true); setRaw(window.SEED_FINDINGS); setCounts(null); }); }; useEffectL(() => { load(); }, []); const all = raw || []; const data = all.filter(f => (filter==='all'||f.status===filter) && (sevF==='all'||f.sev===sevF)); const c = counts ? counts.sev : all.reduce((a,f)=>{a[f.sev]=(a[f.sev]||0)+1;return a;},{}); const total = counts ? counts.total : all.length; const openN = counts ? counts.status.open : all.filter(f=>f.status==='open').length; const fixedN = counts ? counts.status.fixed : all.filter(f=>f.status==='fixed').length; const cycle = async (f) => { if (demo) return; const next = f.status==='open' ? 'triage' : f.status==='triage' ? 'fixed' : 'open'; try { await window.fsApi('/api/results/findings/'+f.id, { method:'PATCH', body:{ status: next } }); load(); } catch (e) { window.alert('No se pudo cambiar el estado: ' + (e.message||e)); } }; const exportCsv = () => csvDownload('flowshield-hallazgos.csv', ['id','hallazgo','objetivo','severidad','cvss','estado','origen','fecha'], all.map(f=>[f.code||f.id, f.name, f.target, f.sev, f.cvss, f.status, f.source, f.date])); const [pdfBusy, setPdfBusy] = useStateL(false); const exportPdf = async () => { if (demo) return; setPdfBusy(true); try { await window.fsDownloadPdf('/api/report/findings', {}); } catch (e) { window.alert('No se pudo generar el informe: ' + (e.message||e)); } setPdfBusy(false); }; return (
ActualizarCSV{pdfBusy?'Generando…':'Informe PDF'}} /> {demo && }
{[['all','Todos'],['open','Abiertos'],['triage','Triaje'],['fixed','Resueltos']].map(([k,l])=>( ))}
{[['all','Toda severidad'],['crit','Crítico'],['high','Alto'],['med','Medio'],['low','Bajo']].map(([k,l])=>( ))}
{raw === null && } {raw !== null && data.length === 0 && } {data.map(f => ( ))}
IDHallazgoObjetivoSeveridadCVSSEstadoOrigenFecha
Cargando…
Sin hallazgos para este filtro.
{f.code || f.id} {f.name} {f.target} {f.cvss} cycle(f)} title={demo?'':'Cambiar estado (abierto → triaje → resuelto)'} style={{ cursor:demo?'default':'pointer' }}> {f.source} {rptDate(f.date)}
); } const _TEAM_LABEL = { red:'RED', blue:'BLUE', amber:'NORM', fs:'—' }; const _TEAM_COLOR = { red:'var(--red)', blue:'var(--blue)', amber:'var(--amber)', fs:'var(--txt-dim)' }; function Scans({ onNav, onRelaunch }) { const [raw, setRaw] = useStateL(null); const [demo, setDemo] = useStateL(false); const load = () => { window.fsApi('/api/results/scans', { method:'GET' }) .then(d => { const items = (d && d.scans) || []; if (items.length === 0) { setDemo(true); setRaw(window.SEED_SCANS); } else { setDemo(false); setRaw(items); } }) .catch(() => { setDemo(true); setRaw(window.SEED_SCANS); }); }; useEffectL(() => { load(); }, []); const del = async (s) => { if (demo) return; if (!window.confirm('¿Borrar este escaneo y sus hallazgos?')) return; try { await window.fsApi('/api/results/scans/'+s.id, { method:'DELETE' }); load(); } catch (e) { window.alert('No se pudo borrar el escaneo: ' + (e.message||e)); } }; const list = raw || []; return (
ActualizaronNav && onNav('redteam')}>Nuevo escaneo} /> {demo && }
{raw === null && } {raw !== null && list.length === 0 && } {list.map(s => { const team = s.team || (s.module==='redteam'?'red':s.module==='blueteam'?'blue':s.module==='compliance'?'amber':'fs'); return ( );})}
IDHerramientaEquipoObjetivoEstadoResumenFecha
Cargando…
Aún no hay escaneos.
{s.code || s.id} {s.tool} {_TEAM_LABEL[team]} {s.target} {s.status==='running' ?
: }
{s.found && (s.found.crit||s.found.high||s.found.med||s.found.low) ?
{['crit','high','med','low'].map(k=>s.found[k]?{s.found[k]}:null)}
: }
{rptDate(s.date)} {!demo && <> {onRelaunch && onRelaunch(s)} title="Re-escanear con los mismos parámetros">Re-escanear} del(s)} style={{ color:'var(--danger)' }} /> }
); } function rptDate(iso) { try { return new Date(iso).toLocaleString('es-ES', { dateStyle:'medium', timeStyle:'short' }); } catch { return iso || '—'; } } function Reports() { const [reports, setReports] = useStateL(null); // null = cargando const [tab, setTab] = useStateL('all'); // all | red | blue | amber const [gen, setGen] = useStateL(false); // panel generador abierto const [scans, setScans] = useStateL(null); // escaneos Blue Team para generar const [busy, setBusy] = useStateL(''); // id en curso (generar) const refresh = () => window.fsApi('/api/report/list', { method:'GET' }) .then(d => setReports(d.reports || [])).catch(()=> setReports([])); useEffectL(() => { refresh(); }, []); const toggleGen = () => { setGen(g => !g); if (scans === null) window.fsApi('/api/blueteam/scans', { method:'GET' }) .then(d => setScans(d.scans || [])).catch(()=> setScans([])); }; const genFromScan = async (s) => { setBusy('gen:'+s.scan_id); try { await window.fsDownloadPdf('/api/report/blueteam', s); await refresh(); } catch(e) { window.alert('No se pudo generar el informe: ' + (e.message||e)); } finally { setBusy(''); } }; const del = async (id) => { if (!window.confirm('¿Eliminar este informe de la biblioteca?')) return; try { await window.fsApi('/api/report/'+encodeURIComponent(id), { method:'DELETE' }); setReports(rs => (rs||[]).filter(r => r.id !== id)); } catch(e) { window.alert('No se pudo eliminar el informe: ' + (e.message||e)); } }; const TABS = [['all','Todos'],['red','Red Team'],['blue','Blue Team'],['amber','Cumplimiento']]; const list = (reports||[]).filter(r => tab==='all' || r.team===tab); return (
{gen?'Cerrar':'Crear informe'}} /> {gen && (
Generar informe desde un escaneo Blue Team
{scans === null ? (
Cargando escaneos…
) : !scans.length ? (
No hay escaneos Blue Team registrados. Lanza uno en Blue Team → Nuevo scan. Los informes de Red Team y Normativa se archivan automáticamente al pulsar «Informe PDF» en sus paneles.
) : (
{scans.slice(0,20).map(s => { const su=s.summary||{}; return (
{s.os} · {s.target}
{rptDate(s.ts)} · {s.profile_title||s.profile}
{su.score||0}% genFromScan(s)}>{busy==='gen:'+s.scan_id?'Generando…':'Generar PDF'}
);})}
)}
)}
{TABS.map(([k,l])=>)}
{reports === null ? (
Cargando informes…
) : !list.length ? ( ) : (
{list.map(r => (
{r.fmt||'PDF'}
{r.title}
{rptDate(r.date)}
{ if(!r.available) return; const a=document.createElement('a'); a.href='/api/report/file/'+r.id; a.download=r.filename||''; document.body.appendChild(a); a.click(); a.remove(); }} style={{ opacity:r.available?1:0.4 }} title={r.available?'Descargar PDF':'Archivo no disponible'}>Descargar del(r.id)} title="Eliminar informe">Borrar
))}
)}
); } const SEED_SURFACE = [ { ip:'10.0.4.21', host:'web-prod-01', os:'Ubuntu 22.04', port:'443', svc:'nginx 1.24', tech:'PHP 8.2', risk:'crit' }, { ip:'10.0.4.32', host:'db-core-02', os:'RHEL 9', port:'5432', svc:'PostgreSQL 14.2', tech:'—', risk:'high' }, { ip:'23.214.88.10', host:'app.flowia.es', os:'Linux', port:'443', svc:'Cloudflare', tech:'React · Node', risk:'crit' }, { ip:'23.214.88.14', host:'mail.flowia.es', os:'Linux', port:'993', svc:'Dovecot', tech:'—', risk:'med' }, { ip:'192.168.10.1', host:'edge-fw-01', os:'Cisco IOS', port:'22', svc:'SSH', tech:'—', risk:'low' }, ]; function Surface() { const [rows, setRows] = useStateL(null); const [counts, setCounts] = useStateL(null); const [demo, setDemo] = useStateL(false); const load = () => { window.fsApi('/api/results/surface', { method:'GET' }) .then(d => { const items = (d && d.assets) || []; if (items.length === 0) { setDemo(true); setRows(SEED_SURFACE); setCounts(null); } else { setDemo(false); setRows(items); setCounts(d.counts); } }) .catch(() => { setDemo(true); setRows(SEED_SURFACE); setCounts(null); }); }; useEffectL(() => { load(); }, []); const list = rows || []; const exportCsv = () => csvDownload('flowshield-superficie.csv', ['ip','hostname','so','puerto','servicio','tecnologia','riesgo'], list.map(r=>[r.ip, r.host, r.os, r.port, r.svc, r.tech, r.risk])); const atRisk = counts ? counts.at_risk : list.filter(r=>r.risk==='crit'||r.risk==='high').length; const [pdfBusy, setPdfBusy] = useStateL(false); const exportPdf = async () => { if (demo) return; setPdfBusy(true); try { await window.fsDownloadPdf('/api/report/surface', {}); } catch (e) { window.alert('No se pudo generar el informe: ' + (e.message||e)); } setPdfBusy(false); }; return (
ActualizarCSV{pdfBusy?'Generando…':'Informe PDF'}} /> {demo && }
r.host)).size} icon="server" accent="var(--fs)" /> r.port&&r.port!=='—').length} icon="port" accent="var(--blue)" /> r.svc).filter(s=>s&&s!=='—')).size} icon="network" accent="var(--amber)" />
{rows === null && } {rows !== null && list.length === 0 && } {list.map((r,i)=>( ))}
IPHostnameSOPuertoServicioTecnologíaRiesgo
Cargando…
Sin activos. Lanza un Port Scanner, Subdomain Finder o Fingerprint.
{r.ip} {r.host} {r.os} {r.port} {r.svc} {r.tech}
); } function Handlers() { const h = [ { name:'XSS Exploiter', icon:'xss', desc:'Explota un XSS extrayendo cookies, contenido HTML, capturas o pulsaciones de teclado.' }, { name:'HTTP Request Loggers', icon:'http', desc:'Captura peticiones HTTP entrantes: IP origen, método, parámetros, User-Agent, SO, cabeceras.' }, ]; return (
{h.map((x,i)=>(
PRÓXIMAMENTE
{x.name}
{x.desc}
REQUIERE MÓDULO DE EXPLOTACIÓN · PRÓXIMAMENTE
))}
); } const _INT_CATALOG = [ { n:'Microsoft Teams', d:'Resumen instantáneo de resultados en tu canal.', c:'#5059C9', kind:'teams' }, { n:'Slack', d:'Digest de escaneos y alertas por indicadores de riesgo.', c:'#E01E5A', kind:'slack' }, { n:'Discord', d:'Recibe el resumen de tus escaneos en tu canal.', c:'#5865F2', kind:'discord' }, { n:'Webhook', d:'POST JSON a tu endpoint ante cada escaneo o hallazgo.', c:'#00F5C0', kind:'webhook' }, { n:'Jira', d:'Crea y asigna tareas desde los hallazgos.', c:'#2684FF', kind:null }, { n:'AWS', d:'Importa automáticamente tus targets EC2 y S3.', c:'#FF9900', kind:null }, { n:'GitHub Actions', d:'Testea apps web en cada despliegue.', c:'#AEBAD0', kind:null }, { n:'Burp Suite', d:'Envía issues de Burp a tus Findings en segundos.', c:'#FF6633', kind:null }, ]; const _KIND_LABEL = { slack:'Slack', teams:'Microsoft Teams', discord:'Discord', webhook:'Webhook' }; function Integrations() { const [list, setList] = useStateL(null); // null = cargando const [form, setForm] = useStateL(null); // {kind,name,url} | null const [busy, setBusy] = useStateL(''); // id/acción en curso const [msg, setMsg] = useStateL(null); // {type,text} const load = () => window.fsApi('/api/integrations', { method:'GET' }) .then(d => setList((d&&d.integrations)||[])).catch(()=>setList([])); useEffectL(() => { load(); }, []); const openForm = (kind) => { setMsg(null); setForm({ kind, name:_KIND_LABEL[kind]||'', url:'' }); }; const create = async () => { if (!form || !form.url.trim()) return; setBusy('create'); setMsg(null); try { await window.fsApi('/api/integrations', { body:{ kind:form.kind, name:form.name, url:form.url.trim() } }); setForm(null); load(); setMsg({ type:'ok', text:'Integración añadida.' }); } catch (e) { setMsg({ type:'err', text:e.message||'No se pudo añadir.' }); } setBusy(''); }; const toggle = async (it) => { setBusy(it.id); try { await window.fsApi('/api/integrations/'+it.id+'/enabled', { body:{ enabled: !it.enabled } }); load(); } catch (e) { setMsg({ type:'err', text:e.message||'Error.' }); } setBusy(''); }; const test = async (it) => { setBusy('test:'+it.id); setMsg(null); try { await window.fsApi('/api/integrations/'+it.id+'/test', { method:'POST' }); setMsg({ type:'ok', text:'Mensaje de prueba enviado a «'+it.name+'».' }); } catch (e) { setMsg({ type:'err', text:e.message||'El webhook no respondió.' }); } setBusy(''); }; const del = async (it) => { if (!window.confirm('¿Eliminar la integración «'+it.name+'»?')) return; setBusy(it.id); try { await window.fsApi('/api/integrations/'+it.id, { method:'DELETE' }); load(); } catch (e) { setMsg({ type:'err', text:e.message||'Error.' }); } setBusy(''); }; const connected = list || []; return (
{msg &&
{msg.text}
} {/* connected */} {connected.length > 0 && (
Conectadas
{connected.map(it => ( ))}
IntegraciónTipoEndpointEstado
{it.name} {_KIND_LABEL[it.kind]||it.kind} {it.url_masked} {it.enabled?'Activa':'Pausada'} test(it)}>{busy==='test:'+it.id?'Enviando…':'Probar'} toggle(it)}>{it.enabled?'Pausar':'Activar'} del(it)} style={{ color:'var(--danger)' }} />
)} {/* add form */} {form && (
Nueva integración · {_KIND_LABEL[form.kind]}
setForm(f=>({...f,name:e.target.value}))} placeholder="p.ej. #seguridad" style={{ width:'100%' }} />
setForm(f=>({...f,url:e.target.value}))} placeholder="https://…" style={{ width:'100%' }} />
Solo URLs públicas. Las direcciones internas/metadata se rechazan por seguridad.
{busy==='create'?'Añadiendo…':'Añadir integración'} setForm(null)}>Cancelar
)} {/* catalog */}
Disponibles
{_INT_CATALOG.map((x,i)=>{ const soon = !x.kind; return (
{ if(!soon){e.currentTarget.style.borderColor=x.c;e.currentTarget.style.transform='translateY(-2px)';} }} onMouseLeave={e=>{e.currentTarget.style.borderColor='var(--line)';e.currentTarget.style.transform='none';}}>
{x.n[0]}
{soon && PRÓXIMAMENTE}
{x.n}
{x.d}
);})}
); } function Settings({ initialTab }) { const tabs = [ { id:'workspace', label:'Workspace', icon:'folder' }, { id:'vpn', label:'Perfiles VPN', icon:'lock' }, { id:'notif', label:'Notificaciones', icon:'alert' }, { id:'wordlists', label:'Wordlists', icon:'hash' }, { id:'license', label:'Licencia', icon:'cert' }, ]; const [tab, setTab] = useStateL(initialTab || 'workspace'); useEffectL(()=>{ if(initialTab) setTab(initialTab); }, [initialTab]); return (
{tabs.map(t => ( ))}
{tab==='workspace' && } {tab==='vpn' && } {tab==='notif' && } {tab==='wordlists' && } {tab==='license' && }
); } function WorkspacePanel() { const [u, setU] = useStateL(null); // {plan,name,email,tenant_name,role,assets,scans_month,reports} useEffectL(() => { let alive = true; window.fsApi('/api/account/usage', { method:'GET' }) .then(d => { if(alive) setU(d); }) .catch(() => { if(alive) setU('na'); }); return () => { alive = false; }; }, []); const ok = u && u !== 'na'; const email = ok ? (u.email || '') : ''; const display = email ? email.split('@')[0] : '—'; const initial = (String(display||'?').trim()[0] || '?').toUpperCase(); const planName = ok ? (u.name || u.plan || '—') : '—'; const role = ok ? (u.role || 'owner') : ''; const roleLabel = role === 'owner' ? 'Propietario' : role === 'admin' ? 'Administrador' : 'Operador'; return (
Cuenta del operador
{initial}
{display}
{email || '—'}
Límites del plan · {planName}
); } function VpnPanel() { const [profiles, setProfiles] = useStateL(null); // null = cargando const [busy, setBusy] = useStateL(false); const [err, setErr] = useStateL(''); const fileRef = useRefL(null); const load = () => window.fsApi('/api/vpn/profiles', { method:'GET' }) .then(d => setProfiles((d&&d.profiles)||[])).catch(()=>setProfiles([])); useEffectL(() => { load(); }, []); const onFile = async (e) => { const file = e.target.files && e.target.files[0]; if (!file) return; e.target.value=''; setBusy(true); setErr(''); try { const fd = new FormData(); fd.append('file', file); fd.append('name', file.name); await window.fsApiForm('/api/vpn/profiles', fd); load(); } catch (e) { setErr(e.message || 'No se pudo subir el perfil.'); } setBusy(false); }; const del = async (p) => { if (!window.confirm('¿Borrar el perfil «'+p.name+'»?')) return; try { await window.fsApi('/api/vpn/profiles/'+p.id, { method:'DELETE' }); load(); } catch (e) { setErr(e.message || 'No se pudo borrar.'); } }; const list = profiles || []; return (
Perfiles VPN
Sube y gestiona tus perfiles OpenVPN para escaneo de redes internas. El túnel en vivo llegará próximamente.
{ if(!busy && fileRef.current) fileRef.current.click(); }}>{busy?'Subiendo…':'Subir .ovpn'}
{err &&
{err}
}
{profiles === null && } {profiles !== null && list.length === 0 && } {list.map((p)=>( ))}
PerfilRemoteTamañoEstado
Cargando…
No hay perfiles. Sube un fichero .ovpn.
{p.name} {p.region} {wlFmtBytes(p.size)} Próximamente del(p)} style={{ color:'var(--danger)' }}>Borrar
); } function NotifPanel() { const [rules, setRules] = useStateL(null); // null = cargando const [channels, setChannels] = useStateL(null); const [saving, setSaving] = useStateL(false); const [err, setErr] = useStateL(''); const items = [ ['crit','Hallazgo crítico detectado','Avisa al instante ante cualquier severidad crítica.'], ['high','Hallazgo de severidad alta',''], ['scanDone','Escaneo completado',''], ['newAsset','Nuevo activo en la superficie de ataque',''], ['ssl','Certificado SSL próximo a caducar','Aviso con 30 días de antelación.'], ['weekly','Resumen semanal de postura',''], ]; useEffectL(() => { window.fsApi('/api/notifications/rules', { method:'GET' }).then(d => setRules(d.rules||{})).catch(()=>setRules({})); window.fsApi('/api/integrations', { method:'GET' }).then(d => setChannels((d&&d.integrations)||[])).catch(()=>setChannels([])); }, []); const toggle = async (k) => { if (!rules) return; const next = { ...rules, [k]: !rules[k] }; setRules(next); setSaving(true); setErr(''); try { const d = await window.fsApi('/api/notifications/rules', { method:'PUT', body:{ rules:{ [k]: next[k] } } }); setRules(d.rules); } catch (e) { setErr(e.message||'No se pudo guardar.'); setRules(r=>({ ...r, [k]: !next[k] })); } // revertir setSaving(false); }; return (
Canales de notificación
{channels === null ?
Cargando…
: channels.length === 0 ?
No hay canales configurados. Añade uno en Integraciones (Slack, Teams, Discord o Webhook) y las alertas se enviarán ahí.
:
{channels.map((c,i)=>(
{(_KIND_LABEL[c.kind]||c.kind)} · {c.name} {c.enabled ? Activo : Pausado}
))}
}
Reglas de alerta
{saving && guardando…}
{err &&
{err}
}
{items.map(([k,label,desc])=>(
{label}
{desc &&
{desc}
}
toggle(k)} />
))}
); } function wlFmtBytes(n) { if (n == null) return '—'; if (n < 1024) return n + ' B'; if (n < 1024*1024) return (n/1024).toFixed(1) + ' K'; return (n/(1024*1024)).toFixed(1) + ' M'; } function WordlistsPanel() { const [lists, setLists] = useStateL([]); const [cats, setCats] = useStateL([]); const [loading, setLoading] = useStateL(true); const [upCat, setUpCat] = useStateL('url-fuzzer'); const [busy, setBusy] = useStateL(false); const [err, setErr] = useStateL(''); const fileRef = useRefL(null); const load = () => { setLoading(true); window.fsApi('/api/wordlists', { method:'GET' }) .then(d => { setLists((d && d.wordlists) || []); setCats((d && d.categories) || []); }) .catch(() => setErr('No se pudieron cargar las wordlists.')) .finally(() => setLoading(false)); }; useEffectL(() => { load(); }, []); const catLabel = (id) => { const c = cats.find(c => c.id === id); return c ? c.label : id; }; const onFile = async (e) => { const file = e.target.files && e.target.files[0]; if (file) { e.target.value = ''; await doUpload(file); } }; const doUpload = async (file) => { setBusy(true); setErr(''); try { const fd = new FormData(); fd.append('file', file); fd.append('category', upCat); fd.append('name', file.name); await window.fsApiForm('/api/wordlists', fd); load(); } catch (e) { setErr(e.message || 'Error al subir el fichero.'); } setBusy(false); }; const del = async (id) => { if (!window.confirm('¿Borrar esta wordlist?')) return; try { await window.fsApi('/api/wordlists/' + encodeURIComponent(id), { method:'DELETE' }); load(); } catch (e) { setErr(e.message || 'No se pudo borrar.'); } }; return (
Wordlists
Diccionarios para fuzzing, subdominios, vhosts, APIs y credenciales. Catalógalos por uso y aparecerán en el escáner correspondiente.
{ if (!busy && fileRef.current) fileRef.current.click(); }}>{busy ? 'Subiendo…' : 'Subir wordlist'}
{err &&
{err}
}
{loading && } {!loading && lists.length === 0 && } {lists.map((l) => ( ))}
NombreCategoríaEntradasTamaño
Cargando…
No hay wordlists.
{l.name} {l.builtin ? INCORPORADA : SUBIDA} {catLabel(l.category)} {l.entries != null ? l.entries.toLocaleString('es') : '—'} {wlFmtBytes(l.bytes)} Descargar {!l.builtin && del(l.id)} style={{ color:'var(--danger)' }}>Borrar}
); } const PLANS = [ { id:'pentester', name:'Pentester', theme:'red', sub:'Escáneres ligeros · Red Team', tag:'Reconocimiento y escáneres livianos para auditar tu superficie de ataque.', m:'19,99', y:'16,66', feats:['Escaneo de puertos y servicios','Análisis TLS/SSL','DNS, WHOIS y subdominios','Reconocimiento y fingerprint web','Detección de WAF','Informes PDF y biblioteca'] }, { id:'purple', name:'Purple Team', theme:'fs', popular:true, plus:'Pentester', sub:'Red Team + Blue Team', tag:'Ofensiva con más profundidad y hardening defensivo con OpenSCAP.', m:'29,99', y:'24,99', feats:['Escaneo web de vulnerabilidades (nuclei · CVEs)','WordPress scanner (WPScan)','Fuzzing de rutas y vhosts','Módulo Blue Team: hardening CIS','Remediación guiada por bloques','Escaneo remoto por SSH'] }, { id:'security', name:'Security Team', theme:'amber', plus:'Purple Team', sub:'Red + Blue + Normativa', tag:'Suite completa: ofensiva, defensiva y cumplimiento normativo.', m:'39,99', y:'33,32', feats:['Módulo Normativa: RGPD / LOPDGDD','Cookies, CMP y terceros (Playwright)','Evidencias y capturas de pantalla','Informes ejecutivos de cumplimiento','Soporte prioritario'] }, ]; function LicensePanel() { const [yearly, setYearly] = useStateL(false); const plan = usePlan(); const current = plan ? plan.get() : 'pentester'; return (
Cambia tu plan

Elige lo que mejor encaje con tu misión

PLAN ACTUAL · {(plan?plan.name():'Pentester').toUpperCase()}
{/* monthly / yearly toggle */}
{PLANS.map(p => (
{p.name}
{current===p.id ? PLAN ACTUAL : p.popular && POPULAR}
{p.tag}
{yearly?p.y:p.m} € /mes
{p.sub}{yearly?' · facturado anual':''}
{p.plus &&
Todo en {p.plus}, más:
}
{p.feats.map((f,i)=>(
{f}
))}
))}
Incluido en todos los planes
{['Acceso a la API real','Re-escaneos ilimitados','Histórico de escaneos','Biblioteca de informes PDF','Exporta evidencias y resultados'].map((f,i)=>(
{f}
))}
Complementos opcionales
{['Escaneo de red interno','Informes y correos con tu marca','Bolsa extra de activos','Soporte prioritario 24/7'].map((f,i)=>( {f} ))}
¿Necesitas un plan a medida?
Volumen, on-premise o requisitos específicos de cumplimiento.
{ window.location.href='mailto:ventas@flowia.es?subject='+encodeURIComponent('Plan a medida FlowShield'); }}>Contactar con ventas
); } function SetRow({ k, v, ok }) { return
{k}{v}
; } function TokensMeter() { const [u, setU] = useStateL(null); // {used, limit, period} | 'na' | null useEffectL(() => { let alive = true; window.fsApi('/api/assistant/usage', { method:'GET' }) .then(d => { if(alive) setU(d); }) .catch(() => { if(alive) setU('na'); }); return () => { alive = false; }; }, []); if (u === null) return null; if (u === 'na' || !u.limit) { return (
Tokens IA No incluido en tu plan
); } const pct = Math.min(100, Math.round((u.used / u.limit) * 100)); const full = u.used >= u.limit; const fmt = (n) => n.toLocaleString('es-ES'); return (
Tokens IA · {u.period} {fmt(u.used)} / {fmt(u.limit)}
); } function LimitMeter({ label, v, max }) { return (
{label}{v}/{max}
); } function ChipCSS(){ return ; } Object.assign(window, { Findings, Scans, Reports, Surface, Handlers, Integrations, Settings });