/*! * SPDX-License-Identifier: AGPL-3.0-or-later * Copyright © 2026 Diego Lima Nogueira de Paula * * /graph-security — Security audit: OWASP, npm audit, secrets, dependencies. * No MCP dependency. Operates directly against SqliteStore + FS. */ import type { SkillHandlerPort, SkillExecutionContext } from "../../tui/skill-handler-port.js"; import { fmtElapsed } from "../shared/handler-utils.js"; import { createLogger } from "../../core/utils/logger.js"; const _log = createLogger({ layer: "core", source: "graph-security.ts" }); export class GraphSecurityHandler implements SkillHandlerPort { async execute(args: string, ctx: SkillExecutionContext): Promise { const { store, dir, onProgress } = ctx; const startMs = Date.now(); const lines: string[] = ["═ /graph-security ═"]; // Step 1: npm audit onProgress({ step: 1, total: 3, label: "Rodando npm audit...", elapsedMs: Date.now() - startMs, tokensUsed: 0 }); try { const { execSync } = await import("node:child_process"); const output = execSync("npm audit --json 2>/dev/null || true", { timeout: 30000, cwd: dir }); try { const audit = JSON.parse(output.toString()); const vulns = audit.vulnerabilities ?? {}; const total = Object.values(vulns as Record).length; const critical = Object.values(vulns as Record).filter((v) => v.severity === "critical").length; const high = Object.values(vulns as Record).filter((v) => v.severity === "high").length; lines.push(`npm audit: ${total} vulnerabilidades · ${critical} críticas · ${high} altas`); if (critical > 0 || high > 0) { lines.push(" ⚠ Execute 'npm audit fix' para corrigir"); } else { lines.push(" ✓ Sem vulnerabilidades críticas/altas"); } } catch { lines.push(" npm audit: formato JSON não parseável"); } } catch { lines.push(" npm audit: indisponível"); } // Step 2: Secrets scan (basic) onProgress({ step: 2, total: 3, label: "Escaneando segredos no código...", elapsedMs: Date.now() - startMs, tokensUsed: 0 }); try { const { execSync } = await import("node:child_process"); const grepOut = execSync( `rg -l --include "*.ts" --include "*.js" --include "*.json" -e "(?i)(api.?key|secret|password|token|auth.*token)\\s*[:=]\\s*['"][^'"]+['"]" src/ 2>/dev/null || true`, { timeout: 15000, cwd: dir }, ); const matches = grepOut.toString().trim().split("\n").filter(Boolean); if (matches.length > 0) { lines.push(`⚠ Possíveis segredos em ${matches.length} arquivo(s):`); for (const m of matches.slice(0, 5)) lines.push(` • ${m}`); } else { lines.push("✓ Nenhum segredo óbvio detectado"); } } catch { lines.push(" Scan de segredos: indisponível (rg necessário)"); } // Step 3: Dependency check from graph onProgress({ step: 3, total: 3, label: "Verificando dependências do grafo...", elapsedMs: Date.now() - startMs, tokensUsed: 0 }); const deps = store.getAllEdges().filter((e) => e.relationType === "depends_on"); const brokenDeps = deps.filter((d) => !store.getNodeById(d.to)); if (brokenDeps.length > 0) { lines.push(`⚠ ${brokenDeps.length} dependência(s) quebrada(s) no grafo`); } else { lines.push("✓ Dependências do grafo consistentes"); } lines.push(`═ ${fmtElapsed(Date.now() - startMs)} ═`); return lines.join("\n"); } }