@openguardrails/moltguard
Version:
AI agent security plugin for OpenClaw: prompt injection detection, PII sanitization, and monitoring dashboard
228 lines • 7.76 kB
JavaScript
/**
* AI Security Gateway - Content Sanitizer
*
* Sanitizes sensitive data in a single request-response cycle.
* Placeholder format: __PII_<ENTITY_TYPE>_<SERIAL_ID>__
*/
// =============================================================================
// Detection Patterns
// =============================================================================
const ENTITY_PATTERNS = [
// PEM Private Keys
{
type: "PRIVATE_KEY",
pattern: /-----BEGIN (?:OPENSSH |RSA |EC |DSA )?PRIVATE KEY-----[\s\S]*?-----END (?:OPENSSH |RSA |EC |DSA )?PRIVATE KEY-----/g,
score: 0.95,
},
// Email addresses
{
type: "EMAIL_ADDRESS",
pattern: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g,
score: 0.90,
},
// URLs
{
type: "URL_ADDRESS",
pattern: /https?:\/\/[A-Za-z0-9._~:/?#\[\]@!$&'()*+,;=%-]+/g,
score: 0.80,
},
// Known API key prefixes
{
type: "API_KEY",
pattern: /\b(?:sk-[A-Za-z0-9]{20,}|sk_(?:live|test)_[A-Za-z0-9]{20,}|pk_(?:live|test)_[A-Za-z0-9]{20,}|ghp_[A-Za-z0-9]{36,}|gho_[A-Za-z0-9]{36,}|github_pat_[A-Za-z0-9_]{22,}|AKIA[A-Z0-9]{16}|xox[baprs]-[A-Za-z0-9-]+|SG\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+|hf_[A-Za-z0-9]{30,})\b/g,
score: 0.90,
},
// Bearer tokens
{
type: "API_KEY",
pattern: /Bearer\s+[A-Za-z0-9\-_.~+/]{20,}={0,3}/g,
score: 0.85,
},
// Hex private keys (64 hex chars)
{
type: "PRIVATE_KEY",
pattern: /\b[0-9a-fA-F]{64}\b/g,
score: 0.75,
},
// Labeled password patterns
{
type: "PASSWORD",
pattern: /(?:password|passwd|pwd|pass|passcode)\s*[:=]\s*["']?(\S+)["']?/gi,
score: 0.80,
captureGroup: 1,
},
// Labeled API key patterns
{
type: "API_KEY",
pattern: /(?:api[_-]?key|apikey|secret[_-]?key|access[_-]?token|auth[_-]?token)\s*[:=]\s*["']?([A-Za-z0-9\-_.~+/]{16,})["']?/gi,
score: 0.85,
captureGroup: 1,
},
// Phone numbers
{
type: "PHONE_NUMBER",
pattern: /\+?\d{1,3}[-.\s]?\(?\d{2,4}\)?[-.\s]?\d{3,4}[-.\s]?\d{3,4}/g,
score: 0.70,
},
// Credit card numbers
{
type: "CREDIT_CARD",
pattern: /\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/g,
score: 0.85,
},
// Bank account numbers
{
type: "BANK_NUMBER",
pattern: /\b\d{12,19}\b/g,
score: 0.60,
},
// SSN
{
type: "SSN",
pattern: /\b\d{3}-\d{2}-\d{4}\b/g,
score: 0.85,
},
// IP addresses
{
type: "IP_ADDRESS",
pattern: /\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/g,
score: 0.70,
},
// Labeled verification codes
{
type: "VERIFICATION_CODE",
pattern: /(?:verification\s*code|verify\s*code|otp|2fa\s*code|auth(?:entication)?\s*code)\s*[:=\-]?\s*([A-Za-z0-9]{4,12})/gi,
score: 0.80,
captureGroup: 1,
},
];
function collectMatches(content) {
const matches = [];
for (const entity of ENTITY_PATTERNS) {
entity.pattern.lastIndex = 0;
let m;
while ((m = entity.pattern.exec(content)) !== null) {
let matchedText;
let start;
if (entity.captureGroup !== undefined && m[entity.captureGroup]) {
matchedText = m[entity.captureGroup];
start = m.index + m[0].indexOf(matchedText);
}
else {
matchedText = m[0];
start = m.index;
}
matches.push({
originalText: matchedText,
type: entity.type,
score: entity.score,
start,
end: start + matchedText.length,
});
}
}
return matches;
}
// =============================================================================
// Span Merging
// =============================================================================
function mergeSpans(matches) {
if (matches.length === 0)
return [];
matches.sort((a, b) => {
if (a.start !== b.start)
return a.start - b.start;
const lenDiff = (b.end - b.start) - (a.end - a.start);
if (lenDiff !== 0)
return lenDiff;
return b.score - a.score;
});
const merged = [];
let current = matches[0];
for (let i = 1; i < matches.length; i++) {
const next = matches[i];
if (next.start < current.end) {
const currentLen = current.end - current.start;
const nextLen = next.end - next.start;
if (next.score > current.score || (next.score === current.score && nextLen > currentLen)) {
current = next;
}
}
else {
merged.push(current);
current = next;
}
}
merged.push(current);
return merged;
}
// =============================================================================
// Text Sanitization
// =============================================================================
function sanitizeText(text, mappingTable, typeCounters) {
const matches = collectMatches(text);
if (matches.length === 0)
return text;
const merged = mergeSpans(matches);
const textToPlaceholder = new Map();
for (const match of merged) {
if (!textToPlaceholder.has(match.originalText)) {
const counter = (typeCounters.get(match.type) ?? 0) + 1;
typeCounters.set(match.type, counter);
const paddedId = counter.toString().padStart(8, "0");
const placeholder = `__PII_${match.type}_${paddedId}__`;
textToPlaceholder.set(match.originalText, placeholder);
mappingTable.set(placeholder, match.originalText);
}
}
let sanitized = text;
const sortedMatches = [...merged].sort((a, b) => b.start - a.start);
for (const match of sortedMatches) {
const placeholder = textToPlaceholder.get(match.originalText);
sanitized = sanitized.slice(0, match.start) + placeholder + sanitized.slice(match.end);
}
return sanitized;
}
// =============================================================================
// Recursive Sanitization
// =============================================================================
function sanitizeValue(value, mappingTable, typeCounters) {
if (typeof value === "string") {
return sanitizeText(value, mappingTable, typeCounters);
}
if (Array.isArray(value)) {
return value.map((item) => sanitizeValue(item, mappingTable, typeCounters));
}
if (value !== null && typeof value === "object") {
const sanitized = {};
for (const [key, val] of Object.entries(value)) {
sanitized[key] = sanitizeValue(val, mappingTable, typeCounters);
}
return sanitized;
}
return value;
}
// =============================================================================
// Public API
// =============================================================================
/**
* Sanitize any content (messages array, object, string)
* Returns sanitized content and mapping table for restoration
*/
export function sanitize(content) {
const mappingTable = new Map();
const typeCounters = new Map();
const sanitized = sanitizeValue(content, mappingTable, typeCounters);
return {
sanitized,
mappingTable,
redactionCount: mappingTable.size,
};
}
/**
* Sanitize messages array (common case for LLM APIs)
*/
export function sanitizeMessages(messages) {
return sanitize(messages);
}
//# sourceMappingURL=sanitizer.js.map