@openguardrails/moltguard
Version:
AI agent security plugin for OpenClaw: prompt injection detection, PII sanitization, and monitoring dashboard
194 lines • 6.46 kB
JavaScript
/**
* 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