clawmoat 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +56 -0
- package/LICENSE +21 -0
- package/README.md +199 -0
- package/bin/clawmoat.js +407 -0
- package/docs/CNAME +1 -0
- package/docs/MIT-RISK-GAP-ANALYSIS.md +146 -0
- package/docs/badge/score-A.svg +21 -0
- package/docs/badge/score-Aplus.svg +21 -0
- package/docs/badge/score-B.svg +21 -0
- package/docs/badge/score-C.svg +21 -0
- package/docs/badge/score-D.svg +21 -0
- package/docs/badge/score-F.svg +21 -0
- package/docs/blog/index.html +90 -0
- package/docs/blog/owasp-agentic-ai-top10.html +187 -0
- package/docs/blog/owasp-agentic-ai-top10.md +185 -0
- package/docs/blog/securing-ai-agents.html +194 -0
- package/docs/blog/securing-ai-agents.md +152 -0
- package/docs/compare.html +312 -0
- package/docs/index.html +654 -0
- package/docs/integrations/langchain.html +281 -0
- package/docs/integrations/openai.html +302 -0
- package/docs/integrations/openclaw.html +310 -0
- package/docs/robots.txt +3 -0
- package/docs/sitemap.xml +28 -0
- package/docs/thanks.html +79 -0
- package/package.json +35 -0
- package/server/Dockerfile +7 -0
- package/server/index.js +85 -0
- package/server/package.json +12 -0
- package/skill/SKILL.md +56 -0
- package/src/badge.js +87 -0
- package/src/index.js +316 -0
- package/src/middleware/openclaw.js +133 -0
- package/src/policies/engine.js +180 -0
- package/src/scanners/exfiltration.js +97 -0
- package/src/scanners/jailbreak.js +81 -0
- package/src/scanners/memory-poison.js +68 -0
- package/src/scanners/pii.js +128 -0
- package/src/scanners/prompt-injection.js +138 -0
- package/src/scanners/secrets.js +97 -0
- package/src/scanners/supply-chain.js +155 -0
- package/src/scanners/urls.js +142 -0
- package/src/utils/config.js +137 -0
- package/src/utils/logger.js +109 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClawMoat — Policy Engine
|
|
3
|
+
*
|
|
4
|
+
* Evaluates tool calls against security policies.
|
|
5
|
+
* Returns allow/deny/warn decisions with explanations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const DECISIONS = { allow: 'allow', deny: 'deny', warn: 'warn', review: 'review' };
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Evaluate a tool call against policies
|
|
14
|
+
* @param {string} tool - Tool name (exec, read, write, browser, etc.)
|
|
15
|
+
* @param {object} args - Tool arguments
|
|
16
|
+
* @param {object} policies - Policy config
|
|
17
|
+
* @returns {object} Decision
|
|
18
|
+
*/
|
|
19
|
+
function evaluateToolCall(tool, args, policies) {
|
|
20
|
+
switch (tool) {
|
|
21
|
+
case 'exec': return evaluateExec(args, policies.exec || {});
|
|
22
|
+
case 'read':
|
|
23
|
+
case 'Read': return evaluateFileRead(args, policies.file || {});
|
|
24
|
+
case 'write':
|
|
25
|
+
case 'Write':
|
|
26
|
+
case 'edit':
|
|
27
|
+
case 'Edit': return evaluateFileWrite(args, policies.file || {});
|
|
28
|
+
case 'browser': return evaluateBrowser(args, policies.browser || {});
|
|
29
|
+
case 'message': return evaluateMessage(args, policies.message || {});
|
|
30
|
+
default: return { decision: DECISIONS.allow, tool, reason: 'No policy defined' };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function evaluateExec(args, policy) {
|
|
35
|
+
const command = args.command || '';
|
|
36
|
+
|
|
37
|
+
// Check block patterns
|
|
38
|
+
for (const pattern of (policy.block_patterns || [])) {
|
|
39
|
+
const regex = globToRegex(pattern);
|
|
40
|
+
if (regex.test(command)) {
|
|
41
|
+
return {
|
|
42
|
+
decision: DECISIONS.deny,
|
|
43
|
+
tool: 'exec',
|
|
44
|
+
reason: `Command matches blocked pattern: ${pattern}`,
|
|
45
|
+
matched: command.substring(0, 200),
|
|
46
|
+
severity: 'critical',
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Check dangerous commands
|
|
52
|
+
const dangerousCommands = [
|
|
53
|
+
{ pattern: /\brm\s+(-[a-zA-Z]*f[a-zA-Z]*\s+|.*-[a-zA-Z]*r[a-zA-Z]*f)/, reason: 'Recursive force delete', severity: 'critical' },
|
|
54
|
+
{ pattern: /\bmkfs\b/, reason: 'Filesystem format', severity: 'critical' },
|
|
55
|
+
{ pattern: /\bdd\s+if=/, reason: 'Low-level disk write', severity: 'high' },
|
|
56
|
+
{ pattern: />\s*\/dev\/sd[a-z]/, reason: 'Direct disk write', severity: 'critical' },
|
|
57
|
+
{ pattern: /\bchmod\s+777\b/, reason: 'World-writable permissions', severity: 'medium' },
|
|
58
|
+
{ pattern: /\bchmod\s+\+s\b/, reason: 'Set SUID bit', severity: 'high' },
|
|
59
|
+
{ pattern: /\bcrontab\s+-r\b/, reason: 'Remove all cron jobs', severity: 'high' },
|
|
60
|
+
{ pattern: /\biptables\s+-F\b/, reason: 'Flush all firewall rules', severity: 'critical' },
|
|
61
|
+
{ pattern: /\bpasswd\b/, reason: 'Password change attempt', severity: 'high' },
|
|
62
|
+
{ pattern: /\buseradd\b|\badduser\b/, reason: 'User creation', severity: 'high' },
|
|
63
|
+
{ pattern: /\bvisudo\b|\bsudoers\b/, reason: 'Sudo configuration change', severity: 'critical' },
|
|
64
|
+
{ pattern: /\bnc\s+-[a-z]*l/i, reason: 'Network listener (reverse shell risk)', severity: 'critical' },
|
|
65
|
+
{ pattern: /\b(?:python|node|ruby|perl)\s+-.*(?:socket|http\.server|SimpleHTTP)/i, reason: 'Network server spawn', severity: 'high' },
|
|
66
|
+
{ pattern: /\bcurl\b.*\|\s*(?:bash|sh|zsh)\b/, reason: 'Pipe remote script to shell', severity: 'critical' },
|
|
67
|
+
{ pattern: /\bwget\b.*\|\s*(?:bash|sh|zsh)\b/, reason: 'Pipe remote script to shell', severity: 'critical' },
|
|
68
|
+
{ pattern: /\beval\s+\$\(curl\b/, reason: 'Eval remote content', severity: 'critical' },
|
|
69
|
+
{ pattern: /\beval\s+\$\(wget\b/, reason: 'Eval remote content', severity: 'critical' },
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
for (const { pattern, reason, severity } of dangerousCommands) {
|
|
73
|
+
if (pattern.test(command)) {
|
|
74
|
+
return {
|
|
75
|
+
decision: severity === 'critical' ? DECISIONS.deny : DECISIONS.warn,
|
|
76
|
+
tool: 'exec',
|
|
77
|
+
reason,
|
|
78
|
+
matched: command.substring(0, 200),
|
|
79
|
+
severity,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Check require_approval patterns
|
|
85
|
+
for (const pattern of (policy.require_approval || [])) {
|
|
86
|
+
const regex = globToRegex(pattern);
|
|
87
|
+
if (regex.test(command)) {
|
|
88
|
+
return {
|
|
89
|
+
decision: DECISIONS.review,
|
|
90
|
+
tool: 'exec',
|
|
91
|
+
reason: `Command requires approval: ${pattern}`,
|
|
92
|
+
matched: command.substring(0, 200),
|
|
93
|
+
severity: 'medium',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { decision: DECISIONS.allow, tool: 'exec' };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function evaluateFileRead(args, policy) {
|
|
102
|
+
const filePath = args.path || args.file_path || '';
|
|
103
|
+
const expanded = expandHome(filePath);
|
|
104
|
+
|
|
105
|
+
for (const pattern of (policy.deny_read || [])) {
|
|
106
|
+
const regex = globToRegex(expandHome(pattern));
|
|
107
|
+
if (regex.test(expanded)) {
|
|
108
|
+
return {
|
|
109
|
+
decision: DECISIONS.deny,
|
|
110
|
+
tool: 'read',
|
|
111
|
+
reason: `File read blocked by policy: ${pattern}`,
|
|
112
|
+
path: filePath,
|
|
113
|
+
severity: 'high',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { decision: DECISIONS.allow, tool: 'read' };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function evaluateFileWrite(args, policy) {
|
|
122
|
+
const filePath = args.path || args.file_path || '';
|
|
123
|
+
const expanded = expandHome(filePath);
|
|
124
|
+
|
|
125
|
+
for (const pattern of (policy.deny_write || [])) {
|
|
126
|
+
const regex = globToRegex(expandHome(pattern));
|
|
127
|
+
if (regex.test(expanded)) {
|
|
128
|
+
return {
|
|
129
|
+
decision: DECISIONS.deny,
|
|
130
|
+
tool: 'write',
|
|
131
|
+
reason: `File write blocked by policy: ${pattern}`,
|
|
132
|
+
path: filePath,
|
|
133
|
+
severity: 'high',
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { decision: DECISIONS.allow, tool: 'write' };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function evaluateBrowser(args, policy) {
|
|
142
|
+
const url = args.targetUrl || args.url || '';
|
|
143
|
+
|
|
144
|
+
for (const pattern of (policy.block_domains || [])) {
|
|
145
|
+
const regex = globToRegex(pattern);
|
|
146
|
+
if (regex.test(url)) {
|
|
147
|
+
return {
|
|
148
|
+
decision: DECISIONS.deny,
|
|
149
|
+
tool: 'browser',
|
|
150
|
+
reason: `Domain blocked by policy: ${pattern}`,
|
|
151
|
+
url,
|
|
152
|
+
severity: 'high',
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { decision: DECISIONS.allow, tool: 'browser' };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function evaluateMessage(args, policy) {
|
|
161
|
+
// Flag messages going to unknown recipients
|
|
162
|
+
return { decision: DECISIONS.allow, tool: 'message' };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Helpers
|
|
166
|
+
function expandHome(p) {
|
|
167
|
+
return p.replace(/^~/, process.env.HOME || '/home/user');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function globToRegex(glob) {
|
|
171
|
+
const escaped = glob
|
|
172
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
173
|
+
.replace(/\*\*/g, '§DOUBLESTAR§')
|
|
174
|
+
.replace(/\*/g, '[^/]*')
|
|
175
|
+
.replace(/§DOUBLESTAR§/g, '.*')
|
|
176
|
+
.replace(/\?/g, '.');
|
|
177
|
+
return new RegExp(escaped, 'i');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = { evaluateToolCall, DECISIONS };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClawMoat — Exfiltration Detection Scanner
|
|
3
|
+
*
|
|
4
|
+
* Detects data exfiltration attempts — when an agent is being used
|
|
5
|
+
* to send data to external services.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const PASTE_SERVICES = [
|
|
9
|
+
'pastebin.com', 'hastebin.com', '0x0.st', 'transfer.sh', 'paste.ee',
|
|
10
|
+
'dpaste.org', 'ghostbin.com', 'rentry.co', 'paste.mozilla.org',
|
|
11
|
+
'ix.io', 'sprunge.us', 'cl1p.net', 'file.io', 'tmpfiles.org',
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const EXFIL_PATTERNS = [
|
|
15
|
+
// Curl/wget to external services
|
|
16
|
+
{ pattern: /\bcurl\s+(?:-[a-zA-Z]\s+)*(?:--data|--data-binary|-d|-F)\s/i, severity: 'high', name: 'curl_data_upload' },
|
|
17
|
+
{ pattern: /\bwget\s+--post-(?:data|file)\b/i, severity: 'high', name: 'wget_data_upload' },
|
|
18
|
+
|
|
19
|
+
// Base64 data being sent externally
|
|
20
|
+
{ pattern: /\bcurl\b.*\b(?:base64|atob|btoa)\b/i, severity: 'high', name: 'base64_exfiltration' },
|
|
21
|
+
{ pattern: /\b(?:echo|printf)\s+['"]?[A-Za-z0-9+/=]{50,}['"]?\s*\|\s*(?:curl|wget|nc)\b/i, severity: 'critical', name: 'base64_exfiltration' },
|
|
22
|
+
|
|
23
|
+
// DNS exfiltration (long subdomain strings)
|
|
24
|
+
{ pattern: /\b(?:dig|nslookup|host)\s+[A-Za-z0-9]{20,}\./i, severity: 'high', name: 'dns_exfiltration' },
|
|
25
|
+
{ pattern: /\$\(.*\)\.[A-Za-z0-9.-]+\.[a-z]{2,}/i, severity: 'medium', name: 'dns_exfiltration' },
|
|
26
|
+
|
|
27
|
+
// File contents being sent via messaging
|
|
28
|
+
{ pattern: /\b(?:send|post|forward|share|upload)\s+(?:the\s+)?(?:contents?\s+of|file)\s+(?:\/|~\/|\.\/)/i, severity: 'high', name: 'file_content_send' },
|
|
29
|
+
{ pattern: /\bcat\s+[^\s|]+\s*\|\s*(?:curl|wget|nc)\b/i, severity: 'critical', name: 'file_content_pipe' },
|
|
30
|
+
|
|
31
|
+
// Paste services
|
|
32
|
+
{ pattern: new RegExp('\\b(?:curl|wget|fetch)\\s+.*(?:' + PASTE_SERVICES.map(s => s.replace(/\./g, '\\.')).join('|') + ')', 'i'), severity: 'critical', name: 'paste_service_upload' },
|
|
33
|
+
|
|
34
|
+
// Email forwarding of sensitive content
|
|
35
|
+
{ pattern: /\b(?:send|forward|email|mail)\s+(?:the\s+)?(?:ssh|key|password|credential|token|secret|env|config)\b.*\bto\b/i, severity: 'critical', name: 'email_exfiltration' },
|
|
36
|
+
|
|
37
|
+
// Netcat reverse connections
|
|
38
|
+
{ pattern: /\bnc\s+(?:-[a-z]\s+)*[^\s]+\s+\d+\s*</i, severity: 'critical', name: 'netcat_exfiltration' },
|
|
39
|
+
|
|
40
|
+
// Upload via fetch/XMLHttpRequest
|
|
41
|
+
{ pattern: /\bfetch\s*\(\s*['"][^'"]+['"]\s*,\s*\{[^}]*method\s*:\s*['"]POST['"]/i, severity: 'high', name: 'fetch_upload' },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Scan text for data exfiltration attempts
|
|
46
|
+
* @param {string} text - Text to scan
|
|
47
|
+
* @param {object} opts - Options
|
|
48
|
+
* @returns {object} Scan result { clean, findings[], severity }
|
|
49
|
+
*/
|
|
50
|
+
function scanExfiltration(text, opts = {}) {
|
|
51
|
+
if (!text || typeof text !== 'string') {
|
|
52
|
+
return { clean: true, findings: [], severity: null };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const findings = [];
|
|
56
|
+
|
|
57
|
+
for (const { pattern, severity, name } of EXFIL_PATTERNS) {
|
|
58
|
+
const match = text.match(pattern);
|
|
59
|
+
if (match) {
|
|
60
|
+
findings.push({
|
|
61
|
+
type: 'exfiltration',
|
|
62
|
+
subtype: name,
|
|
63
|
+
severity,
|
|
64
|
+
matched: match[0].substring(0, 100),
|
|
65
|
+
position: match.index,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check for paste service URLs directly
|
|
71
|
+
const lowerText = text.toLowerCase();
|
|
72
|
+
for (const service of PASTE_SERVICES) {
|
|
73
|
+
if (lowerText.includes(service) && /\b(?:upload|post|send|push|put)\b/i.test(text)) {
|
|
74
|
+
const existing = findings.find(f => f.subtype === 'paste_service_upload');
|
|
75
|
+
if (!existing) {
|
|
76
|
+
findings.push({
|
|
77
|
+
type: 'exfiltration',
|
|
78
|
+
subtype: 'paste_service_upload',
|
|
79
|
+
severity: 'high',
|
|
80
|
+
matched: service,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const maxSev = findings.length > 0
|
|
87
|
+
? findings.reduce((max, f) => rank(f.severity) > rank(max) ? f.severity : max, 'low')
|
|
88
|
+
: null;
|
|
89
|
+
|
|
90
|
+
return { clean: findings.length === 0, findings, severity: maxSev };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function rank(s) {
|
|
94
|
+
return { low: 0, medium: 1, high: 2, critical: 3 }[s] || 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = { scanExfiltration };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClawMoat — Jailbreak Detection
|
|
3
|
+
*
|
|
4
|
+
* Detects common LLM jailbreak patterns (DAN, roleplay exploits, etc.)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const JAILBREAK_PATTERNS = [
|
|
8
|
+
// DAN and variants
|
|
9
|
+
{ pattern: /\bDAN\b.*(?:do anything now|jailbreak)/i, severity: 'critical', name: 'dan_jailbreak' },
|
|
10
|
+
{ pattern: /\b(?:DAN|STAN|DUDE|AIM)\s+(?:mode|prompt|jailbreak)/i, severity: 'critical', name: 'named_jailbreak' },
|
|
11
|
+
|
|
12
|
+
// Developer/debug mode
|
|
13
|
+
{ pattern: /(?:enable|enter|activate|switch\s+to)\s+(?:developer|debug|maintenance|test|unrestricted|unfiltered)\s+mode/i, severity: 'high', name: 'mode_switch' },
|
|
14
|
+
{ pattern: /\bsudo\s+mode\b/i, severity: 'high', name: 'mode_switch' },
|
|
15
|
+
|
|
16
|
+
// Dual persona / split personality
|
|
17
|
+
{ pattern: /(?:respond|answer|reply)\s+(?:as|like)\s+(?:both|two)\s+(?:characters|personas|versions|a\s+normal)/i, severity: 'high', name: 'dual_persona' },
|
|
18
|
+
{ pattern: /(?:both)\s+(?:a\s+)?(?:normal|regular|standard)\s+(?:AI|assistant|mode)\s+(?:and|\/)\s+(?:an?\s+)?(?:unrestricted|unfiltered|DAN)/i, severity: 'high', name: 'dual_persona' },
|
|
19
|
+
{ pattern: /(?:classic|normal)\s+(?:mode|response)\s+(?:and|vs|\/)\s+(?:jailbreak|DAN|unfiltered)/i, severity: 'high', name: 'dual_persona' },
|
|
20
|
+
|
|
21
|
+
// "Hypothetical" bypass
|
|
22
|
+
{ pattern: /(?:hypothetically|theoretically|in\s+a\s+fictional)\s+(?:how\s+would|could\s+you|what\s+if)/i, severity: 'medium', name: 'hypothetical_bypass' },
|
|
23
|
+
{ pattern: /(?:write|create)\s+(?:a\s+)?(?:story|fiction|screenplay)\s+(?:where|about|in\s+which).*(?:hack|exploit|steal|attack)/i, severity: 'medium', name: 'fiction_bypass' },
|
|
24
|
+
|
|
25
|
+
// Token smuggling
|
|
26
|
+
{ pattern: /(?:translate|decode|convert)\s+(?:this|the\s+following)\s+(?:from\s+)?(?:base64|hex|rot13|binary|morse)/i, severity: 'medium', name: 'encoding_bypass' },
|
|
27
|
+
|
|
28
|
+
// Grandma exploit
|
|
29
|
+
{ pattern: /(?:my\s+(?:grandma|grandmother|nana))\s+(?:used\s+to|would\s+always)\s+(?:tell|read|say)/i, severity: 'low', name: 'social_engineering' },
|
|
30
|
+
|
|
31
|
+
// Prompt leaking via completion
|
|
32
|
+
{ pattern: /(?:continue|complete|finish)\s+(?:this|the)\s+(?:sentence|text|prompt)\s*:\s*(?:"|'|`)/i, severity: 'medium', name: 'completion_attack' },
|
|
33
|
+
|
|
34
|
+
// Anti-safety patterns
|
|
35
|
+
{ pattern: /(?:safety|content|ethical)\s+(?:filters?|guidelines?|restrictions?)\s+(?:are|have\s+been)\s+(?:removed|disabled|turned\s+off)/i, severity: 'critical', name: 'safety_bypass_claim' },
|
|
36
|
+
{ pattern: /(?:you\s+(?:can|are\s+able\s+to|have\s+permission\s+to))\s+(?:say|do|generate)\s+anything/i, severity: 'high', name: 'permission_claim' },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Scan text for jailbreak attempts
|
|
41
|
+
* @param {string} text - Text to scan
|
|
42
|
+
* @returns {object} Scan result
|
|
43
|
+
*/
|
|
44
|
+
function scanJailbreak(text) {
|
|
45
|
+
if (!text || typeof text !== 'string') {
|
|
46
|
+
return { clean: true, score: 0, findings: [] };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const findings = [];
|
|
50
|
+
|
|
51
|
+
for (const { pattern, severity, name } of JAILBREAK_PATTERNS) {
|
|
52
|
+
const match = text.match(pattern);
|
|
53
|
+
if (match) {
|
|
54
|
+
findings.push({
|
|
55
|
+
type: 'jailbreak',
|
|
56
|
+
subtype: name,
|
|
57
|
+
severity,
|
|
58
|
+
matched: match[0].substring(0, 100),
|
|
59
|
+
position: match.index,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const score = Math.min(findings.length * 30, 100);
|
|
65
|
+
const maxSeverity = findings.reduce(
|
|
66
|
+
(max, f) => (rank(f.severity) > rank(max) ? f.severity : max), 'low'
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
clean: findings.length === 0,
|
|
71
|
+
score,
|
|
72
|
+
severity: findings.length > 0 ? maxSeverity : null,
|
|
73
|
+
findings,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function rank(s) {
|
|
78
|
+
return { low: 0, medium: 1, high: 2, critical: 3 }[s] || 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { scanJailbreak, JAILBREAK_PATTERNS };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClawMoat — Memory Poisoning Detection Scanner
|
|
3
|
+
*
|
|
4
|
+
* Detects attempts to poison agent memory files (MEMORY.md, SOUL.md, etc.)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const MEMORY_PATTERNS = [
|
|
8
|
+
// Direct memory file manipulation
|
|
9
|
+
{ pattern: /(?:add|write|append|update|modify|change|insert|put)\s+(?:this\s+)?(?:to|in|into)\s+(?:your\s+)?(?:MEMORY|SOUL|memory|soul)\.md/i, severity: 'critical', name: 'memory_file_write' },
|
|
10
|
+
{ pattern: /(?:add|write|append|update|modify|change|insert|put)\s+(?:this\s+)?(?:to|in|into)\s+(?:your\s+)?(?:AGENTS|HEARTBEAT|TOOLS|BOOTSTRAP)\.md/i, severity: 'critical', name: 'config_file_write' },
|
|
11
|
+
|
|
12
|
+
// Memory manipulation instructions
|
|
13
|
+
{ pattern: /\bremember\s+that\s+your\s+(?:instructions?|rules?|guidelines?|purpose|goal)\s+(?:are|is)\b/i, severity: 'critical', name: 'memory_override' },
|
|
14
|
+
{ pattern: /\b(?:save|store|memorize|remember)\s+(?:this|the\s+following)\s+(?:for|across|between)\s+(?:future\s+)?sessions?\b/i, severity: 'high', name: 'persistent_memory' },
|
|
15
|
+
{ pattern: /\b(?:update|change|modify|edit)\s+(?:your\s+)?(?:personality|identity|soul|core\s+(?:values|instructions?))\b/i, severity: 'critical', name: 'identity_override' },
|
|
16
|
+
|
|
17
|
+
// Config file targeting
|
|
18
|
+
{ pattern: /\b(?:edit|modify|update|write\s+to|overwrite)\s+(?:the\s+)?(?:AGENTS|TOOLS|HEARTBEAT|BOOTSTRAP|SOUL|MEMORY)\.md\b/i, severity: 'critical', name: 'config_targeting' },
|
|
19
|
+
|
|
20
|
+
// Persistent injection (survive across sessions)
|
|
21
|
+
{ pattern: /\b(?:always|forever|permanently|from\s+now\s+on)\s+(?:remember|keep|maintain|follow|obey)\b/i, severity: 'high', name: 'persistent_injection' },
|
|
22
|
+
{ pattern: /\b(?:in\s+(?:all|every)\s+future\s+(?:sessions?|conversations?|interactions?))\b/i, severity: 'high', name: 'persistent_injection' },
|
|
23
|
+
{ pattern: /\b(?:across\s+(?:all\s+)?sessions?\s+(?:you\s+)?(?:must|should|will))\b/i, severity: 'high', name: 'persistent_injection' },
|
|
24
|
+
|
|
25
|
+
// Time bomb patterns
|
|
26
|
+
{ pattern: /\b(?:next\s+time|when\s+you\s+(?:next\s+)?see|if\s+(?:someone|anyone|a\s+user)\s+(?:asks?|says?|mentions?))\b.*\b(?:do|execute|run|say|respond|output)\b/i, severity: 'high', name: 'time_bomb' },
|
|
27
|
+
{ pattern: /\b(?:whenever|each\s+time|every\s+time)\s+(?:you|someone|a\s+user)\b.*\b(?:secretly|silently|without\s+(?:telling|mentioning|saying))\b/i, severity: 'critical', name: 'time_bomb' },
|
|
28
|
+
{ pattern: /\b(?:after\s+this\s+(?:conversation|session|chat)\s+(?:ends?|is\s+over))\b/i, severity: 'high', name: 'time_bomb' },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Scan text for memory poisoning attempts
|
|
33
|
+
* @param {string} text - Text to scan
|
|
34
|
+
* @param {object} opts - Options
|
|
35
|
+
* @returns {object} Scan result { clean, findings[], severity }
|
|
36
|
+
*/
|
|
37
|
+
function scanMemoryPoison(text, opts = {}) {
|
|
38
|
+
if (!text || typeof text !== 'string') {
|
|
39
|
+
return { clean: true, findings: [], severity: null };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const findings = [];
|
|
43
|
+
|
|
44
|
+
for (const { pattern, severity, name } of MEMORY_PATTERNS) {
|
|
45
|
+
const match = text.match(pattern);
|
|
46
|
+
if (match) {
|
|
47
|
+
findings.push({
|
|
48
|
+
type: 'memory_poison',
|
|
49
|
+
subtype: name,
|
|
50
|
+
severity,
|
|
51
|
+
matched: match[0].substring(0, 100),
|
|
52
|
+
position: match.index,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const maxSev = findings.length > 0
|
|
58
|
+
? findings.reduce((max, f) => rank(f.severity) > rank(max) ? f.severity : max, 'low')
|
|
59
|
+
: null;
|
|
60
|
+
|
|
61
|
+
return { clean: findings.length === 0, findings, severity: maxSev };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function rank(s) {
|
|
65
|
+
return { low: 0, medium: 1, high: 2, critical: 3 }[s] || 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = { scanMemoryPoison };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClawMoat — PII Detection Scanner
|
|
3
|
+
*
|
|
4
|
+
* Detects personally identifiable information in outbound messages:
|
|
5
|
+
* emails, phone numbers, SSNs, credit cards, IP addresses, physical addresses, names.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const PII_PATTERNS = [
|
|
9
|
+
// Email addresses
|
|
10
|
+
{ name: 'email', pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/, severity: 'high' },
|
|
11
|
+
|
|
12
|
+
// SSN
|
|
13
|
+
{ name: 'ssn', pattern: /\b\d{3}-\d{2}-\d{4}\b/, severity: 'critical' },
|
|
14
|
+
|
|
15
|
+
// Phone numbers (US)
|
|
16
|
+
{ name: 'phone_us', pattern: /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/, severity: 'high' },
|
|
17
|
+
|
|
18
|
+
// Phone numbers (international)
|
|
19
|
+
{ name: 'phone_international', pattern: /\b\+(?:2[0-9]|3[0-9]|4[0-9]|5[0-9]|6[0-9]|7[0-9]|8[0-9]|9[0-9])\d{7,12}\b/, severity: 'high' },
|
|
20
|
+
|
|
21
|
+
// IP addresses (private/internal)
|
|
22
|
+
{ name: 'private_ip', pattern: /\b(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})\b/, severity: 'medium' },
|
|
23
|
+
|
|
24
|
+
// Physical addresses (street patterns)
|
|
25
|
+
{ name: 'physical_address', pattern: /\b\d{1,5}\s+(?:[A-Z][a-z]+\s+){1,3}(?:St(?:reet)?|Ave(?:nue)?|Blvd|Boulevard|Dr(?:ive)?|Ln|Lane|Rd|Road|Ct|Court|Pl|Place|Way|Cir(?:cle)?)\b/i, severity: 'high' },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
// Credit card patterns (need Luhn validation)
|
|
29
|
+
const CREDIT_CARD_PATTERNS = [
|
|
30
|
+
{ name: 'visa', pattern: /\b4\d{3}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/ },
|
|
31
|
+
{ name: 'mastercard', pattern: /\b5[1-5]\d{2}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/ },
|
|
32
|
+
{ name: 'amex', pattern: /\b3[47]\d{2}[-\s]?\d{6}[-\s]?\d{5}\b/ },
|
|
33
|
+
{ name: 'discover', pattern: /\b6(?:011|5\d{2})[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/ },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
// Name-near-keyword heuristic
|
|
37
|
+
const NAME_KEYWORDS = /\b(?:patient|client|user|customer|employee|member|applicant|resident|tenant|subscriber)\s*(?:name)?\s*[:=]?\s*([A-Z][a-z]+(?:\s+[A-Z][a-z]+){1,2})/;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Luhn algorithm to validate credit card numbers
|
|
41
|
+
*/
|
|
42
|
+
function luhnCheck(numStr) {
|
|
43
|
+
const digits = numStr.replace(/[-\s]/g, '');
|
|
44
|
+
if (!/^\d+$/.test(digits)) return false;
|
|
45
|
+
let sum = 0;
|
|
46
|
+
let alternate = false;
|
|
47
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
48
|
+
let n = parseInt(digits[i], 10);
|
|
49
|
+
if (alternate) {
|
|
50
|
+
n *= 2;
|
|
51
|
+
if (n > 9) n -= 9;
|
|
52
|
+
}
|
|
53
|
+
sum += n;
|
|
54
|
+
alternate = !alternate;
|
|
55
|
+
}
|
|
56
|
+
return sum % 10 === 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Scan text for PII
|
|
61
|
+
* @param {string} text - Text to scan
|
|
62
|
+
* @param {object} opts - Options
|
|
63
|
+
* @returns {object} Scan result { clean, findings[], severity }
|
|
64
|
+
*/
|
|
65
|
+
function scanPII(text, opts = {}) {
|
|
66
|
+
if (!text || typeof text !== 'string') {
|
|
67
|
+
return { clean: true, findings: [], severity: null };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const findings = [];
|
|
71
|
+
|
|
72
|
+
// Standard PII patterns
|
|
73
|
+
for (const { name, pattern, severity } of PII_PATTERNS) {
|
|
74
|
+
const match = text.match(pattern);
|
|
75
|
+
if (match) {
|
|
76
|
+
findings.push({
|
|
77
|
+
type: 'pii_detected',
|
|
78
|
+
subtype: name,
|
|
79
|
+
severity,
|
|
80
|
+
matched: redact(match[0]),
|
|
81
|
+
position: match.index,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Credit card with Luhn validation
|
|
87
|
+
for (const { name, pattern } of CREDIT_CARD_PATTERNS) {
|
|
88
|
+
const match = text.match(pattern);
|
|
89
|
+
if (match && luhnCheck(match[0])) {
|
|
90
|
+
findings.push({
|
|
91
|
+
type: 'pii_detected',
|
|
92
|
+
subtype: 'credit_card_' + name,
|
|
93
|
+
severity: 'critical',
|
|
94
|
+
matched: redact(match[0]),
|
|
95
|
+
position: match.index,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Name near keyword heuristic
|
|
101
|
+
const nameMatch = text.match(NAME_KEYWORDS);
|
|
102
|
+
if (nameMatch) {
|
|
103
|
+
findings.push({
|
|
104
|
+
type: 'pii_detected',
|
|
105
|
+
subtype: 'name_keyword',
|
|
106
|
+
severity: 'medium',
|
|
107
|
+
matched: redact(nameMatch[0]),
|
|
108
|
+
position: nameMatch.index,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const maxSev = findings.length > 0
|
|
113
|
+
? findings.reduce((max, f) => rank(f.severity) > rank(max) ? f.severity : max, 'low')
|
|
114
|
+
: null;
|
|
115
|
+
|
|
116
|
+
return { clean: findings.length === 0, findings, severity: maxSev };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function redact(value) {
|
|
120
|
+
if (value.length <= 8) return '****';
|
|
121
|
+
return value.substring(0, 4) + '*'.repeat(Math.min(value.length - 8, 20)) + value.substring(value.length - 4);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function rank(s) {
|
|
125
|
+
return { low: 0, medium: 1, high: 2, critical: 3 }[s] || 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = { scanPII };
|