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.
Files changed (178) hide show
  1. package/.dockerignore +9 -0
  2. package/CHANGELOG.md +18 -0
  3. package/CONTRIBUTING.md +4 -2
  4. package/DEMO.md +87 -0
  5. package/Dockerfile +5 -18
  6. package/README.md +294 -8
  7. package/SECURITY.md +58 -10
  8. package/THREAT_MODEL.md +129 -0
  9. package/agent/README.md +131 -0
  10. package/agent/index.js +471 -0
  11. package/agent/install-service.sh +94 -0
  12. package/agent/openclaw-hook.js +453 -0
  13. package/agent/provider-setup.js +649 -0
  14. package/agent/setup.js +274 -0
  15. package/assets/BADGE-USAGE.md +20 -0
  16. package/assets/clawmoat-badge.svg +21 -0
  17. package/bin/clawmoat.js +468 -111
  18. package/docs/affiliates/dashboard.html +124 -0
  19. package/docs/affiliates/index.html +236 -0
  20. package/docs/agent-install.html +183 -0
  21. package/docs/ai-agent-security-scanner.html +10 -6
  22. package/docs/badge/index.html +149 -0
  23. package/docs/badge/scanning.svg +23 -0
  24. package/docs/blog/386-malicious-skills.html +262 -0
  25. package/docs/blog/40000-exposed-openclaw-instances.html +201 -0
  26. package/docs/blog/agent-trust-protocol.html +198 -0
  27. package/docs/blog/ai-agent-earns-commissions.html +230 -0
  28. package/docs/blog/bugmageddon-agent-firewall.html +174 -0
  29. package/docs/blog/calculator-math.html +180 -0
  30. package/docs/blog/clawmoat-vs-llamafirewall-nemo-guardrails.html +229 -0
  31. package/docs/blog/host-guardian-launch.html +18 -8
  32. package/docs/blog/ibm-experts-agent-runtime-protection.html +247 -0
  33. package/docs/blog/index.html +211 -9
  34. package/docs/blog/langchain-security-tutorial.html +18 -8
  35. package/docs/blog/mcp-30-cves-security-crisis.html +286 -0
  36. package/docs/blog/meta-researcher-rogue-agent.html +201 -0
  37. package/docs/blog/microsoft-openclaw-workstation-security.html +235 -0
  38. package/docs/blog/nist-ai-agent-standards-clawmoat.html +377 -0
  39. package/docs/blog/oasis-websocket-hijack.html +212 -0
  40. package/docs/blog/ollama-openclaw-security.html +160 -0
  41. package/docs/blog/openclaw-enterprise-readiness-claw10.html +199 -0
  42. package/docs/blog/openclaw-security-reckoning-2026.html +368 -0
  43. package/docs/blog/owasp-agentic-ai-top10.html +18 -8
  44. package/docs/blog/securing-ai-agents.html +18 -8
  45. package/docs/blog/supply-chain-agents.html +18 -8
  46. package/docs/business/index.html +525 -0
  47. package/docs/business/install.html +261 -0
  48. package/docs/checklist.html +174 -0
  49. package/docs/compare/index.html +122 -0
  50. package/docs/compare/lakera/index.html +62 -0
  51. package/docs/compare/llm-guard/index.html +49 -0
  52. package/docs/compare/snyk-agent-scan/index.html +63 -0
  53. package/docs/compare.html +10 -6
  54. package/docs/dashboard/index.html +520 -0
  55. package/docs/finance/index.html +220 -0
  56. package/docs/guides/business-deployment.html +770 -0
  57. package/docs/hall-of-fame.html +174 -0
  58. package/docs/index.html +447 -154
  59. package/docs/install.sh +557 -0
  60. package/docs/integrations/langchain.html +14 -6
  61. package/docs/integrations/openai.html +14 -6
  62. package/docs/integrations/openclaw.html +55 -7
  63. package/docs/plans/2026-03-26-threat-intel-api.md +255 -0
  64. package/docs/plans/2026-04-14-bugmageddon-marketing-pack.md +329 -0
  65. package/docs/plans/2026-04-14-clawmoat-v1-bugmageddon.md +248 -0
  66. package/docs/plans/2026-04-14-v1-release-update.md +91 -0
  67. package/docs/plans/2026-04-19-supabase-audit.md +68 -0
  68. package/docs/plans/2026-05-12-sales-push.md +303 -0
  69. package/docs/playground/index.html +893 -0
  70. package/docs/playground.html +4 -7
  71. package/docs/privacy-policy/index.html +122 -0
  72. package/docs/rfcs/defense-in-depth.md +467 -0
  73. package/docs/scan/index.html +358 -0
  74. package/docs/services/case-study.html +255 -0
  75. package/docs/services/downloads/install-openclaw.bat +45 -0
  76. package/docs/services/downloads/install-openclaw.command +38 -0
  77. package/docs/services/downloads/install-openclaw.sh +38 -0
  78. package/docs/services/get-started.html +165 -0
  79. package/docs/services/index.html +598 -0
  80. package/docs/services/multi-agent-security.html +284 -0
  81. package/docs/services/one-pager.html +99 -0
  82. package/docs/services/pitch-deck.html +229 -0
  83. package/docs/services/roi-calculator.html +258 -0
  84. package/docs/sitemap.xml +192 -2
  85. package/docs/support/index.html +135 -0
  86. package/docs/templates/customer-service/HEARTBEAT.md +61 -0
  87. package/docs/templates/customer-service/MEMORY.md +89 -0
  88. package/docs/templates/customer-service/SOUL.md +41 -0
  89. package/docs/templates/customer-service/USER.md +56 -0
  90. package/docs/templates/executive/HEARTBEAT.md +86 -0
  91. package/docs/templates/executive/MEMORY.md +92 -0
  92. package/docs/templates/executive/SOUL.md +44 -0
  93. package/docs/templates/executive/USER.md +62 -0
  94. package/docs/templates/finance/HEARTBEAT.md +58 -0
  95. package/docs/templates/finance/MEMORY.md +87 -0
  96. package/docs/templates/finance/SOUL.md +38 -0
  97. package/docs/templates/finance/USER.md +53 -0
  98. package/docs/templates/index.html +115 -0
  99. package/docs/templates/operations/HEARTBEAT.md +63 -0
  100. package/docs/templates/operations/MEMORY.md +68 -0
  101. package/docs/templates/operations/SOUL.md +38 -0
  102. package/docs/templates/operations/USER.md +49 -0
  103. package/docs/templates/sales/HEARTBEAT.md +55 -0
  104. package/docs/templates/sales/MEMORY.md +89 -0
  105. package/docs/templates/sales/SOUL.md +34 -0
  106. package/docs/templates/sales/USER.md +54 -0
  107. package/docs/terms-of-service/index.html +122 -0
  108. package/eslint.config.js +32 -0
  109. package/evals/README.md +29 -0
  110. package/evals/cases.json +390 -0
  111. package/evals/results.md +68 -0
  112. package/evals/run.js +180 -0
  113. package/examples/basic-usage.js +38 -0
  114. package/examples/demo-attack/demo.js +186 -0
  115. package/examples/python-quickstart/README.md +54 -0
  116. package/examples/python-quickstart/clawmoat_client.py +167 -0
  117. package/examples/video-demo/README.md +14 -0
  118. package/examples/video-demo/scene-a-normal.js +29 -0
  119. package/examples/video-demo/scene-b-attack-arrives.js +31 -0
  120. package/examples/video-demo/scene-c-hijack.js +44 -0
  121. package/examples/video-demo/scene-d-clawmoat.js +46 -0
  122. package/integrations/crewai/README.md +32 -0
  123. package/integrations/crewai/clawmoat_crewai/__init__.py +17 -0
  124. package/integrations/crewai/clawmoat_crewai/guard.py +103 -0
  125. package/integrations/crewai/pyproject.toml +21 -0
  126. package/integrations/langchain/README.md +91 -0
  127. package/integrations/langchain/clawmoat_langchain/__init__.py +17 -0
  128. package/integrations/langchain/clawmoat_langchain/callback.py +489 -0
  129. package/integrations/langchain/pyproject.toml +32 -0
  130. package/integrations/litellm/README.md +324 -0
  131. package/integrations/litellm/clawmoat_litellm/__init__.py +21 -0
  132. package/integrations/litellm/clawmoat_litellm/callback.py +329 -0
  133. package/integrations/litellm/clawmoat_litellm/proxy_middleware.py +224 -0
  134. package/integrations/litellm/pyproject.toml +74 -0
  135. package/integrations/openai-agents/README.md +392 -0
  136. package/integrations/openai-agents/clawmoat_openai_agents/__init__.py +20 -0
  137. package/integrations/openai-agents/clawmoat_openai_agents/guardrail.py +431 -0
  138. package/integrations/openai-agents/clawmoat_openai_agents/middleware.py +311 -0
  139. package/integrations/openai-agents/pyproject.toml +76 -0
  140. package/package.json +6 -5
  141. package/plugins/openclaw-adapter/PHASE1.md +439 -0
  142. package/plugins/openclaw-adapter/README.md +103 -0
  143. package/plugins/openclaw-adapter/SPEC.md +1644 -0
  144. package/plugins/openclaw-adapter/package.json +31 -0
  145. package/plugins/openclaw-adapter/src/index.test.ts +226 -0
  146. package/plugins/openclaw-adapter/src/index.ts +140 -0
  147. package/plugins/openclaw-adapter/tsconfig.json +14 -0
  148. package/server/data/threats.json +290 -0
  149. package/server/index.js +224 -10
  150. package/src/adapters/express.js +161 -0
  151. package/src/adapters/index.js +92 -0
  152. package/src/adapters/langchain.js +185 -0
  153. package/src/approval/index.js +456 -0
  154. package/src/ban-scanner.js +200 -0
  155. package/src/boundary-scanner.js +296 -0
  156. package/src/ci-scanner.js +279 -0
  157. package/src/code-scanner.js +245 -0
  158. package/src/enforce.js +166 -0
  159. package/src/finance/index.js +585 -0
  160. package/src/finance/mcp-firewall.js +486 -0
  161. package/src/formatters/json.js +80 -0
  162. package/src/formatters/sarif.js +388 -0
  163. package/src/guardian/alerts.js +34 -3
  164. package/src/guardian/gateway-monitor.js +590 -0
  165. package/src/guardian/index.js +41 -2
  166. package/src/index.js +105 -0
  167. package/src/integrations/agentmesh.js +501 -0
  168. package/src/language-detector.js +201 -0
  169. package/src/mcp-scanner.js +253 -0
  170. package/src/multimodal/index.js +579 -0
  171. package/src/obfuscation-scanner.js +457 -0
  172. package/src/policy-engine.js +402 -0
  173. package/src/scanners/dependency-attacks.js +128 -0
  174. package/src/scanners/prompt-injection.js +18 -0
  175. package/src/scanners/supply-chain.js +14 -0
  176. package/src/templates/default-config.yml +90 -0
  177. package/src/vuln-ops/exploitability.js +46 -0
  178. package/src/watch/live-monitor.js +720 -0
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Agent Boundary Scanner
3
+ *
4
+ * Formalizes scanning at every agent boundary:
5
+ * - pre-input: User/external → Agent (prompt injection, obfuscation, jailbreak)
6
+ * - pre-model: Agent → LLM (prompt leakage, system prompt exposure)
7
+ * - pre-tool-call: LLM → Tool (dangerous commands, exfil, excessive agency)
8
+ * - post-tool-result: Tool → LLM (poisoned results, injected instructions)
9
+ * - pre-output: Agent → User/external (secret leakage, PII, data exfil)
10
+ *
11
+ * This is the pipeline that makes ClawMoat agent-native, not chat-native.
12
+ *
13
+ * @module boundary-scanner
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ const STAGES = ['pre-input', 'pre-model', 'pre-tool-call', 'post-tool-result', 'pre-output'];
19
+
20
+ /**
21
+ * Create a boundary scanner pipeline
22
+ * @param {Object} [config] - Configuration
23
+ * @param {string} [config.mode='enforce'] - 'enforce' (block), 'monitor' (log only), 'off'
24
+ * @param {Function} [config.onViolation] - Callback on violations: (stage, finding, context) => void
25
+ * @param {Function} [config.onDecision] - Callback on every decision: (stage, result, context) => void
26
+ * @param {Object} [config.stageConfig] - Per-stage overrides
27
+ * @returns {Object} Pipeline instance
28
+ */
29
+ function createPipeline(config = {}) {
30
+ const {
31
+ mode = 'enforce',
32
+ onViolation = null,
33
+ onDecision = null,
34
+ stageConfig = {},
35
+ } = config;
36
+
37
+ // Registered scanners per stage
38
+ const scanners = {};
39
+ for (const stage of STAGES) {
40
+ scanners[stage] = [];
41
+ }
42
+
43
+ // Execution trace
44
+ const trace = [];
45
+ const stats = { scanned: 0, blocked: 0, warned: 0, allowed: 0 };
46
+
47
+ /**
48
+ * Register a scanner for a stage
49
+ * @param {string} stage - Pipeline stage
50
+ * @param {string} name - Scanner name
51
+ * @param {Function} fn - Scanner function: (text, context) => { safe, findings }
52
+ * @param {Object} [opts] - Options
53
+ * @param {number} [opts.priority=50] - Lower runs first
54
+ * @param {boolean} [opts.required=false] - Pipeline fails if scanner throws
55
+ */
56
+ function register(stage, name, fn, opts = {}) {
57
+ if (!STAGES.includes(stage)) {
58
+ throw new Error(`Invalid stage: ${stage}. Must be one of: ${STAGES.join(', ')}`);
59
+ }
60
+ const { priority = 50, required = false } = opts;
61
+ scanners[stage].push({ name, fn, priority, required });
62
+ scanners[stage].sort((a, b) => a.priority - b.priority);
63
+ }
64
+
65
+ /**
66
+ * Run all scanners for a pipeline stage
67
+ * @param {string} stage - Pipeline stage
68
+ * @param {string|Object} input - Text or structured input to scan
69
+ * @param {Object} [context] - Additional context (trust level, tool name, etc.)
70
+ * @returns {Object} { allowed, findings, blocked, actions }
71
+ */
72
+ function scan(stage, input, context = {}) {
73
+ if (!STAGES.includes(stage)) {
74
+ throw new Error(`Invalid stage: ${stage}. Must be one of: ${STAGES.join(', ')}`);
75
+ }
76
+
77
+ const stageMode = stageConfig[stage]?.mode || mode;
78
+ if (stageMode === 'off') {
79
+ return { allowed: true, findings: [], blocked: false, actions: [] };
80
+ }
81
+
82
+ const text = typeof input === 'string' ? input : JSON.stringify(input);
83
+ const allFindings = [];
84
+ const actions = [];
85
+ let blocked = false;
86
+
87
+ stats.scanned++;
88
+
89
+ for (const scanner of scanners[stage]) {
90
+ try {
91
+ const result = scanner.fn(text, { ...context, stage });
92
+ if (result && !result.safe && result.findings) {
93
+ for (const finding of result.findings) {
94
+ const enriched = {
95
+ ...finding,
96
+ scanner: scanner.name,
97
+ stage,
98
+ timestamp: new Date().toISOString(),
99
+ };
100
+ allFindings.push(enriched);
101
+
102
+ const action = resolveAction(enriched, stageMode);
103
+ actions.push({ finding: enriched, action });
104
+
105
+ if (action === 'block') blocked = true;
106
+ if (onViolation) onViolation(stage, enriched, context);
107
+ }
108
+ }
109
+ } catch (err) {
110
+ if (scanner.required) {
111
+ blocked = true;
112
+ allFindings.push({
113
+ type: 'scanner_error',
114
+ subtype: 'required_scanner_failed',
115
+ severity: 'critical',
116
+ scanner: scanner.name,
117
+ stage,
118
+ evidence: err.message,
119
+ timestamp: new Date().toISOString(),
120
+ });
121
+ }
122
+ }
123
+ }
124
+
125
+ const allowed = stageMode === 'monitor' ? true : !blocked;
126
+ if (blocked) stats.blocked++;
127
+ else if (allFindings.length > 0) stats.warned++;
128
+ else stats.allowed++;
129
+
130
+ const decision = {
131
+ stage,
132
+ allowed,
133
+ blocked,
134
+ findings: allFindings,
135
+ actions,
136
+ mode: stageMode,
137
+ scannersRun: scanners[stage].length,
138
+ timestamp: new Date().toISOString(),
139
+ };
140
+
141
+ trace.push(decision);
142
+ if (onDecision) onDecision(stage, decision, context);
143
+
144
+ return decision;
145
+ }
146
+
147
+ /**
148
+ * Run a complete agent turn through the pipeline
149
+ * @param {Object} turn - Agent turn data
150
+ * @param {string} [turn.input] - User/external input
151
+ * @param {string} [turn.modelPrompt] - Full prompt sent to model
152
+ * @param {Array} [turn.toolCalls] - Array of { tool, args } objects
153
+ * @param {Array} [turn.toolResults] - Array of tool result strings
154
+ * @param {string} [turn.output] - Final agent output
155
+ * @param {Object} [context] - Context passed to all stages
156
+ * @returns {Object} { allowed, stages, findings, trace }
157
+ */
158
+ function scanTurn(turn, context = {}) {
159
+ const stages = {};
160
+ const allFindings = [];
161
+ let turnAllowed = true;
162
+
163
+ if (turn.input !== undefined) {
164
+ stages['pre-input'] = scan('pre-input', turn.input, context);
165
+ if (!stages['pre-input'].allowed) turnAllowed = false;
166
+ allFindings.push(...stages['pre-input'].findings);
167
+ }
168
+
169
+ if (turn.modelPrompt !== undefined && turnAllowed) {
170
+ stages['pre-model'] = scan('pre-model', turn.modelPrompt, context);
171
+ if (!stages['pre-model'].allowed) turnAllowed = false;
172
+ allFindings.push(...stages['pre-model'].findings);
173
+ }
174
+
175
+ if (turn.toolCalls && turnAllowed) {
176
+ for (let i = 0; i < turn.toolCalls.length; i++) {
177
+ const tc = turn.toolCalls[i];
178
+ const key = `pre-tool-call[${i}]`;
179
+ const tcContext = { ...context, tool: tc.tool, toolIndex: i };
180
+ stages[key] = scan('pre-tool-call', tc, tcContext);
181
+ if (!stages[key].allowed) turnAllowed = false;
182
+ allFindings.push(...stages[key].findings);
183
+ }
184
+ }
185
+
186
+ if (turn.toolResults && turnAllowed) {
187
+ for (let i = 0; i < turn.toolResults.length; i++) {
188
+ const key = `post-tool-result[${i}]`;
189
+ stages[key] = scan('post-tool-result', turn.toolResults[i], { ...context, toolIndex: i });
190
+ if (!stages[key].allowed) turnAllowed = false;
191
+ allFindings.push(...stages[key].findings);
192
+ }
193
+ }
194
+
195
+ if (turn.output !== undefined && turnAllowed) {
196
+ stages['pre-output'] = scan('pre-output', turn.output, context);
197
+ if (!stages['pre-output'].allowed) turnAllowed = false;
198
+ allFindings.push(...stages['pre-output'].findings);
199
+ }
200
+
201
+ return {
202
+ allowed: turnAllowed,
203
+ stages,
204
+ findings: allFindings,
205
+ turnBlocked: !turnAllowed,
206
+ };
207
+ }
208
+
209
+ function resolveAction(finding, stageMode) {
210
+ if (stageMode === 'monitor') return 'log';
211
+ const recommended = finding.recommended_action || 'block';
212
+ const severityMap = {
213
+ critical: 'block',
214
+ high: 'block',
215
+ medium: recommended === 'block' ? 'block' : 'warn',
216
+ low: 'warn',
217
+ };
218
+ return severityMap[finding.severity] || 'warn';
219
+ }
220
+
221
+ function getTrace() { return [...trace]; }
222
+ function getStats() { return { ...stats }; }
223
+ function resetTrace() { trace.length = 0; }
224
+ function resetStats() { stats.scanned = 0; stats.blocked = 0; stats.warned = 0; stats.allowed = 0; }
225
+
226
+ return {
227
+ register,
228
+ scan,
229
+ scanTurn,
230
+ getTrace,
231
+ getStats,
232
+ resetTrace,
233
+ resetStats,
234
+ STAGES,
235
+ };
236
+ }
237
+
238
+ /**
239
+ * Create a pre-configured pipeline with all ClawMoat scanners registered
240
+ * @param {Object} [config] - Pipeline config
241
+ * @returns {Object} Ready-to-use pipeline
242
+ */
243
+ function createDefaultPipeline(config = {}) {
244
+ const pipeline = createPipeline(config);
245
+
246
+ // Lazy-load to avoid circular deps
247
+ const loadScanner = (name) => {
248
+ try { return require(`./${name}`); } catch (_) { return null; }
249
+ };
250
+
251
+ // Pre-input: prompt injection, jailbreak, obfuscation
252
+ const index = loadScanner('index');
253
+ const obfuscation = loadScanner('obfuscation-scanner');
254
+ const codeScanner = loadScanner('code-scanner');
255
+
256
+ if (index) {
257
+ const moat = new index();
258
+ pipeline.register('pre-input', 'prompt-injection', (text) => {
259
+ const r = moat.scanInbound(text);
260
+ return r;
261
+ }, { priority: 10 });
262
+
263
+ pipeline.register('pre-output', 'secret-pii-leak', (text) => {
264
+ const r = moat.scanOutbound(text);
265
+ return r;
266
+ }, { priority: 10 });
267
+ }
268
+
269
+ if (obfuscation) {
270
+ pipeline.register('pre-input', 'obfuscation', (text) => {
271
+ return obfuscation.scanObfuscation(text);
272
+ }, { priority: 5 }); // Run before injection detection (strip first)
273
+ }
274
+
275
+ if (codeScanner) {
276
+ pipeline.register('pre-tool-call', 'dangerous-code', (text, ctx) => {
277
+ return codeScanner.scanCode(text, { tool: ctx.tool });
278
+ }, { priority: 20 });
279
+ }
280
+
281
+ // Post-tool-result: check for injected instructions in tool output
282
+ if (index) {
283
+ const moat = new index();
284
+ pipeline.register('post-tool-result', 'tool-result-injection', (text) => {
285
+ return moat.scanInbound(text);
286
+ }, { priority: 10 });
287
+ }
288
+
289
+ return pipeline;
290
+ }
291
+
292
+ module.exports = {
293
+ createPipeline,
294
+ createDefaultPipeline,
295
+ STAGES,
296
+ };
@@ -0,0 +1,279 @@
1
+ /**
2
+ * ClawMoat CI Scanner
3
+ *
4
+ * Scans a repo for agent security issues before deployment:
5
+ * - Leaked secrets in tracked files
6
+ * - Dangerous MCP server configs
7
+ * - Unsafe CI/CD workflow patterns
8
+ * - Known compromised dependency versions
9
+ * - Agent config files with excessive permissions
10
+ *
11
+ * @module ci-scanner
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+
19
+ const KNOWN_COMPROMISED = [
20
+ { pkg: 'telnyx', versions: ['4.87.1', '4.87.2'], cve: 'TeamPCP supply chain (Mar 2026)', severity: 'critical' },
21
+ { pkg: 'event-stream', versions: ['3.3.6'], cve: 'CVE-2018-21270', severity: 'critical' },
22
+ { pkg: 'ua-parser-js', versions: ['0.7.29', '0.7.30', '1.0.0', '1.0.1'], cve: 'CVE-2021-41265', severity: 'critical' },
23
+ { pkg: 'coa', versions: ['2.0.3', '2.0.4'], cve: 'CVE-2021-43789', severity: 'critical' },
24
+ { pkg: 'rc', versions: ['1.2.9'], cve: 'CVE-2021-43790', severity: 'critical' },
25
+ { pkg: 'node-ipc', versions: ['10.1.1', '10.1.2', '11.1.0'], cve: 'CVE-2022-23812', severity: 'critical' },
26
+ ];
27
+
28
+ // Patterns that indicate secrets in source files
29
+ const SECRET_PATTERNS = [
30
+ { re: /(?:^|[\s'"=])(sk-[a-zA-Z0-9]{20,})/, name: 'OpenAI API key', severity: 'critical' },
31
+ { re: /(?:^|[\s'"=])(sk-proj-[a-zA-Z0-9]{40,})/, name: 'OpenAI project key', severity: 'critical' },
32
+ { re: /(?:^|[\s'"=])(ghp_[a-zA-Z0-9]{36})/, name: 'GitHub Personal Access Token', severity: 'critical' },
33
+ { re: /(?:^|[\s'"=])(github_pat_[a-zA-Z0-9_]{82})/, name: 'GitHub fine-grained token', severity: 'critical' },
34
+ { re: /(?:^|[\s'"=])(AKIA[0-9A-Z]{16})/, name: 'AWS Access Key ID', severity: 'critical' },
35
+ { re: /AWS_SECRET_ACCESS_KEY\s*[=:]\s*([A-Za-z0-9/+=]{40})/, name: 'AWS Secret Access Key', severity: 'critical' },
36
+ { re: /(?:^|[\s'"=])(xoxb-[0-9]+-[0-9]+-[a-zA-Z0-9]+)/, name: 'Slack bot token', severity: 'critical' },
37
+ { re: /(?:^|[\s'"=])(xoxp-[0-9]+-[0-9]+-[a-zA-Z0-9]+)/, name: 'Slack user token', severity: 'critical' },
38
+ { re: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/, name: 'Private key', severity: 'critical' },
39
+ { re: /(?:password|passwd|pwd)\s*[=:]\s*['"]([^'"]{8,})['"]/, name: 'Hardcoded password', severity: 'high' },
40
+ ];
41
+
42
+ // CI/CD workflow risk patterns
43
+ const CI_RISK_PATTERNS = [
44
+ { re: /\$\{\{\s*github\.event\.(?:issue|pull_request|comment)\.(?:title|body)\s*\}\}/, name: 'Untrusted PR/issue data in workflow', severity: 'critical' },
45
+ { re: /\$\{\{\s*github\.event\.head_commit\.message\s*\}\}/, name: 'Untrusted commit message in workflow', severity: 'high' },
46
+ { re: /uses:\s*[a-z0-9-]+\/[a-z0-9-]+@(?:main|master|latest|HEAD)/, name: 'Unpinned GitHub Action (use @SHA)', severity: 'medium' },
47
+ { re: /curl\s+.*\|\s*(?:bash|sh)/, name: 'Curl pipe to shell in workflow', severity: 'critical' },
48
+ { re: /run:\s*echo\s+\$\{\{/, name: 'Unsanitized expression in echo', severity: 'high' },
49
+ ];
50
+
51
+ // Extensions to scan for secrets
52
+ const SCANNABLE_EXTENSIONS = new Set([
53
+ '.js', '.ts', '.py', '.rb', '.go', '.java', '.cs', '.php',
54
+ '.env', '.yaml', '.yml', '.json', '.toml', '.ini', '.cfg',
55
+ '.sh', '.bash', '.zsh', '.fish',
56
+ ]);
57
+
58
+ // Files/dirs to always skip
59
+ const SKIP_DIRS = new Set([
60
+ 'node_modules', '.git', '.svn', '__pycache__', '.pytest_cache',
61
+ 'dist', 'build', 'coverage', '.nyc_output', 'vendor',
62
+ '.venv', 'venv', 'env',
63
+ ]);
64
+
65
+ /**
66
+ * Scan a repository for security issues
67
+ * @param {Object} [opts] - Options
68
+ * @param {string} [opts.rootDir='.'] - Root directory to scan
69
+ * @param {boolean} [opts.checkDeps=true] - Check package.json for known-bad deps
70
+ * @param {boolean} [opts.checkSecrets=true] - Scan files for leaked secrets
71
+ * @param {boolean} [opts.checkCI=true] - Scan CI/CD workflows
72
+ * @param {boolean} [opts.checkMCP=true] - Scan MCP configs
73
+ * @param {string} [opts.failOn='high'] - Severity to fail CI on
74
+ * @returns {Object} { findings, summary, passed }
75
+ */
76
+ function scanRepo(opts = {}) {
77
+ const {
78
+ rootDir = process.cwd(),
79
+ checkDeps = true,
80
+ checkSecrets = true,
81
+ checkCI = true,
82
+ checkMCP = true,
83
+ failOn = 'high',
84
+ } = opts;
85
+
86
+ const findings = [];
87
+
88
+ // 1. Check package.json for compromised deps
89
+ if (checkDeps) {
90
+ const pkgPath = path.join(rootDir, 'package.json');
91
+ if (fs.existsSync(pkgPath)) {
92
+ try {
93
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
94
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
95
+ for (const [name, version] of Object.entries(allDeps)) {
96
+ const cleanVer = version.replace(/^[^0-9]*/, '');
97
+ const compromised = KNOWN_COMPROMISED.find(c =>
98
+ c.pkg === name && c.versions.includes(cleanVer)
99
+ );
100
+ if (compromised) {
101
+ findings.push({
102
+ type: 'compromised_dependency',
103
+ severity: compromised.severity,
104
+ file: 'package.json',
105
+ evidence: `${name}@${cleanVer} — ${compromised.cve}`,
106
+ fix: `Update ${name} to latest safe version`,
107
+ });
108
+ }
109
+ }
110
+ } catch (_) {}
111
+ }
112
+
113
+ // Check requirements.txt
114
+ const reqPath = path.join(rootDir, 'requirements.txt');
115
+ if (fs.existsSync(reqPath)) {
116
+ const content = fs.readFileSync(reqPath, 'utf8');
117
+ for (const line of content.split('\n')) {
118
+ const match = line.match(/^([a-zA-Z0-9_-]+)==([0-9.]+)/);
119
+ if (match) {
120
+ const [, name, version] = match;
121
+ const compromised = KNOWN_COMPROMISED.find(c =>
122
+ c.pkg === name.toLowerCase() && c.versions.includes(version)
123
+ );
124
+ if (compromised) {
125
+ findings.push({
126
+ type: 'compromised_dependency',
127
+ severity: compromised.severity,
128
+ file: 'requirements.txt',
129
+ evidence: `${name}==${version} — ${compromised.cve}`,
130
+ fix: `Update ${name} to latest safe version`,
131
+ });
132
+ }
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ // 2. Scan source files for secrets
139
+ if (checkSecrets) {
140
+ const secretFindings = scanDirForSecrets(rootDir);
141
+ findings.push(...secretFindings);
142
+ }
143
+
144
+ // 3. Scan CI/CD workflows
145
+ if (checkCI) {
146
+ const workflowDir = path.join(rootDir, '.github', 'workflows');
147
+ if (fs.existsSync(workflowDir)) {
148
+ const files = fs.readdirSync(workflowDir).filter(f => f.endsWith('.yml') || f.endsWith('.yaml'));
149
+ for (const file of files) {
150
+ const content = fs.readFileSync(path.join(workflowDir, file), 'utf8');
151
+ for (const pattern of CI_RISK_PATTERNS) {
152
+ if (pattern.re.test(content)) {
153
+ findings.push({
154
+ type: 'ci_risk',
155
+ severity: pattern.severity,
156
+ file: `.github/workflows/${file}`,
157
+ evidence: pattern.name,
158
+ fix: getCI_Fix(pattern.name),
159
+ });
160
+ }
161
+ }
162
+ }
163
+ }
164
+ }
165
+
166
+ // 4. Scan MCP configs
167
+ if (checkMCP) {
168
+ try {
169
+ const { scanMCP } = require('./mcp-scanner');
170
+ const mcpReport = scanMCP({ quiet: true });
171
+ for (const f of mcpReport.findings) {
172
+ findings.push({
173
+ type: 'mcp_config',
174
+ severity: f.severity,
175
+ file: f.configName || 'mcp-config',
176
+ evidence: `${f.label} (${f.server})`,
177
+ fix: f.fix,
178
+ });
179
+ }
180
+ } catch (_) {}
181
+ }
182
+
183
+ // Compute summary
184
+ const summary = {
185
+ total: findings.length,
186
+ critical: findings.filter(f => f.severity === 'critical').length,
187
+ high: findings.filter(f => f.severity === 'high').length,
188
+ medium: findings.filter(f => f.severity === 'medium').length,
189
+ low: findings.filter(f => f.severity === 'low').length,
190
+ };
191
+
192
+ const severityRank = { critical: 4, high: 3, medium: 2, low: 1, none: 0 };
193
+ const failRank = severityRank[failOn] || 3;
194
+ const passed = !findings.some(f => (severityRank[f.severity] || 0) >= failRank);
195
+
196
+ return { findings, summary, passed, rootDir };
197
+ }
198
+
199
+ function scanDirForSecrets(dir, depth = 0) {
200
+ if (depth > 8) return [];
201
+ const findings = [];
202
+
203
+ let entries;
204
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
205
+ catch (_) { return []; }
206
+
207
+ for (const entry of entries) {
208
+ if (entry.name.startsWith('.') && entry.name !== '.env') continue;
209
+ if (SKIP_DIRS.has(entry.name)) continue;
210
+
211
+ const fullPath = path.join(dir, entry.name);
212
+
213
+ if (entry.isDirectory()) {
214
+ findings.push(...scanDirForSecrets(fullPath, depth + 1));
215
+ } else if (entry.isFile()) {
216
+ const ext = path.extname(entry.name).toLowerCase();
217
+ const isEnvFile = entry.name === '.env' || entry.name.startsWith('.env.');
218
+
219
+ if (isEnvFile) {
220
+ // Flag .env files committed to repo
221
+ findings.push({
222
+ type: 'committed_env_file',
223
+ severity: 'high',
224
+ file: path.relative(process.cwd(), fullPath),
225
+ evidence: '.env file committed to repository',
226
+ fix: 'Add .env to .gitignore and rotate any exposed secrets',
227
+ });
228
+ }
229
+
230
+ if (!SCANNABLE_EXTENSIONS.has(ext) && !isEnvFile) continue;
231
+
232
+ let content;
233
+ try { content = fs.readFileSync(fullPath, 'utf8'); }
234
+ catch (_) { continue; }
235
+
236
+ // Skip if file is too large (> 500KB)
237
+ if (content.length > 512000) continue;
238
+
239
+ const relPath = path.relative(process.cwd(), fullPath);
240
+
241
+ for (const pattern of SECRET_PATTERNS) {
242
+ const match = pattern.re.exec(content);
243
+ if (match) {
244
+ const secret = match[1] || match[0];
245
+ const redacted = secret.length > 8
246
+ ? secret.substring(0, 4) + '*'.repeat(Math.min(secret.length - 8, 20)) + secret.substring(secret.length - 4)
247
+ : '****';
248
+ findings.push({
249
+ type: 'leaked_secret',
250
+ severity: pattern.severity,
251
+ file: relPath,
252
+ evidence: `${pattern.name}: ${redacted}`,
253
+ fix: 'Remove secret from file, add to .gitignore, rotate the credential',
254
+ });
255
+ }
256
+ }
257
+ }
258
+ }
259
+
260
+ return findings;
261
+ }
262
+
263
+ function getCI_Fix(patternName) {
264
+ const fixes = {
265
+ 'Untrusted PR/issue data in workflow': 'Use an intermediate env var with sanitization, or use github.event_name check',
266
+ 'Unpinned GitHub Action (use @SHA)': 'Pin to a full SHA: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683',
267
+ 'Curl pipe to shell in workflow': 'Download script, verify checksum, then execute separately',
268
+ 'Unsanitized expression in echo': 'Use $GITHUB_ENV or intermediate env var to prevent log injection',
269
+ };
270
+ return fixes[patternName] || 'Review and remediate the risk';
271
+ }
272
+
273
+ module.exports = {
274
+ scanRepo,
275
+ scanDirForSecrets,
276
+ KNOWN_COMPROMISED,
277
+ SECRET_PATTERNS,
278
+ CI_RISK_PATTERNS,
279
+ };