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.
Files changed (171) hide show
  1. package/.dockerignore +9 -0
  2. package/CHANGELOG.md +18 -0
  3. package/DEMO.md +87 -0
  4. package/Dockerfile +5 -18
  5. package/README.md +232 -8
  6. package/THREAT_MODEL.md +129 -0
  7. package/agent/README.md +131 -0
  8. package/agent/index.js +471 -0
  9. package/agent/install-service.sh +94 -0
  10. package/agent/openclaw-hook.js +453 -0
  11. package/agent/provider-setup.js +649 -0
  12. package/agent/setup.js +274 -0
  13. package/assets/BADGE-USAGE.md +20 -0
  14. package/assets/clawmoat-badge.svg +21 -0
  15. package/bin/clawmoat.js +468 -111
  16. package/docs/affiliates/dashboard.html +124 -0
  17. package/docs/affiliates/index.html +236 -0
  18. package/docs/agent-install.html +183 -0
  19. package/docs/ai-agent-security-scanner.html +10 -6
  20. package/docs/badge/index.html +149 -0
  21. package/docs/badge/scanning.svg +23 -0
  22. package/docs/blog/386-malicious-skills.html +11 -4
  23. package/docs/blog/40000-exposed-openclaw-instances.html +11 -4
  24. package/docs/blog/agent-trust-protocol.html +5 -4
  25. package/docs/blog/ai-agent-earns-commissions.html +230 -0
  26. package/docs/blog/bugmageddon-agent-firewall.html +174 -0
  27. package/docs/blog/calculator-math.html +180 -0
  28. package/docs/blog/clawmoat-vs-llamafirewall-nemo-guardrails.html +10 -4
  29. package/docs/blog/host-guardian-launch.html +18 -8
  30. package/docs/blog/ibm-experts-agent-runtime-protection.html +15 -6
  31. package/docs/blog/index.html +67 -9
  32. package/docs/blog/langchain-security-tutorial.html +18 -8
  33. package/docs/blog/mcp-30-cves-security-crisis.html +11 -4
  34. package/docs/blog/meta-researcher-rogue-agent.html +201 -0
  35. package/docs/blog/microsoft-openclaw-workstation-security.html +5 -4
  36. package/docs/blog/nist-ai-agent-standards-clawmoat.html +16 -8
  37. package/docs/blog/oasis-websocket-hijack.html +11 -4
  38. package/docs/blog/ollama-openclaw-security.html +10 -4
  39. package/docs/blog/openclaw-enterprise-readiness-claw10.html +5 -4
  40. package/docs/blog/openclaw-security-reckoning-2026.html +11 -4
  41. package/docs/blog/owasp-agentic-ai-top10.html +18 -8
  42. package/docs/blog/securing-ai-agents.html +18 -8
  43. package/docs/blog/supply-chain-agents.html +18 -8
  44. package/docs/business/index.html +11 -16
  45. package/docs/business/install.html +21 -7
  46. package/docs/checklist.html +10 -4
  47. package/docs/compare/index.html +122 -0
  48. package/docs/compare/lakera/index.html +62 -0
  49. package/docs/compare/llm-guard/index.html +49 -0
  50. package/docs/compare/snyk-agent-scan/index.html +63 -0
  51. package/docs/compare.html +10 -6
  52. package/docs/dashboard/index.html +520 -0
  53. package/docs/finance/index.html +9 -6
  54. package/docs/guides/business-deployment.html +770 -0
  55. package/docs/hall-of-fame.html +11 -5
  56. package/docs/index.html +266 -137
  57. package/docs/integrations/langchain.html +14 -6
  58. package/docs/integrations/openai.html +14 -6
  59. package/docs/integrations/openclaw.html +55 -7
  60. package/docs/plans/2026-03-26-threat-intel-api.md +255 -0
  61. package/docs/plans/2026-04-14-bugmageddon-marketing-pack.md +329 -0
  62. package/docs/plans/2026-04-14-clawmoat-v1-bugmageddon.md +248 -0
  63. package/docs/plans/2026-04-14-v1-release-update.md +91 -0
  64. package/docs/plans/2026-04-19-supabase-audit.md +68 -0
  65. package/docs/plans/2026-05-12-sales-push.md +303 -0
  66. package/docs/playground/index.html +893 -0
  67. package/docs/playground.html +4 -7
  68. package/docs/rfcs/defense-in-depth.md +467 -0
  69. package/docs/scan/index.html +156 -12
  70. package/docs/services/case-study.html +255 -0
  71. package/docs/services/downloads/install-openclaw.bat +45 -0
  72. package/docs/services/downloads/install-openclaw.command +38 -0
  73. package/docs/services/downloads/install-openclaw.sh +38 -0
  74. package/docs/services/get-started.html +165 -0
  75. package/docs/services/index.html +598 -0
  76. package/docs/services/multi-agent-security.html +284 -0
  77. package/docs/services/one-pager.html +99 -0
  78. package/docs/services/pitch-deck.html +229 -0
  79. package/docs/services/roi-calculator.html +258 -0
  80. package/docs/sitemap.xml +62 -2
  81. package/docs/support/index.html +12 -1
  82. package/docs/templates/customer-service/HEARTBEAT.md +61 -0
  83. package/docs/templates/customer-service/MEMORY.md +89 -0
  84. package/docs/templates/customer-service/SOUL.md +41 -0
  85. package/docs/templates/customer-service/USER.md +56 -0
  86. package/docs/templates/executive/HEARTBEAT.md +86 -0
  87. package/docs/templates/executive/MEMORY.md +92 -0
  88. package/docs/templates/executive/SOUL.md +44 -0
  89. package/docs/templates/executive/USER.md +62 -0
  90. package/docs/templates/finance/HEARTBEAT.md +58 -0
  91. package/docs/templates/finance/MEMORY.md +87 -0
  92. package/docs/templates/finance/SOUL.md +38 -0
  93. package/docs/templates/finance/USER.md +53 -0
  94. package/docs/templates/index.html +115 -0
  95. package/docs/templates/operations/HEARTBEAT.md +63 -0
  96. package/docs/templates/operations/MEMORY.md +68 -0
  97. package/docs/templates/operations/SOUL.md +38 -0
  98. package/docs/templates/operations/USER.md +49 -0
  99. package/docs/templates/sales/HEARTBEAT.md +55 -0
  100. package/docs/templates/sales/MEMORY.md +89 -0
  101. package/docs/templates/sales/SOUL.md +34 -0
  102. package/docs/templates/sales/USER.md +54 -0
  103. package/eslint.config.js +32 -0
  104. package/evals/README.md +29 -0
  105. package/evals/cases.json +390 -0
  106. package/evals/results.md +68 -0
  107. package/evals/run.js +180 -0
  108. package/examples/demo-attack/demo.js +186 -0
  109. package/examples/python-quickstart/README.md +54 -0
  110. package/examples/python-quickstart/clawmoat_client.py +167 -0
  111. package/examples/video-demo/README.md +14 -0
  112. package/examples/video-demo/scene-a-normal.js +29 -0
  113. package/examples/video-demo/scene-b-attack-arrives.js +31 -0
  114. package/examples/video-demo/scene-c-hijack.js +44 -0
  115. package/examples/video-demo/scene-d-clawmoat.js +46 -0
  116. package/integrations/crewai/README.md +32 -0
  117. package/integrations/crewai/clawmoat_crewai/__init__.py +17 -0
  118. package/integrations/crewai/clawmoat_crewai/guard.py +103 -0
  119. package/integrations/crewai/pyproject.toml +21 -0
  120. package/integrations/langchain/README.md +91 -0
  121. package/integrations/langchain/clawmoat_langchain/__init__.py +17 -0
  122. package/integrations/langchain/clawmoat_langchain/callback.py +489 -0
  123. package/integrations/langchain/pyproject.toml +32 -0
  124. package/integrations/litellm/README.md +324 -0
  125. package/integrations/litellm/clawmoat_litellm/__init__.py +21 -0
  126. package/integrations/litellm/clawmoat_litellm/callback.py +329 -0
  127. package/integrations/litellm/clawmoat_litellm/proxy_middleware.py +224 -0
  128. package/integrations/litellm/pyproject.toml +74 -0
  129. package/integrations/openai-agents/README.md +392 -0
  130. package/integrations/openai-agents/clawmoat_openai_agents/__init__.py +20 -0
  131. package/integrations/openai-agents/clawmoat_openai_agents/guardrail.py +431 -0
  132. package/integrations/openai-agents/clawmoat_openai_agents/middleware.py +311 -0
  133. package/integrations/openai-agents/pyproject.toml +76 -0
  134. package/package.json +6 -5
  135. package/plugins/openclaw-adapter/PHASE1.md +439 -0
  136. package/plugins/openclaw-adapter/README.md +103 -0
  137. package/plugins/openclaw-adapter/SPEC.md +1644 -0
  138. package/plugins/openclaw-adapter/package.json +31 -0
  139. package/plugins/openclaw-adapter/src/index.test.ts +226 -0
  140. package/plugins/openclaw-adapter/src/index.ts +140 -0
  141. package/plugins/openclaw-adapter/tsconfig.json +14 -0
  142. package/server/data/threats.json +290 -0
  143. package/server/index.js +142 -7
  144. package/src/adapters/express.js +161 -0
  145. package/src/adapters/index.js +92 -0
  146. package/src/adapters/langchain.js +185 -0
  147. package/src/approval/index.js +456 -0
  148. package/src/ban-scanner.js +200 -0
  149. package/src/boundary-scanner.js +296 -0
  150. package/src/ci-scanner.js +279 -0
  151. package/src/code-scanner.js +245 -0
  152. package/src/enforce.js +166 -0
  153. package/src/formatters/json.js +80 -0
  154. package/src/formatters/sarif.js +388 -0
  155. package/src/guardian/alerts.js +34 -3
  156. package/src/guardian/index.js +41 -2
  157. package/src/index.js +102 -0
  158. package/src/integrations/agentmesh.js +501 -0
  159. package/src/language-detector.js +201 -0
  160. package/src/mcp-scanner.js +253 -0
  161. package/src/multimodal/index.js +579 -0
  162. package/src/obfuscation-scanner.js +457 -0
  163. package/src/policy-engine.js +402 -0
  164. package/src/scanners/dependency-attacks.js +128 -0
  165. package/src/scanners/prompt-injection.js +18 -0
  166. package/src/scanners/supply-chain.js +14 -0
  167. package/src/templates/default-config.yml +90 -0
  168. package/src/vuln-ops/exploitability.js +46 -0
  169. package/src/watch/live-monitor.js +720 -0
  170. package/clawmoat-0.8.0.tgz +0 -0
  171. 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
+ };