clawmoat 0.7.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/CONTRIBUTING.md +4 -2
- package/DEMO.md +87 -0
- package/Dockerfile +5 -18
- package/README.md +294 -8
- package/SECURITY.md +58 -10
- 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 +262 -0
- package/docs/blog/40000-exposed-openclaw-instances.html +201 -0
- package/docs/blog/agent-trust-protocol.html +198 -0
- 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 +229 -0
- package/docs/blog/host-guardian-launch.html +18 -8
- package/docs/blog/ibm-experts-agent-runtime-protection.html +247 -0
- package/docs/blog/index.html +211 -9
- package/docs/blog/langchain-security-tutorial.html +18 -8
- package/docs/blog/mcp-30-cves-security-crisis.html +286 -0
- package/docs/blog/meta-researcher-rogue-agent.html +201 -0
- package/docs/blog/microsoft-openclaw-workstation-security.html +235 -0
- package/docs/blog/nist-ai-agent-standards-clawmoat.html +377 -0
- package/docs/blog/oasis-websocket-hijack.html +212 -0
- package/docs/blog/ollama-openclaw-security.html +160 -0
- package/docs/blog/openclaw-enterprise-readiness-claw10.html +199 -0
- package/docs/blog/openclaw-security-reckoning-2026.html +368 -0
- 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 +525 -0
- package/docs/business/install.html +261 -0
- package/docs/checklist.html +174 -0
- 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 +220 -0
- package/docs/guides/business-deployment.html +770 -0
- package/docs/hall-of-fame.html +174 -0
- package/docs/index.html +447 -154
- package/docs/install.sh +557 -0
- 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/privacy-policy/index.html +122 -0
- package/docs/rfcs/defense-in-depth.md +467 -0
- package/docs/scan/index.html +358 -0
- 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 +192 -2
- package/docs/support/index.html +135 -0
- 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/docs/terms-of-service/index.html +122 -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/basic-usage.js +38 -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 +224 -10
- 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/finance/index.js +585 -0
- package/src/finance/mcp-firewall.js +486 -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/gateway-monitor.js +590 -0
- package/src/guardian/index.js +41 -2
- package/src/index.js +105 -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/src/index.js
CHANGED
|
@@ -30,6 +30,8 @@ const { scanMemoryPoison } = require('./scanners/memory-poison');
|
|
|
30
30
|
const { scanExfiltration } = require('./scanners/exfiltration');
|
|
31
31
|
const { scanExcessiveAgency } = require('./scanners/excessive-agency');
|
|
32
32
|
const { scanSkill, scanSkillContent } = require('./scanners/supply-chain');
|
|
33
|
+
const { scanDependencyAttacks } = require('./scanners/dependency-attacks');
|
|
34
|
+
const { scoreExploitability } = require('./vuln-ops/exploitability');
|
|
33
35
|
const { evaluateToolCall } = require('./policies/engine');
|
|
34
36
|
const { HostGuardian, TIERS } = require('./guardian');
|
|
35
37
|
const { SecurityLogger } = require('./utils/logger');
|
|
@@ -165,6 +167,15 @@ class ClawMoat {
|
|
|
165
167
|
}
|
|
166
168
|
}
|
|
167
169
|
|
|
170
|
+
// Dependency attack patterns (prototype pollution, ReDoS, decompression bombs, JWT manipulation)
|
|
171
|
+
if (this.config.detection?.dependency_attacks !== false) {
|
|
172
|
+
const da = scanDependencyAttacks(text);
|
|
173
|
+
if (!da.clean) {
|
|
174
|
+
results.findings.push(...da.findings);
|
|
175
|
+
results.safe = false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
168
179
|
// Determine action
|
|
169
180
|
if (!results.safe) {
|
|
170
181
|
const maxSev = this._maxSeverity(results.findings);
|
|
@@ -289,6 +300,21 @@ class ClawMoat {
|
|
|
289
300
|
};
|
|
290
301
|
}
|
|
291
302
|
|
|
303
|
+
/**
|
|
304
|
+
* Analyze findings with exploitability-oriented scoring.
|
|
305
|
+
* @param {string} text - Text to scan
|
|
306
|
+
* @param {Object} [context] - Reachability/exposure context
|
|
307
|
+
* @returns {{ safe: boolean, findings: ScanFinding[], exploitability: { score: number, priority: string, reasons: string[] } }}
|
|
308
|
+
*/
|
|
309
|
+
analyzeFindings(text, context = {}) {
|
|
310
|
+
const result = this.scan(text, context);
|
|
311
|
+
return {
|
|
312
|
+
safe: result.safe,
|
|
313
|
+
findings: result.findings,
|
|
314
|
+
exploitability: scoreExploitability(result.findings, context),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
292
318
|
/**
|
|
293
319
|
* Get security event log
|
|
294
320
|
*/
|
|
@@ -348,6 +374,85 @@ module.exports.scanExfiltration = scanExfiltration;
|
|
|
348
374
|
module.exports.scanExcessiveAgency = scanExcessiveAgency;
|
|
349
375
|
module.exports.scanSkill = scanSkill;
|
|
350
376
|
module.exports.scanSkillContent = scanSkillContent;
|
|
377
|
+
module.exports.scanDependencyAttacks = scanDependencyAttacks;
|
|
378
|
+
module.exports.scoreExploitability = scoreExploitability;
|
|
351
379
|
module.exports.evaluateToolCall = evaluateToolCall;
|
|
352
380
|
module.exports.HostGuardian = HostGuardian;
|
|
353
381
|
module.exports.TIERS = TIERS;
|
|
382
|
+
module.exports.GatewayMonitor = require('./guardian/gateway-monitor').GatewayMonitor;
|
|
383
|
+
module.exports.FinanceGuard = require('./finance').FinanceGuard;
|
|
384
|
+
module.exports.McpFirewall = require('./finance/mcp-firewall').McpFirewall;
|
|
385
|
+
module.exports.LiveMonitor = require('./watch/live-monitor').LiveMonitor;
|
|
386
|
+
|
|
387
|
+
// Interactive Approval Workflow
|
|
388
|
+
const { ApprovalWorkflow, NotificationChannel, ConsoleChannel, WebhookChannel, CallbackChannel } = require('./approval');
|
|
389
|
+
module.exports.ApprovalWorkflow = ApprovalWorkflow;
|
|
390
|
+
module.exports.NotificationChannel = NotificationChannel;
|
|
391
|
+
module.exports.ConsoleChannel = ConsoleChannel;
|
|
392
|
+
module.exports.WebhookChannel = WebhookChannel;
|
|
393
|
+
module.exports.CallbackChannel = CallbackChannel;
|
|
394
|
+
|
|
395
|
+
// Multimodal Input Scanning
|
|
396
|
+
const { scanMultimodalInput, scanImageDataUrl, scanFileMetadata } = require('./multimodal');
|
|
397
|
+
module.exports.scanMultimodalInput = scanMultimodalInput;
|
|
398
|
+
module.exports.scanImageDataUrl = scanImageDataUrl;
|
|
399
|
+
module.exports.scanFileMetadata = scanFileMetadata;
|
|
400
|
+
|
|
401
|
+
// AgentMesh Governance Integration
|
|
402
|
+
const { AgentMeshBridge, OWASP_AGENTIC_MAPPING, DEFAULT_POLICIES, GOVERNANCE_ACTIONS } = require('./integrations/agentmesh');
|
|
403
|
+
module.exports.AgentMeshBridge = AgentMeshBridge;
|
|
404
|
+
module.exports.OWASP_AGENTIC_MAPPING = OWASP_AGENTIC_MAPPING;
|
|
405
|
+
module.exports.DEFAULT_POLICIES = DEFAULT_POLICIES;
|
|
406
|
+
module.exports.GOVERNANCE_ACTIONS = GOVERNANCE_ACTIONS;
|
|
407
|
+
|
|
408
|
+
// MCP Scanner
|
|
409
|
+
const mcpScanner = require('./mcp-scanner');
|
|
410
|
+
module.exports.scanMCP = mcpScanner.scanMCP;
|
|
411
|
+
module.exports.discoverMCPConfigs = mcpScanner.discoverMCPConfigs;
|
|
412
|
+
module.exports.parseMCPConfig = mcpScanner.parseMCPConfig;
|
|
413
|
+
module.exports.scanMCPServer = mcpScanner.scanMCPServer;
|
|
414
|
+
|
|
415
|
+
// Enforcement mode
|
|
416
|
+
const enforce = require('./enforce');
|
|
417
|
+
module.exports.enforceInbound = enforce.enforceInbound;
|
|
418
|
+
module.exports.enforceOutbound = enforce.enforceOutbound;
|
|
419
|
+
module.exports.firewall = enforce.middleware;
|
|
420
|
+
module.exports.wrapAgent = enforce.wrap;
|
|
421
|
+
module.exports.ClawMoatBlocked = enforce.ClawMoatBlocked;
|
|
422
|
+
|
|
423
|
+
// Policy Engine
|
|
424
|
+
const { PolicyEngine, DATA_PATTERNS, TOOL_RISK_DEFAULTS } = require('./policy-engine');
|
|
425
|
+
module.exports.PolicyEngine = PolicyEngine;
|
|
426
|
+
module.exports.DATA_PATTERNS = DATA_PATTERNS;
|
|
427
|
+
module.exports.TOOL_RISK_DEFAULTS = TOOL_RISK_DEFAULTS;
|
|
428
|
+
|
|
429
|
+
// Obfuscation Scanner
|
|
430
|
+
const obfuscation = require('./obfuscation-scanner');
|
|
431
|
+
module.exports.scanObfuscation = obfuscation.scanObfuscation;
|
|
432
|
+
module.exports.stripObfuscation = obfuscation.stripObfuscation;
|
|
433
|
+
|
|
434
|
+
// Code Scanner
|
|
435
|
+
const codeScanner = require('./code-scanner');
|
|
436
|
+
module.exports.scanCode = codeScanner.scanCode;
|
|
437
|
+
module.exports.quickToolCheck = codeScanner.quickToolCheck;
|
|
438
|
+
|
|
439
|
+
// Boundary Scanner (Agent Pipeline)
|
|
440
|
+
const boundary = require('./boundary-scanner');
|
|
441
|
+
module.exports.createPipeline = boundary.createPipeline;
|
|
442
|
+
module.exports.createDefaultPipeline = boundary.createDefaultPipeline;
|
|
443
|
+
module.exports.BOUNDARY_STAGES = boundary.STAGES;
|
|
444
|
+
|
|
445
|
+
// Language Detection
|
|
446
|
+
const lang = require('./language-detector');
|
|
447
|
+
module.exports.scanLanguage = lang.scanLanguage;
|
|
448
|
+
module.exports.detectScripts = lang.detectScripts;
|
|
449
|
+
|
|
450
|
+
// Ban Scanner
|
|
451
|
+
const ban = require('./ban-scanner');
|
|
452
|
+
module.exports.createBanScanner = ban.createBanScanner;
|
|
453
|
+
module.exports.BAN_PRESETS = ban.PRESETS;
|
|
454
|
+
|
|
455
|
+
// Framework Adapters (also available via require('clawmoat/adapters'))
|
|
456
|
+
const adapters = require('./adapters');
|
|
457
|
+
module.exports.createGuard = adapters.createGuard;
|
|
458
|
+
module.exports.ClawMoatCallbackHandler = adapters.ClawMoatCallbackHandler;
|
|
@@ -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
|
+
};
|