/* Módulo DevSecOps (4º pilar): SAST · SCA · secretos · IaC · contenedores · cloud · Pipeline/CI. Reutiliza ToolCard/ScanRunner (redteam.jsx) y los motores de REDTEAM_ENGINES; el catálogo vive en window.DEVSECOPS_TABS. */ function DevSecOpsPipeline() { const [tokens, setTokens] = useStateL(null); const [name, setName] = useStateL(''); const [creating, setCreating] = useStateL(false); const [fresh, setFresh] = useStateL(null); // token en claro recién creado (una vez) const [copied, setCopied] = useStateL(false); const load = () => window.fsApi('/api/ci/tokens', { method: 'GET' }) .then(d => setTokens(d.tokens || [])).catch(() => setTokens([])); useEffectL(() => { load(); }, []); const create = async () => { setCreating(true); setFresh(null); setCopied(false); try { const r = await window.fsApi('/api/ci/tokens', { method: 'POST', body: { name } }); setFresh(r.token); setName(''); load(); } catch (e) { window.alert('No se pudo crear el token.'); } finally { setCreating(false); } }; const revoke = async (id) => { if (!window.confirm('¿Revocar este token? Los pipelines que lo usen dejarán de autenticar.')) return; try { await window.fsApi('/api/ci/tokens/' + id + '/revoke', { method: 'POST', body: {} }); load(); } catch (e) { /* noop */ } }; const copyFresh = () => { try { navigator.clipboard.writeText(fresh); setCopied(true); } catch (e) { /* noop */ } }; const ACTION_YML = `name: FlowShield DevSecOps on: [push, pull_request] jobs: security-gate: runs-on: ubuntu-latest steps: - name: FlowShield gate env: FLOWSHIELD_URL: \${{ secrets.FLOWSHIELD_URL }} FLOWSHIELD_TOKEN: \${{ secrets.FLOWSHIELD_TOKEN }} run: | curl -sf -X POST "$FLOWSHIELD_URL/api/ci/scan" \\ -H "Authorization: Bearer $FLOWSHIELD_TOKEN" \\ -H "Content-Type: application/json" \\ -d "{\\"repo_url\\":\\"$GITHUB_SERVER_URL/$GITHUB_REPOSITORY\\",\\"branch\\":\\"$GITHUB_REF_NAME\\",\\"commit\\":\\"$GITHUB_SHA\\",\\"fail_on\\":\\"high\\",\\"include_sarif\\":true}" \\ -o fs.json python -c "import json,sys;d=json.load(open('fs.json'));print(d['gate']);sys.exit(0 if d['gate']['passed'] else 1)"`; const box = { background: 'var(--bg-void)', border: '1px solid var(--line)', borderRadius: 8 }; return (
Tokens de API (CI/CD)
Un token de máquina autentica tu pipeline contra POST /api/ci/scan. Se muestra una sola vez; guárdalo como secreto en tu CI. Revócalo cuando quieras.
{fresh && (
Token creado · cópialo ahora
{fresh} {copied ? 'Copiado' : 'Copiar'}
)}
setName(e.target.value)} style={{ flex: 1, minWidth: 220, background: 'var(--bg-panel)' }} /> {creating ? 'Creando…' : 'Crear token'}
{tokens === null && } {tokens !== null && tokens.length === 0 && } {(tokens || []).map(t => ( ))}
NombrePrefijoCreadoÚltimo uso
Cargando…
Sin tokens todavía.
{t.name || '—'} {t.prefix} {window.rptDate ? window.rptDate(t.created_at) : (t.created_at || '').slice(0, 10)} {t.last_used_at ? (window.rptDate ? window.rptDate(t.last_used_at) : t.last_used_at.slice(0, 10)) : '—'} {t.revoked ? revocado : revoke(t.id)}>Revocar}
GitHub Actions · gate de seguridad
Define los secretos FLOWSHIELD_URL y FLOWSHIELD_TOKEN en tu repo. El workflow falla el build (exit 1) si el gate no pasa (fail_on: high por defecto).
{ACTION_YML}
Política del gate
fail_on: critical · high (def.) · medium · low · never (solo informe). Herramientas del gate (campo tools): semgrep, gitleaks, trivy (def.), y opcionalmente osv, checkov, bandit, gosec, njsscan. Añade include_sarif:true / include_junit:true para subir resultados a GitHub code scanning / GitLab. Los hallazgos se registran en Hallazgos e Informes con su contexto de commit/rama.
); } /* Centro de operaciones DevSecOps: postura consolidada del ciclo de desarrollo (SAST/SCA/IaC/contenedores/cloud). Comparte el motor de dashboard con Red Team vía el módulo virtual `devsecops` (acota los escaneos DevSecOps del store). */ function DevSecOpsDashboard({ onOpenTools }) { const [d, setD] = useStateL(null); const [err, setErr] = useStateL(false); const [drawer, setDrawer] = useStateL(null); const load = () => { setErr(false); window.fsApi('/api/results/dashboard?module=devsecops', { method: 'GET' }).then(setD).catch(() => setErr(true)); }; useEffectL(() => { load(); }, []); const openFinding = (id) => setDrawer({ mode: 'detail', id }); const openScan = (id, label) => setDrawer({ mode: 'scan', id, label }); const openTarget = (target, label) => setDrawer({ mode: 'target', target, label }); const openSource = (src, label) => setDrawer({ mode: 'list', filter: { source: src }, label }); const header = ( ActualizarNuevo escaneo} /> ); if (err) return
{header}
No se pudo cargar el panel.
; if (!d) return
{header}
Cargando…
; const k = d.kpis || {}; const sev = d.sev || { crit: 0, high: 0, med: 0, low: 0 }; const stt = d.status || { open: 0, triage: 0, fixed: 0 }; const maxSev = Math.max(1, sev.crit || 0, sev.high || 0, sev.med || 0, sev.low || 0); const tl = d.timeline || []; const tlMax = Math.max(1, ...tl.map(x => x.count || 0)); const tools = d.by_tool || []; const toolMax = Math.max(1, ...tools.map(x => x.count || 0)); const targets = d.by_target || []; const tgtMax = Math.max(1, ...targets.map(x => x.count || 0)); const total = k.total_findings || 0; const exposure = Math.min(100, Math.round(((sev.crit * 100) + (sev.high * 45) + (sev.med * 12) + (sev.low * 3)) / Math.max(1, total))); const empty = (k.scans || 0) === 0; return (
{header} {empty && }
Resumen de hallazgos
{total} hallazgos
{[['crit', 'Críticos'], ['high', 'Altos'], ['med', 'Medios'], ['low', 'Bajos']].map(([kk, l]) => { const n = sev[kk] || 0; return (
{l}
{n}
); })}
Exposición y estado
Actividad de hallazgos · últimos 14 días
{tl.map((x, i) => { const h = Math.round((x.count / tlMax) * 100); const ch = x.count ? Math.round((x.crit / x.count) * h) : 0; return (
{ch > 0 &&
}
{(x.date || '').slice(5)}
); })}
Hallazgos por herramienta
{tools.length === 0 && Sin datos.} {tools.map((t, i) => ( openSource(t.tool, 'Herramienta · ' + t.tool)} /> ))}
Repos / imágenes más expuestos
{targets.length === 0 && Sin datos.} {targets.map((t, i) => (
openTarget(t.target, 'Objetivo · ' + t.target)} style={{ display: 'flex', alignItems: 'center', gap: 10, cursor: 'pointer' }}> {t.target}
{t.count}
))}
Hallazgos prioritarios
{(d.recent_findings || []).length === 0 && } {(d.recent_findings || []).map(f => ( openFinding(f.id)} style={{ cursor: 'pointer' }}> ))}
IDHallazgoObjetivoSeveridadCVSSOrigen
Sin hallazgos todavía.
{f.code || f.id} {f.name} {f.target} {f.cvss} {f.source}
Escaneos recientes
{(d.recent_scans || []).length === 0 && } {(d.recent_scans || []).map(s => ( openScan(s.id, 'Escaneo · ' + (s.code || s.id))} style={{ cursor: 'pointer' }}> ))}
IDHerramientaObjetivoEstadoHallazgosFecha
Aún no hay escaneos.
{s.code || s.id} {s.tool} {s.target} {(s.found && (s.found.crit || s.found.high || s.found.med || s.found.low)) ?
{['crit', 'high', 'med', 'low'].map(kk => s.found[kk] ? {s.found[kk]} : null)}
: }
{window.rptDate ? window.rptDate(s.date) : s.date}
{drawer && setDrawer(null)} />}
); } function DevSecOps() { window.usePlan && window.usePlan(); const [tab, setTab] = useStateL('board'); const [q, setQ] = useStateL(''); const [active, setActive] = useStateL(null); if (active) return (
setActive(null)} />
); const tabs = window.DEVSECOPS_TABS || []; const current = tabs.find(t => t.id === tab) || null; const filterTools = (tools) => q ? tools.filter(t => (t.name + t.desc).toLowerCase().includes(q.toLowerCase())) : tools; const tabbar = (
{tabs.map(t => ( ))}
); if (tab === 'board') return (
{tabbar} setTab((tabs[0] && tabs[0].id) || 'code')} />
); return (
{tabbar} {(current || {}).pipeline ? : ( <>
setQ(e.target.value)} style={{ padding: '13px 14px 13px 40px', background: 'var(--bg-panel)' }} />
{((current && current.sections) || []).map((sec, si) => { const tools = filterTools(sec.tools); if (!tools.length) return null; return (
{sec.title}
{tools.map(t => setActive(t)} />)}
); })}
)}
); } Object.assign(window, { DevSecOps });