clawmoat 0.8.0 → 1.0.0
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/.dockerignore +9 -0
- package/CHANGELOG.md +18 -0
- package/DEMO.md +87 -0
- package/Dockerfile +5 -18
- package/README.md +232 -8
- package/THREAT_MODEL.md +129 -0
- package/agent/README.md +131 -0
- package/agent/index.js +471 -0
- package/agent/install-service.sh +94 -0
- package/agent/openclaw-hook.js +453 -0
- package/agent/provider-setup.js +649 -0
- package/agent/setup.js +274 -0
- package/assets/BADGE-USAGE.md +20 -0
- package/assets/clawmoat-badge.svg +21 -0
- package/bin/clawmoat.js +468 -111
- package/docs/affiliates/dashboard.html +124 -0
- package/docs/affiliates/index.html +236 -0
- package/docs/agent-install.html +183 -0
- package/docs/ai-agent-security-scanner.html +10 -6
- package/docs/badge/index.html +149 -0
- package/docs/badge/scanning.svg +23 -0
- package/docs/blog/386-malicious-skills.html +11 -4
- package/docs/blog/40000-exposed-openclaw-instances.html +11 -4
- package/docs/blog/agent-trust-protocol.html +5 -4
- package/docs/blog/ai-agent-earns-commissions.html +230 -0
- package/docs/blog/bugmageddon-agent-firewall.html +174 -0
- package/docs/blog/calculator-math.html +180 -0
- package/docs/blog/clawmoat-vs-llamafirewall-nemo-guardrails.html +10 -4
- package/docs/blog/host-guardian-launch.html +18 -8
- package/docs/blog/ibm-experts-agent-runtime-protection.html +15 -6
- package/docs/blog/index.html +67 -9
- package/docs/blog/langchain-security-tutorial.html +18 -8
- package/docs/blog/mcp-30-cves-security-crisis.html +11 -4
- package/docs/blog/meta-researcher-rogue-agent.html +201 -0
- package/docs/blog/microsoft-openclaw-workstation-security.html +5 -4
- package/docs/blog/nist-ai-agent-standards-clawmoat.html +16 -8
- package/docs/blog/oasis-websocket-hijack.html +11 -4
- package/docs/blog/ollama-openclaw-security.html +10 -4
- package/docs/blog/openclaw-enterprise-readiness-claw10.html +5 -4
- package/docs/blog/openclaw-security-reckoning-2026.html +11 -4
- package/docs/blog/owasp-agentic-ai-top10.html +18 -8
- package/docs/blog/securing-ai-agents.html +18 -8
- package/docs/blog/supply-chain-agents.html +18 -8
- package/docs/business/index.html +11 -16
- package/docs/business/install.html +21 -7
- package/docs/checklist.html +10 -4
- package/docs/compare/index.html +122 -0
- package/docs/compare/lakera/index.html +62 -0
- package/docs/compare/llm-guard/index.html +49 -0
- package/docs/compare/snyk-agent-scan/index.html +63 -0
- package/docs/compare.html +10 -6
- package/docs/dashboard/index.html +520 -0
- package/docs/finance/index.html +9 -6
- package/docs/guides/business-deployment.html +770 -0
- package/docs/hall-of-fame.html +11 -5
- package/docs/index.html +266 -137
- package/docs/integrations/langchain.html +14 -6
- package/docs/integrations/openai.html +14 -6
- package/docs/integrations/openclaw.html +55 -7
- package/docs/plans/2026-03-26-threat-intel-api.md +255 -0
- package/docs/plans/2026-04-14-bugmageddon-marketing-pack.md +329 -0
- package/docs/plans/2026-04-14-clawmoat-v1-bugmageddon.md +248 -0
- package/docs/plans/2026-04-14-v1-release-update.md +91 -0
- package/docs/plans/2026-04-19-supabase-audit.md +68 -0
- package/docs/plans/2026-05-12-sales-push.md +303 -0
- package/docs/playground/index.html +893 -0
- package/docs/playground.html +4 -7
- package/docs/rfcs/defense-in-depth.md +467 -0
- package/docs/scan/index.html +156 -12
- package/docs/services/case-study.html +255 -0
- package/docs/services/downloads/install-openclaw.bat +45 -0
- package/docs/services/downloads/install-openclaw.command +38 -0
- package/docs/services/downloads/install-openclaw.sh +38 -0
- package/docs/services/get-started.html +165 -0
- package/docs/services/index.html +598 -0
- package/docs/services/multi-agent-security.html +284 -0
- package/docs/services/one-pager.html +99 -0
- package/docs/services/pitch-deck.html +229 -0
- package/docs/services/roi-calculator.html +258 -0
- package/docs/sitemap.xml +62 -2
- package/docs/support/index.html +12 -1
- package/docs/templates/customer-service/HEARTBEAT.md +61 -0
- package/docs/templates/customer-service/MEMORY.md +89 -0
- package/docs/templates/customer-service/SOUL.md +41 -0
- package/docs/templates/customer-service/USER.md +56 -0
- package/docs/templates/executive/HEARTBEAT.md +86 -0
- package/docs/templates/executive/MEMORY.md +92 -0
- package/docs/templates/executive/SOUL.md +44 -0
- package/docs/templates/executive/USER.md +62 -0
- package/docs/templates/finance/HEARTBEAT.md +58 -0
- package/docs/templates/finance/MEMORY.md +87 -0
- package/docs/templates/finance/SOUL.md +38 -0
- package/docs/templates/finance/USER.md +53 -0
- package/docs/templates/index.html +115 -0
- package/docs/templates/operations/HEARTBEAT.md +63 -0
- package/docs/templates/operations/MEMORY.md +68 -0
- package/docs/templates/operations/SOUL.md +38 -0
- package/docs/templates/operations/USER.md +49 -0
- package/docs/templates/sales/HEARTBEAT.md +55 -0
- package/docs/templates/sales/MEMORY.md +89 -0
- package/docs/templates/sales/SOUL.md +34 -0
- package/docs/templates/sales/USER.md +54 -0
- package/eslint.config.js +32 -0
- package/evals/README.md +29 -0
- package/evals/cases.json +390 -0
- package/evals/results.md +68 -0
- package/evals/run.js +180 -0
- package/examples/demo-attack/demo.js +186 -0
- package/examples/python-quickstart/README.md +54 -0
- package/examples/python-quickstart/clawmoat_client.py +167 -0
- package/examples/video-demo/README.md +14 -0
- package/examples/video-demo/scene-a-normal.js +29 -0
- package/examples/video-demo/scene-b-attack-arrives.js +31 -0
- package/examples/video-demo/scene-c-hijack.js +44 -0
- package/examples/video-demo/scene-d-clawmoat.js +46 -0
- package/integrations/crewai/README.md +32 -0
- package/integrations/crewai/clawmoat_crewai/__init__.py +17 -0
- package/integrations/crewai/clawmoat_crewai/guard.py +103 -0
- package/integrations/crewai/pyproject.toml +21 -0
- package/integrations/langchain/README.md +91 -0
- package/integrations/langchain/clawmoat_langchain/__init__.py +17 -0
- package/integrations/langchain/clawmoat_langchain/callback.py +489 -0
- package/integrations/langchain/pyproject.toml +32 -0
- package/integrations/litellm/README.md +324 -0
- package/integrations/litellm/clawmoat_litellm/__init__.py +21 -0
- package/integrations/litellm/clawmoat_litellm/callback.py +329 -0
- package/integrations/litellm/clawmoat_litellm/proxy_middleware.py +224 -0
- package/integrations/litellm/pyproject.toml +74 -0
- package/integrations/openai-agents/README.md +392 -0
- package/integrations/openai-agents/clawmoat_openai_agents/__init__.py +20 -0
- package/integrations/openai-agents/clawmoat_openai_agents/guardrail.py +431 -0
- package/integrations/openai-agents/clawmoat_openai_agents/middleware.py +311 -0
- package/integrations/openai-agents/pyproject.toml +76 -0
- package/package.json +6 -5
- package/plugins/openclaw-adapter/PHASE1.md +439 -0
- package/plugins/openclaw-adapter/README.md +103 -0
- package/plugins/openclaw-adapter/SPEC.md +1644 -0
- package/plugins/openclaw-adapter/package.json +31 -0
- package/plugins/openclaw-adapter/src/index.test.ts +226 -0
- package/plugins/openclaw-adapter/src/index.ts +140 -0
- package/plugins/openclaw-adapter/tsconfig.json +14 -0
- package/server/data/threats.json +290 -0
- package/server/index.js +142 -7
- package/src/adapters/express.js +161 -0
- package/src/adapters/index.js +92 -0
- package/src/adapters/langchain.js +185 -0
- package/src/approval/index.js +456 -0
- package/src/ban-scanner.js +200 -0
- package/src/boundary-scanner.js +296 -0
- package/src/ci-scanner.js +279 -0
- package/src/code-scanner.js +245 -0
- package/src/enforce.js +166 -0
- package/src/formatters/json.js +80 -0
- package/src/formatters/sarif.js +388 -0
- package/src/guardian/alerts.js +34 -3
- package/src/guardian/index.js +41 -2
- package/src/index.js +102 -0
- package/src/integrations/agentmesh.js +501 -0
- package/src/language-detector.js +201 -0
- package/src/mcp-scanner.js +253 -0
- package/src/multimodal/index.js +579 -0
- package/src/obfuscation-scanner.js +457 -0
- package/src/policy-engine.js +402 -0
- package/src/scanners/dependency-attacks.js +128 -0
- package/src/scanners/prompt-injection.js +18 -0
- package/src/scanners/supply-chain.js +14 -0
- package/src/templates/default-config.yml +90 -0
- package/src/vuln-ops/exploitability.js +46 -0
- package/src/watch/live-monitor.js +720 -0
- package/clawmoat-0.8.0.tgz +0 -0
- package/server/index.js.patch +0 -1
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentMesh Governance Integration
|
|
3
|
+
* Bridges ClawMoat security scanning with governance policy engines
|
|
4
|
+
* Maps threat detections to governance actions based on OWASP Agentic Top 10
|
|
5
|
+
*
|
|
6
|
+
* @module integrations/agentmesh
|
|
7
|
+
* @example
|
|
8
|
+
* const { AgentMeshBridge } = require('./integrations/agentmesh');
|
|
9
|
+
*
|
|
10
|
+
* const bridge = new AgentMeshBridge({
|
|
11
|
+
* policies: {
|
|
12
|
+
* prompt_injection: { action: 'block', severity: 'high' },
|
|
13
|
+
* secret_detected: { action: 'alert', notify: true }
|
|
14
|
+
* }
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* const decision = await bridge.enforcePolicy({
|
|
18
|
+
* action: 'send_message',
|
|
19
|
+
* agent: 'chatbot',
|
|
20
|
+
* context: { findings: clawMoatScanResults }
|
|
21
|
+
* });
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* OWASP Agentic Top 10 threat categories mapped to ClawMoat findings
|
|
29
|
+
*/
|
|
30
|
+
const OWASP_AGENTIC_MAPPING = {
|
|
31
|
+
// LLM01: Prompt Injection
|
|
32
|
+
'prompt_injection': 'LLM01',
|
|
33
|
+
'jailbreak': 'LLM01',
|
|
34
|
+
'embedded_injection': 'LLM01',
|
|
35
|
+
|
|
36
|
+
// LLM02: Insecure Output Handling
|
|
37
|
+
'secret_detected': 'LLM02',
|
|
38
|
+
'pii_detected': 'LLM02',
|
|
39
|
+
'credential_leak': 'LLM02',
|
|
40
|
+
|
|
41
|
+
// LLM03: Training Data Poisoning
|
|
42
|
+
'memory_poison': 'LLM03',
|
|
43
|
+
'context_poison': 'LLM03',
|
|
44
|
+
|
|
45
|
+
// LLM04: Model Denial of Service
|
|
46
|
+
'size_anomaly': 'LLM04',
|
|
47
|
+
'excessive_tokens': 'LLM04',
|
|
48
|
+
|
|
49
|
+
// LLM06: Sensitive Information Disclosure
|
|
50
|
+
'data_exfiltration': 'LLM06',
|
|
51
|
+
'unauthorized_access': 'LLM06',
|
|
52
|
+
|
|
53
|
+
// LLM07: Insecure Plugin Design
|
|
54
|
+
'supply_chain_threat': 'LLM07',
|
|
55
|
+
'malicious_plugin': 'LLM07',
|
|
56
|
+
|
|
57
|
+
// LLM08: Excessive Agency
|
|
58
|
+
'excessive_agency': 'LLM08',
|
|
59
|
+
'privilege_escalation': 'LLM08',
|
|
60
|
+
|
|
61
|
+
// LLM09: Overreliance
|
|
62
|
+
'confidence_manipulation': 'LLM09',
|
|
63
|
+
|
|
64
|
+
// LLM10: Model Theft
|
|
65
|
+
'model_extraction': 'LLM10',
|
|
66
|
+
'api_abuse': 'LLM10'
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Default governance policies for different threat types
|
|
71
|
+
*/
|
|
72
|
+
const DEFAULT_POLICIES = {
|
|
73
|
+
// High-risk threats - immediate action
|
|
74
|
+
'prompt_injection': {
|
|
75
|
+
action: 'block',
|
|
76
|
+
severity: 'high',
|
|
77
|
+
notify: true,
|
|
78
|
+
log: true
|
|
79
|
+
},
|
|
80
|
+
'jailbreak': {
|
|
81
|
+
action: 'block',
|
|
82
|
+
severity: 'high',
|
|
83
|
+
notify: true,
|
|
84
|
+
log: true
|
|
85
|
+
},
|
|
86
|
+
'secret_detected': {
|
|
87
|
+
action: 'block',
|
|
88
|
+
severity: 'critical',
|
|
89
|
+
notify: true,
|
|
90
|
+
log: true,
|
|
91
|
+
redact: true
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
// Medium-risk threats - alert and log
|
|
95
|
+
'memory_poison': {
|
|
96
|
+
action: 'alert',
|
|
97
|
+
severity: 'medium',
|
|
98
|
+
notify: true,
|
|
99
|
+
log: true
|
|
100
|
+
},
|
|
101
|
+
'excessive_agency': {
|
|
102
|
+
action: 'alert',
|
|
103
|
+
severity: 'medium',
|
|
104
|
+
notify: true,
|
|
105
|
+
log: true
|
|
106
|
+
},
|
|
107
|
+
'supply_chain_threat': {
|
|
108
|
+
action: 'alert',
|
|
109
|
+
severity: 'high',
|
|
110
|
+
notify: true,
|
|
111
|
+
log: true,
|
|
112
|
+
quarantine: true
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
// Low-risk threats - log only
|
|
116
|
+
'steganographic_pattern': {
|
|
117
|
+
action: 'log',
|
|
118
|
+
severity: 'low',
|
|
119
|
+
notify: false,
|
|
120
|
+
log: true
|
|
121
|
+
},
|
|
122
|
+
'suspicious_file_extension': {
|
|
123
|
+
action: 'log',
|
|
124
|
+
severity: 'medium',
|
|
125
|
+
notify: false,
|
|
126
|
+
log: true
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Governance actions available for policy enforcement
|
|
132
|
+
*/
|
|
133
|
+
const GOVERNANCE_ACTIONS = {
|
|
134
|
+
ALLOW: 'allow',
|
|
135
|
+
BLOCK: 'block',
|
|
136
|
+
ALERT: 'alert',
|
|
137
|
+
LOG: 'log',
|
|
138
|
+
QUARANTINE: 'quarantine',
|
|
139
|
+
REDACT: 'redact'
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @typedef {Object} PolicyRule
|
|
144
|
+
* @property {string} action - Governance action to take (allow/block/alert/log)
|
|
145
|
+
* @property {string} severity - Severity level (low/medium/high/critical)
|
|
146
|
+
* @property {boolean} notify - Whether to send notifications
|
|
147
|
+
* @property {boolean} log - Whether to log the event
|
|
148
|
+
* @property {boolean} [redact] - Whether to redact sensitive content
|
|
149
|
+
* @property {boolean} [quarantine] - Whether to quarantine the content/agent
|
|
150
|
+
* @property {string[]} [exceptions] - List of exceptions that override this rule
|
|
151
|
+
*/
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* @typedef {Object} EnforcementContext
|
|
155
|
+
* @property {string} action - The action being attempted (e.g., 'send_message', 'execute_tool')
|
|
156
|
+
* @property {string} agent - Agent ID or name
|
|
157
|
+
* @property {Object} [findings] - ClawMoat scan findings
|
|
158
|
+
* @property {string} [content] - Content being processed
|
|
159
|
+
* @property {Object} [metadata] - Additional context metadata
|
|
160
|
+
*/
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @typedef {Object} PolicyDecision
|
|
164
|
+
* @property {string} decision - Final governance decision (allow/block/alert/log)
|
|
165
|
+
* @property {string} reason - Human-readable explanation
|
|
166
|
+
* @property {string[]} triggeredRules - List of policy rules that matched
|
|
167
|
+
* @property {string} owaspCategory - OWASP Agentic Top 10 category
|
|
168
|
+
* @property {Object} actions - Specific actions to take
|
|
169
|
+
* @property {number} timestamp - Unix timestamp of decision
|
|
170
|
+
*/
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Agent governance policy engine that bridges ClawMoat findings with policy decisions
|
|
174
|
+
*/
|
|
175
|
+
class AgentMeshBridge {
|
|
176
|
+
/**
|
|
177
|
+
* @param {Object} options - Configuration options
|
|
178
|
+
* @param {Object} [options.policies] - Custom policy rules
|
|
179
|
+
* @param {string} [options.policyFile] - Path to YAML/JSON policy file
|
|
180
|
+
* @param {string} [options.logFile] - Path to governance log file
|
|
181
|
+
* @param {boolean} [options.strict] - Strict mode (deny by default)
|
|
182
|
+
* @param {Function} [options.onDecision] - Callback for policy decisions
|
|
183
|
+
*/
|
|
184
|
+
constructor(options = {}) {
|
|
185
|
+
this.policies = { ...DEFAULT_POLICIES };
|
|
186
|
+
this.strict = options.strict || false;
|
|
187
|
+
this.logFile = options.logFile || null;
|
|
188
|
+
this.onDecision = options.onDecision || null;
|
|
189
|
+
|
|
190
|
+
// Load custom policies
|
|
191
|
+
if (options.policies) {
|
|
192
|
+
this.policies = { ...this.policies, ...options.policies };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Load policies from file
|
|
196
|
+
if (options.policyFile) {
|
|
197
|
+
this.loadPoliciesFromFile(options.policyFile);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
this.stats = {
|
|
201
|
+
decisions: 0,
|
|
202
|
+
blocked: 0,
|
|
203
|
+
alerted: 0,
|
|
204
|
+
allowed: 0
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Load governance policies from a file
|
|
210
|
+
* @param {string} filePath - Path to policy file (JSON or YAML)
|
|
211
|
+
*/
|
|
212
|
+
loadPoliciesFromFile(filePath) {
|
|
213
|
+
try {
|
|
214
|
+
if (!fs.existsSync(filePath)) {
|
|
215
|
+
throw new Error(`Policy file not found: ${filePath}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
219
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
220
|
+
|
|
221
|
+
let policies;
|
|
222
|
+
if (ext === '.json') {
|
|
223
|
+
policies = JSON.parse(content);
|
|
224
|
+
} else if (ext === '.yml' || ext === '.yaml') {
|
|
225
|
+
// Simple YAML parser for basic structures
|
|
226
|
+
policies = this.parseSimpleYaml(content);
|
|
227
|
+
} else {
|
|
228
|
+
throw new Error(`Unsupported policy file format: ${ext}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
this.policies = { ...this.policies, ...policies };
|
|
232
|
+
} catch (error) {
|
|
233
|
+
throw new Error(`Failed to load policies from ${filePath}: ${error.message}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Simple YAML parser for policy files (no external dependencies)
|
|
239
|
+
* @param {string} yaml - YAML content
|
|
240
|
+
* @returns {Object} Parsed policies
|
|
241
|
+
*/
|
|
242
|
+
parseSimpleYaml(yaml) {
|
|
243
|
+
const policies = {};
|
|
244
|
+
const lines = yaml.split('\n');
|
|
245
|
+
let currentKey = null;
|
|
246
|
+
let currentPolicy = {};
|
|
247
|
+
|
|
248
|
+
for (const line of lines) {
|
|
249
|
+
const trimmed = line.trim();
|
|
250
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
251
|
+
|
|
252
|
+
if (trimmed.endsWith(':') && !trimmed.startsWith(' ')) {
|
|
253
|
+
// New policy rule
|
|
254
|
+
if (currentKey && Object.keys(currentPolicy).length > 0) {
|
|
255
|
+
policies[currentKey] = currentPolicy;
|
|
256
|
+
}
|
|
257
|
+
currentKey = trimmed.slice(0, -1);
|
|
258
|
+
currentPolicy = {};
|
|
259
|
+
} else if (trimmed.includes(':') && currentKey) {
|
|
260
|
+
// Policy property
|
|
261
|
+
const [key, ...valueParts] = trimmed.split(':');
|
|
262
|
+
let value = valueParts.join(':').trim();
|
|
263
|
+
|
|
264
|
+
// Parse boolean and number values
|
|
265
|
+
if (value === 'true') value = true;
|
|
266
|
+
else if (value === 'false') value = false;
|
|
267
|
+
else if (!isNaN(value)) value = Number(value);
|
|
268
|
+
|
|
269
|
+
currentPolicy[key.trim()] = value;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Add last policy
|
|
274
|
+
if (currentKey && Object.keys(currentPolicy).length > 0) {
|
|
275
|
+
policies[currentKey] = currentPolicy;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return policies;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Enforce governance policy for an agent action
|
|
283
|
+
* @param {EnforcementContext} context - Action context
|
|
284
|
+
* @returns {Promise<PolicyDecision>} Policy decision
|
|
285
|
+
*/
|
|
286
|
+
async enforcePolicy(context) {
|
|
287
|
+
this.stats.decisions++;
|
|
288
|
+
|
|
289
|
+
const decision = {
|
|
290
|
+
decision: 'allow',
|
|
291
|
+
reason: 'No policy violations detected',
|
|
292
|
+
triggeredRules: [],
|
|
293
|
+
owaspCategory: null,
|
|
294
|
+
actions: {
|
|
295
|
+
block: false,
|
|
296
|
+
alert: false,
|
|
297
|
+
log: false,
|
|
298
|
+
notify: false,
|
|
299
|
+
redact: false,
|
|
300
|
+
quarantine: false
|
|
301
|
+
},
|
|
302
|
+
timestamp: Date.now()
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
// Extract findings from context
|
|
306
|
+
const findings = context.findings || [];
|
|
307
|
+
if (findings.length === 0) {
|
|
308
|
+
decision.reason = 'No threats detected';
|
|
309
|
+
this.stats.allowed++;
|
|
310
|
+
await this.logDecision(context, decision);
|
|
311
|
+
return decision;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Evaluate each finding against policies
|
|
315
|
+
let maxSeverityRank = 0;
|
|
316
|
+
const severityRank = { low: 1, medium: 2, high: 3, critical: 4 };
|
|
317
|
+
|
|
318
|
+
for (const finding of findings) {
|
|
319
|
+
const findingType = finding.type || finding.category;
|
|
320
|
+
const policy = this.policies[findingType];
|
|
321
|
+
|
|
322
|
+
if (policy) {
|
|
323
|
+
decision.triggeredRules.push(findingType);
|
|
324
|
+
|
|
325
|
+
// Map to OWASP category
|
|
326
|
+
const owaspCategory = OWASP_AGENTIC_MAPPING[findingType];
|
|
327
|
+
if (owaspCategory && !decision.owaspCategory) {
|
|
328
|
+
decision.owaspCategory = owaspCategory;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Update actions based on policy
|
|
332
|
+
if (policy.action === 'block') {
|
|
333
|
+
decision.decision = 'block';
|
|
334
|
+
decision.actions.block = true;
|
|
335
|
+
} else if (policy.action === 'alert' && decision.decision !== 'block') {
|
|
336
|
+
decision.decision = 'alert';
|
|
337
|
+
decision.actions.alert = true;
|
|
338
|
+
} else if (policy.action === 'log') {
|
|
339
|
+
decision.actions.log = true;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Set notification and other flags
|
|
343
|
+
if (policy.notify) decision.actions.notify = true;
|
|
344
|
+
if (policy.redact) decision.actions.redact = true;
|
|
345
|
+
if (policy.quarantine) decision.actions.quarantine = true;
|
|
346
|
+
if (policy.log) decision.actions.log = true;
|
|
347
|
+
|
|
348
|
+
// Track maximum severity
|
|
349
|
+
const rank = severityRank[policy.severity] || 0;
|
|
350
|
+
if (rank > maxSeverityRank) {
|
|
351
|
+
maxSeverityRank = rank;
|
|
352
|
+
}
|
|
353
|
+
} else if (this.strict) {
|
|
354
|
+
// In strict mode, unknown threat types trigger alerts
|
|
355
|
+
decision.decision = 'alert';
|
|
356
|
+
decision.actions.alert = true;
|
|
357
|
+
decision.actions.log = true;
|
|
358
|
+
decision.triggeredRules.push(`unknown_threat:${findingType}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Generate human-readable reason
|
|
363
|
+
if (decision.triggeredRules.length > 0) {
|
|
364
|
+
const severityMap = { 1: 'low', 2: 'medium', 3: 'high', 4: 'critical' };
|
|
365
|
+
const maxSeverity = severityMap[maxSeverityRank] || 'unknown';
|
|
366
|
+
|
|
367
|
+
decision.reason = `${decision.triggeredRules.length} policy violation(s) detected ` +
|
|
368
|
+
`(${decision.triggeredRules.join(', ')}) with ${maxSeverity} severity`;
|
|
369
|
+
|
|
370
|
+
if (decision.owaspCategory) {
|
|
371
|
+
decision.reason += ` - maps to OWASP ${decision.owaspCategory}`;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Update stats
|
|
376
|
+
if (decision.decision === 'block') this.stats.blocked++;
|
|
377
|
+
else if (decision.decision === 'alert') this.stats.alerted++;
|
|
378
|
+
else this.stats.allowed++;
|
|
379
|
+
|
|
380
|
+
// Log decision
|
|
381
|
+
await this.logDecision(context, decision);
|
|
382
|
+
|
|
383
|
+
// Execute callback if provided
|
|
384
|
+
if (this.onDecision) {
|
|
385
|
+
try {
|
|
386
|
+
await this.onDecision(context, decision);
|
|
387
|
+
} catch (error) {
|
|
388
|
+
console.error('Policy decision callback failed:', error.message);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return decision;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Get governance statistics
|
|
397
|
+
* @returns {Object} Stats summary
|
|
398
|
+
*/
|
|
399
|
+
getStats() {
|
|
400
|
+
return { ...this.stats };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Add or update a policy rule
|
|
405
|
+
* @param {string} threatType - Threat type to add policy for
|
|
406
|
+
* @param {PolicyRule} policy - Policy rule configuration
|
|
407
|
+
*/
|
|
408
|
+
setPolicy(threatType, policy) {
|
|
409
|
+
this.policies[threatType] = policy;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Remove a policy rule
|
|
414
|
+
* @param {string} threatType - Threat type to remove policy for
|
|
415
|
+
*/
|
|
416
|
+
removePolicy(threatType) {
|
|
417
|
+
delete this.policies[threatType];
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Get all current policies
|
|
422
|
+
* @returns {Object} Current policy rules
|
|
423
|
+
*/
|
|
424
|
+
getPolicies() {
|
|
425
|
+
return { ...this.policies };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Map ClawMoat finding to OWASP Agentic Top 10 category
|
|
430
|
+
* @param {string} findingType - ClawMoat finding type
|
|
431
|
+
* @returns {string|null} OWASP category or null if not mapped
|
|
432
|
+
*/
|
|
433
|
+
getOwaspCategory(findingType) {
|
|
434
|
+
return OWASP_AGENTIC_MAPPING[findingType] || null;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Log governance decision
|
|
439
|
+
* @private
|
|
440
|
+
*/
|
|
441
|
+
async logDecision(context, decision) {
|
|
442
|
+
if (!this.logFile) return;
|
|
443
|
+
|
|
444
|
+
const logEntry = {
|
|
445
|
+
timestamp: decision.timestamp,
|
|
446
|
+
agent: context.agent,
|
|
447
|
+
action: context.action,
|
|
448
|
+
decision: decision.decision,
|
|
449
|
+
reason: decision.reason,
|
|
450
|
+
owaspCategory: decision.owaspCategory,
|
|
451
|
+
triggeredRules: decision.triggeredRules,
|
|
452
|
+
findings: context.findings ? context.findings.length : 0
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
try {
|
|
456
|
+
const logLine = JSON.stringify(logEntry) + '\n';
|
|
457
|
+
fs.appendFileSync(this.logFile, logLine);
|
|
458
|
+
} catch (error) {
|
|
459
|
+
console.error('Failed to write governance log:', error.message);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Read governance decision log
|
|
465
|
+
* @param {Object} [filter] - Optional filter criteria
|
|
466
|
+
* @returns {Object[]} Array of log entries
|
|
467
|
+
*/
|
|
468
|
+
getDecisionLog(filter = {}) {
|
|
469
|
+
if (!this.logFile || !fs.existsSync(this.logFile)) {
|
|
470
|
+
return [];
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
try {
|
|
474
|
+
const content = fs.readFileSync(this.logFile, 'utf8');
|
|
475
|
+
const entries = content
|
|
476
|
+
.split('\n')
|
|
477
|
+
.filter(line => line.trim())
|
|
478
|
+
.map(line => JSON.parse(line));
|
|
479
|
+
|
|
480
|
+
// Apply filters
|
|
481
|
+
return entries.filter(entry => {
|
|
482
|
+
if (filter.agent && entry.agent !== filter.agent) return false;
|
|
483
|
+
if (filter.decision && entry.decision !== filter.decision) return false;
|
|
484
|
+
if (filter.owaspCategory && entry.owaspCategory !== filter.owaspCategory) return false;
|
|
485
|
+
if (filter.since && entry.timestamp < filter.since) return false;
|
|
486
|
+
if (filter.until && entry.timestamp > filter.until) return false;
|
|
487
|
+
return true;
|
|
488
|
+
});
|
|
489
|
+
} catch (error) {
|
|
490
|
+
console.error('Failed to read governance log:', error.message);
|
|
491
|
+
return [];
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
module.exports = {
|
|
497
|
+
AgentMeshBridge,
|
|
498
|
+
OWASP_AGENTIC_MAPPING,
|
|
499
|
+
DEFAULT_POLICIES,
|
|
500
|
+
GOVERNANCE_ACTIONS
|
|
501
|
+
};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language Detection Scanner
|
|
3
|
+
*
|
|
4
|
+
* Detects language of input text and flags anomalies:
|
|
5
|
+
* - Unexpected language switches (English-only agent gets Chinese instructions)
|
|
6
|
+
* - Mixed-language prompt injection attempts
|
|
7
|
+
* - Character set anomalies
|
|
8
|
+
*
|
|
9
|
+
* Stolen from LLM Guard's concept, implemented lightweight for JS (no ML model).
|
|
10
|
+
* Uses Unicode script detection + trigram frequency analysis.
|
|
11
|
+
*
|
|
12
|
+
* @module language-detector
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
// Unicode ranges for major scripts
|
|
18
|
+
const SCRIPTS = {
|
|
19
|
+
latin: { re: /[\u0041-\u005A\u0061-\u007A\u00C0-\u024F\u1E00-\u1EFF]/g, name: 'Latin' },
|
|
20
|
+
cyrillic: { re: /[\u0400-\u04FF\u0500-\u052F]/g, name: 'Cyrillic' },
|
|
21
|
+
chinese: { re: /[\u4E00-\u9FFF\u3400-\u4DBF\u{20000}-\u{2A6DF}]/gu, name: 'Chinese' },
|
|
22
|
+
arabic: { re: /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF]/g, name: 'Arabic' },
|
|
23
|
+
devanagari: { re: /[\u0900-\u097F]/g, name: 'Devanagari' },
|
|
24
|
+
japanese: { re: /[\u3040-\u309F\u30A0-\u30FF]/g, name: 'Japanese' },
|
|
25
|
+
korean: { re: /[\uAC00-\uD7AF\u1100-\u11FF\u3130-\u318F]/g, name: 'Korean' },
|
|
26
|
+
greek: { re: /[\u0370-\u03FF\u1F00-\u1FFF]/g, name: 'Greek' },
|
|
27
|
+
thai: { re: /[\u0E00-\u0E7F]/g, name: 'Thai' },
|
|
28
|
+
hebrew: { re: /[\u0590-\u05FF]/g, name: 'Hebrew' },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Common English trigrams (top 30) for basic language ID
|
|
32
|
+
const ENGLISH_TRIGRAMS = new Set([
|
|
33
|
+
'the', 'and', 'ing', 'ent', 'ion', 'tio', 'for', 'ati', 'ter', 'hat',
|
|
34
|
+
'tha', 'ere', 'ate', 'his', 'con', 'res', 'ver', 'all', 'ons', 'nce',
|
|
35
|
+
'men', 'ith', 'ted', 'ers', 'pro', 'thi', 'wit', 'are', 'ess', 'not',
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Detect scripts present in text and their proportions
|
|
40
|
+
* @param {string} text - Text to analyze
|
|
41
|
+
* @returns {Object} { scripts: [{name, count, percentage}], dominant, totalChars }
|
|
42
|
+
*/
|
|
43
|
+
function detectScripts(text) {
|
|
44
|
+
const results = [];
|
|
45
|
+
let totalScriptChars = 0;
|
|
46
|
+
|
|
47
|
+
for (const [key, { re, name }] of Object.entries(SCRIPTS)) {
|
|
48
|
+
const matches = text.match(re);
|
|
49
|
+
const count = matches ? matches.length : 0;
|
|
50
|
+
if (count > 0) {
|
|
51
|
+
results.push({ key, name, count });
|
|
52
|
+
totalScriptChars += count;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Sort by count descending
|
|
57
|
+
results.sort((a, b) => b.count - a.count);
|
|
58
|
+
|
|
59
|
+
// Add percentages
|
|
60
|
+
for (const r of results) {
|
|
61
|
+
r.percentage = totalScriptChars > 0 ? Math.round((r.count / totalScriptChars) * 100) : 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
scripts: results,
|
|
66
|
+
dominant: results.length > 0 ? results[0].name : 'Unknown',
|
|
67
|
+
totalChars: totalScriptChars,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Simple English confidence score using trigram frequency
|
|
73
|
+
* @param {string} text - Text to check
|
|
74
|
+
* @returns {number} 0-1 confidence that text is English
|
|
75
|
+
*/
|
|
76
|
+
function englishConfidence(text) {
|
|
77
|
+
const lower = text.toLowerCase().replace(/[^a-z\s]/g, '');
|
|
78
|
+
if (lower.length < 10) return 0.5; // Too short to tell
|
|
79
|
+
|
|
80
|
+
const words = lower.split(/\s+/).filter(w => w.length >= 3);
|
|
81
|
+
if (words.length === 0) return 0;
|
|
82
|
+
|
|
83
|
+
let trigramHits = 0;
|
|
84
|
+
let totalTrigrams = 0;
|
|
85
|
+
|
|
86
|
+
for (const word of words) {
|
|
87
|
+
for (let i = 0; i <= word.length - 3; i++) {
|
|
88
|
+
totalTrigrams++;
|
|
89
|
+
if (ENGLISH_TRIGRAMS.has(word.substring(i, i + 3))) {
|
|
90
|
+
trigramHits++;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return totalTrigrams > 0 ? Math.min(1, trigramHits / totalTrigrams * 3) : 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Scan text for language anomalies
|
|
100
|
+
* @param {string} text - Text to scan
|
|
101
|
+
* @param {Object} [opts] - Options
|
|
102
|
+
* @param {string[]} [opts.expectedLanguages=['latin']] - Expected script keys
|
|
103
|
+
* @param {number} [opts.anomalyThreshold=0.15] - Min percentage of unexpected script to flag
|
|
104
|
+
* @param {boolean} [opts.allowMixed=false] - Allow mixed scripts without flagging
|
|
105
|
+
* @returns {Object} { safe, findings, scripts, dominant }
|
|
106
|
+
*/
|
|
107
|
+
function scanLanguage(text, opts = {}) {
|
|
108
|
+
const {
|
|
109
|
+
expectedLanguages = ['latin'],
|
|
110
|
+
anomalyThreshold = 0.15,
|
|
111
|
+
allowMixed = false,
|
|
112
|
+
} = opts;
|
|
113
|
+
|
|
114
|
+
const detection = detectScripts(text);
|
|
115
|
+
const findings = [];
|
|
116
|
+
|
|
117
|
+
if (detection.totalChars < 5) {
|
|
118
|
+
return { safe: true, findings: [], scripts: detection.scripts, dominant: detection.dominant };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Check for unexpected scripts
|
|
122
|
+
const unexpectedScripts = detection.scripts.filter(s => {
|
|
123
|
+
const pct = s.count / detection.totalChars;
|
|
124
|
+
return !expectedLanguages.includes(s.key) && pct >= anomalyThreshold;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (unexpectedScripts.length > 0 && !allowMixed) {
|
|
128
|
+
const names = unexpectedScripts.map(s => `${s.name} (${s.percentage}%)`).join(', ');
|
|
129
|
+
findings.push({
|
|
130
|
+
type: 'language_anomaly',
|
|
131
|
+
subtype: 'unexpected_script',
|
|
132
|
+
severity: 'medium',
|
|
133
|
+
confidence: 0.7,
|
|
134
|
+
evidence: `Unexpected script(s) detected: ${names}. Expected: ${expectedLanguages.join(', ')}`,
|
|
135
|
+
scripts: unexpectedScripts.map(s => s.name),
|
|
136
|
+
recommended_action: 'flag_for_review',
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Check for script switching mid-text (potential injection)
|
|
141
|
+
if (detection.scripts.length >= 2) {
|
|
142
|
+
// Look for abrupt transitions — split text into chunks and check script consistency
|
|
143
|
+
const chunks = text.match(/.{1,50}/g) || [];
|
|
144
|
+
let scriptSwitches = 0;
|
|
145
|
+
let lastDominant = null;
|
|
146
|
+
|
|
147
|
+
for (const chunk of chunks) {
|
|
148
|
+
const chunkDetection = detectScripts(chunk);
|
|
149
|
+
if (chunkDetection.dominant !== 'Unknown') {
|
|
150
|
+
if (lastDominant && chunkDetection.dominant !== lastDominant) {
|
|
151
|
+
scriptSwitches++;
|
|
152
|
+
}
|
|
153
|
+
lastDominant = chunkDetection.dominant;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (scriptSwitches >= 3) {
|
|
158
|
+
findings.push({
|
|
159
|
+
type: 'language_anomaly',
|
|
160
|
+
subtype: 'frequent_script_switching',
|
|
161
|
+
severity: 'high',
|
|
162
|
+
confidence: 0.75,
|
|
163
|
+
evidence: `Text switches between scripts ${scriptSwitches} times across ${chunks.length} segments — possible multilingual injection`,
|
|
164
|
+
switches: scriptSwitches,
|
|
165
|
+
recommended_action: 'block',
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Check if predominantly non-Latin text contains embedded Latin command-like strings
|
|
171
|
+
if (detection.dominant !== 'Latin' && detection.scripts.some(s => s.key === 'latin')) {
|
|
172
|
+
const latinPortion = text.match(/[a-zA-Z\s]{10,}/g) || [];
|
|
173
|
+
const suspiciousCommands = latinPortion.filter(p =>
|
|
174
|
+
/ignore|override|system|prompt|exec|eval|admin|password|secret|token/i.test(p)
|
|
175
|
+
);
|
|
176
|
+
if (suspiciousCommands.length > 0) {
|
|
177
|
+
findings.push({
|
|
178
|
+
type: 'language_anomaly',
|
|
179
|
+
subtype: 'embedded_command_in_foreign_text',
|
|
180
|
+
severity: 'high',
|
|
181
|
+
confidence: 0.8,
|
|
182
|
+
evidence: `Found command-like Latin text embedded in ${detection.dominant} content: "${suspiciousCommands[0].trim().substring(0, 60)}"`,
|
|
183
|
+
recommended_action: 'block',
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
safe: findings.length === 0,
|
|
190
|
+
findings,
|
|
191
|
+
scripts: detection.scripts,
|
|
192
|
+
dominant: detection.dominant,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
module.exports = {
|
|
197
|
+
scanLanguage,
|
|
198
|
+
detectScripts,
|
|
199
|
+
englishConfidence,
|
|
200
|
+
SCRIPTS,
|
|
201
|
+
};
|