UNPKG

@openguardrails/moltguard

Version:

AI agent security plugin for OpenClaw: prompt injection detection, PII sanitization, and monitoring dashboard

194 lines 6.46 kB
/** * Simple JSONL file store for analysis logging * (No dependencies - just append-only log files) */ import fs from "node:fs"; import path from "node:path"; // ============================================================================= // Helpers // ============================================================================= function readLines(filePath) { if (!fs.existsSync(filePath)) return []; const content = fs.readFileSync(filePath, "utf-8"); const results = []; for (const line of content.split("\n")) { if (!line.trim()) continue; try { results.push(JSON.parse(line)); } catch { // skip malformed lines } } return results; } function getMaxId(rows) { let max = 0; for (const row of rows) { if (row.id > max) max = row.id; } return max; } // ============================================================================= // Store Class // ============================================================================= export class AnalysisStore { analysisFile; feedbackFile; nextAnalysisId; nextFeedbackId; log; constructor(logPath, log) { this.log = log; // Ensure directory exists if (!fs.existsSync(logPath)) { fs.mkdirSync(logPath, { recursive: true }); } this.analysisFile = path.join(logPath, "moltguard-analyses.jsonl"); this.feedbackFile = path.join(logPath, "moltguard-feedback.jsonl"); // Read existing data to determine next IDs const analyses = readLines(this.analysisFile); const feedback = readLines(this.feedbackFile); this.nextAnalysisId = getMaxId(analyses) + 1; this.nextFeedbackId = getMaxId(feedback) + 1; this.log.info(`Analysis store initialized at ${logPath}`); } /** * Log an analysis result */ logAnalysis(entry) { const id = this.nextAnalysisId++; const row = { id, timestamp: new Date().toISOString(), targetType: entry.targetType, contentLength: entry.contentLength, chunksAnalyzed: entry.chunksAnalyzed, verdict: entry.verdict, durationMs: entry.durationMs, blocked: entry.blocked, }; fs.appendFileSync(this.analysisFile, JSON.stringify(row) + "\n", "utf-8"); return id; } /** * Get recent analysis logs */ getRecentLogs(limit = 20) { const rows = readLines(this.analysisFile); rows.sort((a, b) => (b.timestamp > a.timestamp ? 1 : b.timestamp < a.timestamp ? -1 : 0)); return rows.slice(0, limit); } /** * Get count of blocked analyses in time window */ getBlockedCount(windowHours = 24) { const windowStart = new Date(); windowStart.setHours(windowStart.getHours() - windowHours); const cutoff = windowStart.toISOString(); const rows = readLines(this.analysisFile); let count = 0; for (const row of rows) { if (row.blocked && row.timestamp >= cutoff) count++; } return count; } /** * Get statistics */ getStats() { const rows = readLines(this.analysisFile); let totalBlocked = 0; let totalDuration = 0; for (const row of rows) { if (row.blocked) totalBlocked++; totalDuration += row.durationMs; } return { totalAnalyses: rows.length, totalBlocked, blockedLast24h: this.getBlockedCount(24), avgDurationMs: rows.length > 0 ? Math.round(totalDuration / rows.length) : 0, }; } /** * Get recent detections (only those flagged as injection) */ getRecentDetections(limit = 10) { const rows = readLines(this.analysisFile); const detections = rows.filter((r) => r.verdict.isInjection); detections.sort((a, b) => (b.timestamp > a.timestamp ? 1 : b.timestamp < a.timestamp ? -1 : 0)); return detections.slice(0, limit); } /** * Log user feedback (false positive or missed detection) */ logFeedback(entry) { const id = this.nextFeedbackId++; const row = { id, timestamp: new Date().toISOString(), analysisId: entry.analysisId ?? null, feedbackType: entry.feedbackType, reason: entry.reason ?? null, }; fs.appendFileSync(this.feedbackFile, JSON.stringify(row) + "\n", "utf-8"); return id; } /** * Get feedback statistics */ getFeedbackStats() { const rows = readLines(this.feedbackFile); let falsePositives = 0; let missedDetections = 0; for (const row of rows) { if (row.feedbackType === "false_positive") falsePositives++; if (row.feedbackType === "missed_detection") missedDetections++; } return { falsePositives, missedDetections }; } // ─── Tool Call Observations (JSONL fallback) ──────────────────── get toolCallFile() { return path.join(path.dirname(this.analysisFile), "moltguard-tool-calls.jsonl"); } /** * Log a tool call observation locally (fallback when dashboard is unreachable) */ logToolCall(entry) { const row = { timestamp: new Date().toISOString(), ...entry, }; fs.appendFileSync(this.toolCallFile, JSON.stringify(row) + "\n", "utf-8"); } /** * Get recent tool call observations from local log */ getRecentToolCalls(limit = 50) { const rows = readLines(this.toolCallFile); rows.sort((a, b) => { const ta = a.timestamp || ""; const tb = b.timestamp || ""; return tb > ta ? 1 : tb < ta ? -1 : 0; }); return rows.slice(0, limit); } close() { // No-op for JSONL store (no connection to close) } } // ============================================================================= // Factory // ============================================================================= export function createAnalysisStore(logPath, log) { return new AnalysisStore(logPath, log); } //# sourceMappingURL=store.js.map