{ "entries": [ { "id": "foxguard.scanner", "version": "0.13.2", "manifest": { "$schema": "https://raw.githubusercontent.com/0sec-labs/hackstore/main/hackstore-manifest.schema.json", "id": "foxguard.scanner", "name": "Foxguard", "version": "0.13.2", "minCoreVersion": "0.16.0", "tools": [ { "name": "foxguard_scan", "description": "Run the installed Foxguard CLI on an absolute file or directory path. Returns finding counts and locations, rule IDs, CWEs, and descriptions. Requires foxguard on PATH. Scan results depend on the installed scanner version and configuration.", "parameters": { "path": { "type": "string", "description": "Absolute file or directory path you have permission to scan." }, "severity": { "type": "string", "enum": [ "critical", "high", "medium", "low" ], "description": "Only return findings at or above this severity." } }, "required": [ "path" ], "capabilities": [ "process-exec", "filesystem-read" ] } ] }, "source": { "kind": "inline", "files": { "plugin.js": "// Foxguard adapter for the 0sec v1 newline-delimited JSON protocol.\n\"use strict\";\nconst { spawn } = require(\"node:child_process\");\nconst { readFileSync } = require(\"node:fs\");\nconst { isAbsolute, join } = require(\"node:path\");\nconst readline = require(\"node:readline\");\n\nconst manifest = JSON.parse(readFileSync(join(__dirname, \"plugin.json\"), \"utf8\"));\nconst MAX_OUTPUT_BYTES = 8 * 1024 * 1024;\nconst MAX_RESULT_CHARS = 90_000;\nconst SCAN_TIMEOUT_MS = 25_000;\nconst active = new Set();\nconst severities = [\"low\", \"medium\", \"high\", \"critical\"];\n\nfunction send(frame) {\n process.stdout.write(JSON.stringify({ v: 1, ...frame }) + \"\\n\");\n}\n\nfunction formatReport(data) {\n if (!data || !Array.isArray(data.findings)) {\n throw new Error(\"Foxguard returned JSON without a findings array.\");\n }\n const counts = { critical: 0, high: 0, medium: 0, low: 0 };\n const findings = data.findings.map((finding) => {\n if (!finding || !severities.includes(finding.severity) ||\n typeof finding.file !== \"string\" || !Number.isInteger(finding.line) ||\n typeof finding.rule_id !== \"string\" || typeof finding.description !== \"string\") {\n throw new Error(\"Foxguard returned an invalid finding.\");\n }\n counts[finding.severity]++;\n return {\n file: finding.file, line: finding.line, severity: finding.severity,\n rule: finding.rule_id, cwe: finding.cwe, description: finding.description,\n };\n });\n const result = {\n scannerVersion: data.scanner?.version ?? null,\n totalFindings: findings.length,\n counts,\n findings: [],\n omittedFindings: findings.length,\n };\n let size = JSON.stringify(result).length;\n for (const finding of findings) {\n const added = JSON.stringify(finding).length + 1;\n if (size + added > MAX_RESULT_CHARS) break;\n result.findings.push(finding);\n result.omittedFindings--;\n size += added;\n }\n return { content: JSON.stringify(result), truncated: result.omittedFindings > 0 };\n}\n\nasync function runFoxguard(args) {\n if (!args || typeof args.path !== \"string\" || !isAbsolute(args.path) || args.path.includes(\"\\0\")) {\n throw new Error(\"path must be an absolute file or directory path you have permission to scan.\");\n }\n if (args.severity !== undefined && !severities.includes(args.severity)) {\n throw new Error(\"severity must be low, medium, high, or critical.\");\n }\n const argv = [\"--format\", \"json\"];\n if (args.severity !== undefined) argv.push(\"--severity\", args.severity);\n argv.push(\"--\", args.path);\n\n return new Promise((resolve, reject) => {\n const child = spawn(\"foxguard\", argv, { stdio: [\"ignore\", \"pipe\", \"pipe\"], shell: false });\n active.add(child);\n let output = \"\";\n let bytes = 0;\n let stderr = \"\";\n let failure;\n const stop = (reason) => {\n failure ??= new Error(reason);\n child.kill(\"SIGKILL\");\n };\n const timer = setTimeout(() => stop(\"Foxguard exceeded the 25-second scan limit. Scan a smaller path.\"), SCAN_TIMEOUT_MS);\n child.stdout.setEncoding(\"utf8\");\n child.stderr.setEncoding(\"utf8\");\n child.stdout.on(\"data\", (chunk) => {\n bytes += Buffer.byteLength(chunk);\n if (bytes > MAX_OUTPUT_BYTES) stop(\"Foxguard output exceeded 8 MiB. Scan a smaller path or raise the severity threshold.\");\n else if (!failure) output += chunk;\n });\n child.stderr.on(\"data\", (chunk) => { stderr += chunk.slice(0, Math.max(0, 2000 - stderr.length)); });\n child.on(\"error\", (error) => {\n failure = new Error(error.code === \"ENOENT\"\n ? \"foxguard is not on PATH. Install the Foxguard CLI before running this extension.\"\n : `Could not launch Foxguard: ${error.message}`);\n });\n child.on(\"close\", (code, signal) => {\n clearTimeout(timer);\n active.delete(child);\n if (failure) return reject(failure);\n // Exit 1 means findings, while exit 2 or a signal means the scan failed.\n if (signal || (code !== 0 && code !== 1)) {\n return reject(new Error(`Foxguard failed (${signal ?? `exit ${code}`}): ${stderr.trim()}`));\n }\n try {\n resolve(formatReport(JSON.parse(output)));\n } catch (error) {\n reject(new Error(`Could not read Foxguard results: ${error.message}`));\n }\n });\n });\n}\n\nsend({ kind: \"handshake\", pluginId: manifest.id, version: manifest.version, manifest });\nconst input = readline.createInterface({ input: process.stdin, terminal: false });\ninput.on(\"line\", async (line) => {\n let message;\n try { message = JSON.parse(line); } catch { return; }\n if (!message || message.v !== 1 || typeof message.id !== \"string\") return;\n if (message.kind === \"list_tools\") {\n send({ kind: \"list_tools\", id: message.id, tools: manifest.tools });\n } else if (message.kind === \"call_tool\") {\n try {\n if (message.tool !== \"foxguard_scan\") throw new Error(\"Unknown tool.\");\n const result = await runFoxguard(message.args);\n send({ kind: \"tool_result\", id: message.id, ok: true, ...result });\n } catch (error) {\n send({ kind: \"tool_result\", id: message.id, ok: false, content: error.message, truncated: false });\n }\n }\n});\nfunction shutdown() {\n for (const child of active) child.kill(\"SIGKILL\");\n}\ninput.on(\"close\", shutdown);\nprocess.on(\"SIGTERM\", () => { shutdown(); process.exit(0); });\nprocess.on(\"SIGINT\", () => { shutdown(); process.exit(0); });\n" } } } ] }