praxis-sec 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 (183) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +170 -0
  3. package/ai-defense/cost-protection.md +292 -0
  4. package/ai-defense/llm-security-checklist.md +324 -0
  5. package/ai-defense/prompt-injection-patterns.js +283 -0
  6. package/ai-defense/system-prompt-armor.md +327 -0
  7. package/checklists/launch-day.md +168 -0
  8. package/cli/agents/abom-generator.js +225 -0
  9. package/cli/agents/agent-attestation-agent.js +318 -0
  10. package/cli/agents/agent-config-scanner.js +787 -0
  11. package/cli/agents/agent-telemetry-agent.js +415 -0
  12. package/cli/agents/agentic-security-agent.js +296 -0
  13. package/cli/agents/agentic-supply-chain-agent.js +463 -0
  14. package/cli/agents/ai-infra-inventory-agent.js +449 -0
  15. package/cli/agents/api-fuzzer.js +345 -0
  16. package/cli/agents/auth-bypass-agent.js +348 -0
  17. package/cli/agents/base-agent.js +280 -0
  18. package/cli/agents/cicd-scanner.js +300 -0
  19. package/cli/agents/config-auditor.js +757 -0
  20. package/cli/agents/deep-analyzer.js +776 -0
  21. package/cli/agents/endpoint-agent-abuse-agent.js +404 -0
  22. package/cli/agents/exception-handler-agent.js +187 -0
  23. package/cli/agents/git-history-scanner.js +169 -0
  24. package/cli/agents/governance-audits.js +138 -0
  25. package/cli/agents/hermes-security-agent.js +536 -0
  26. package/cli/agents/html-reporter.js +1125 -0
  27. package/cli/agents/index.js +147 -0
  28. package/cli/agents/injection-tester.js +502 -0
  29. package/cli/agents/legal-risk-agent.js +328 -0
  30. package/cli/agents/llm-redteam.js +199 -0
  31. package/cli/agents/managed-agent-scanner.js +333 -0
  32. package/cli/agents/mcp-security-agent.js +588 -0
  33. package/cli/agents/memory-poisoning-agent.js +305 -0
  34. package/cli/agents/mobile-scanner.js +231 -0
  35. package/cli/agents/model-file-scanner.js +259 -0
  36. package/cli/agents/orchestrator.js +355 -0
  37. package/cli/agents/pii-compliance-agent.js +301 -0
  38. package/cli/agents/policy-engine.js +229 -0
  39. package/cli/agents/prompt-injection-prober.js +224 -0
  40. package/cli/agents/rag-security-agent.js +204 -0
  41. package/cli/agents/recon-agent.js +207 -0
  42. package/cli/agents/sbom-generator.js +265 -0
  43. package/cli/agents/scoring-engine.js +273 -0
  44. package/cli/agents/ssrf-prober.js +130 -0
  45. package/cli/agents/stateful-watcher.js +238 -0
  46. package/cli/agents/supabase-rls-agent.js +154 -0
  47. package/cli/agents/supply-chain-agent.js +857 -0
  48. package/cli/agents/swarm-orchestrator.js +200 -0
  49. package/cli/agents/verifier-agent.js +303 -0
  50. package/cli/agents/vibe-coding-agent.js +250 -0
  51. package/cli/bin/praxis.js +866 -0
  52. package/cli/commands/abom.js +73 -0
  53. package/cli/commands/agent-fix.js +1245 -0
  54. package/cli/commands/audit.js +1180 -0
  55. package/cli/commands/autofix.js +383 -0
  56. package/cli/commands/baseline.js +193 -0
  57. package/cli/commands/benchmark.js +327 -0
  58. package/cli/commands/checklist.js +223 -0
  59. package/cli/commands/ci.js +403 -0
  60. package/cli/commands/deps.js +516 -0
  61. package/cli/commands/diff.js +200 -0
  62. package/cli/commands/doctor.js +195 -0
  63. package/cli/commands/env-audit.js +349 -0
  64. package/cli/commands/fix.js +218 -0
  65. package/cli/commands/guard.js +396 -0
  66. package/cli/commands/hooks.js +278 -0
  67. package/cli/commands/init.js +514 -0
  68. package/cli/commands/legal.js +158 -0
  69. package/cli/commands/live-advisories.js +241 -0
  70. package/cli/commands/mcp.js +660 -0
  71. package/cli/commands/openclaw.js +386 -0
  72. package/cli/commands/red-team.js +350 -0
  73. package/cli/commands/redteam.js +78 -0
  74. package/cli/commands/remediate.js +797 -0
  75. package/cli/commands/rotate.js +768 -0
  76. package/cli/commands/rules.js +196 -0
  77. package/cli/commands/scan-mcp.js +534 -0
  78. package/cli/commands/scan-skill.js +588 -0
  79. package/cli/commands/scan-standard.js +251 -0
  80. package/cli/commands/scan.js +524 -0
  81. package/cli/commands/score.js +449 -0
  82. package/cli/commands/shell.js +514 -0
  83. package/cli/commands/team-report.js +398 -0
  84. package/cli/commands/undo.js +161 -0
  85. package/cli/commands/update-intel.js +126 -0
  86. package/cli/commands/vibe-check.js +276 -0
  87. package/cli/commands/watch.js +757 -0
  88. package/cli/commands/web.js +63 -0
  89. package/cli/core/ast/guardrail-detector.js +141 -0
  90. package/cli/core/ast/index.js +22 -0
  91. package/cli/core/ast/parser.js +676 -0
  92. package/cli/core/ast/scope-tree.js +287 -0
  93. package/cli/core/ast/taint-tracker.js +158 -0
  94. package/cli/core/branding.js +37 -0
  95. package/cli/core/env.js +38 -0
  96. package/cli/core/errors.js +61 -0
  97. package/cli/core/fs.js +62 -0
  98. package/cli/core/output/compliance.js +90 -0
  99. package/cli/core/output/html-theme.js +158 -0
  100. package/cli/core/output/index.js +57 -0
  101. package/cli/core/output/json.js +48 -0
  102. package/cli/core/output/sarif.js +240 -0
  103. package/cli/core/version.js +67 -0
  104. package/cli/core/web/jobs.js +183 -0
  105. package/cli/core/web/projects.js +146 -0
  106. package/cli/core/web/server.js +439 -0
  107. package/cli/data/atlas-knowledge.json +5640 -0
  108. package/cli/data/eaa-catalog.json +39 -0
  109. package/cli/data/known-mcps.json +26 -0
  110. package/cli/data/probes/prompt-injection-corpus.json +271 -0
  111. package/cli/data/threat-intel.json +85 -0
  112. package/cli/data/threatpacks/latest.json +41 -0
  113. package/cli/hooks/patterns.js +313 -0
  114. package/cli/hooks/post-tool-use.js +140 -0
  115. package/cli/hooks/pre-tool-use.js +186 -0
  116. package/cli/index.js +90 -0
  117. package/cli/providers/llm-provider.js +766 -0
  118. package/cli/utils/autofix-rules.js +74 -0
  119. package/cli/utils/cache-manager.js +310 -0
  120. package/cli/utils/compliance-map.js +191 -0
  121. package/cli/utils/entropy.js +132 -0
  122. package/cli/utils/fix-ledger.js +127 -0
  123. package/cli/utils/hermes-tool-registry.js +252 -0
  124. package/cli/utils/intel/cache.js +61 -0
  125. package/cli/utils/intel/http.js +88 -0
  126. package/cli/utils/intel/index.js +235 -0
  127. package/cli/utils/intel/merge.js +229 -0
  128. package/cli/utils/intel/sources/epss.js +54 -0
  129. package/cli/utils/intel/sources/ghsa.js +81 -0
  130. package/cli/utils/intel/sources/gitguardian.js +40 -0
  131. package/cli/utils/intel/sources/gitleaks.js +101 -0
  132. package/cli/utils/intel/sources/kev.js +38 -0
  133. package/cli/utils/intel/sources/nvd.js +84 -0
  134. package/cli/utils/intel/sources/osv.js +132 -0
  135. package/cli/utils/intel/sources/phylum.js +44 -0
  136. package/cli/utils/intel/sources/snyk.js +46 -0
  137. package/cli/utils/intel/sources/socket.js +69 -0
  138. package/cli/utils/intel/sources/sonatype.js +84 -0
  139. package/cli/utils/intel/sources/threatpack.js +69 -0
  140. package/cli/utils/mcp-trust.js +60 -0
  141. package/cli/utils/output.js +251 -0
  142. package/cli/utils/patterns.js +1130 -0
  143. package/cli/utils/pdf-generator.js +94 -0
  144. package/cli/utils/plugin-loader.js +364 -0
  145. package/cli/utils/rule-import.js +228 -0
  146. package/cli/utils/rule-registry.js +426 -0
  147. package/cli/utils/scan-fingerprint.js +109 -0
  148. package/cli/utils/scan-playbook.js +312 -0
  149. package/cli/utils/score-history.js +119 -0
  150. package/cli/utils/secrets-verifier.js +247 -0
  151. package/cli/utils/security-memory.js +296 -0
  152. package/cli/utils/standards/atlas-knowledge.js +87 -0
  153. package/cli/utils/standards/index.js +127 -0
  154. package/cli/utils/standards/sources/avid.js +45 -0
  155. package/cli/utils/standards/sources/eu-ai-act.js +89 -0
  156. package/cli/utils/standards/sources/google-saif.js +39 -0
  157. package/cli/utils/standards/sources/iso-42001.js +94 -0
  158. package/cli/utils/standards/sources/mitre-atlas.js +54 -0
  159. package/cli/utils/standards/sources/nist-ai-600-1.js +45 -0
  160. package/cli/utils/standards/sources/owasp-llm.js +45 -0
  161. package/cli/utils/standards/sources/owasp-ml.js +45 -0
  162. package/cli/utils/threat-intel.js +265 -0
  163. package/configs/firebase/firestore-rules.txt +215 -0
  164. package/configs/firebase/security-checklist.md +236 -0
  165. package/configs/firebase/storage-rules.txt +206 -0
  166. package/configs/gitignore-template +258 -0
  167. package/configs/nextjs-security-headers.js +220 -0
  168. package/configs/praxisignore-template +50 -0
  169. package/configs/supabase/secure-client.ts +225 -0
  170. package/configs/supabase/security-checklist.md +278 -0
  171. package/docs/THIRD_PARTY_NOTICES.md +26 -0
  172. package/docs/THREAT_INTEL.md +292 -0
  173. package/docs/USAGE.md +1205 -0
  174. package/docs/design/WEB-UI.md +82 -0
  175. package/package.json +71 -0
  176. package/scripts/check-determinism.mjs +119 -0
  177. package/snippets/README.md +122 -0
  178. package/snippets/api-security/api-security-checklist.md +412 -0
  179. package/snippets/api-security/cors-config.ts +322 -0
  180. package/snippets/api-security/input-validation.ts +430 -0
  181. package/snippets/auth/jwt-checklist.md +322 -0
  182. package/snippets/rate-limiting/nextjs-middleware.ts +211 -0
  183. package/snippets/rate-limiting/upstash-ratelimit.ts +229 -0
@@ -0,0 +1,588 @@
1
+ /**
2
+ * Scan Skill Command
3
+ * ===================
4
+ *
5
+ * Downloads and analyzes an AI agent skill before installation.
6
+ * Checks for malicious patterns, permission abuse, typosquatting,
7
+ * and known threat intelligence indicators.
8
+ *
9
+ * USAGE:
10
+ * praxis scan-skill <url> Analyze a skill from URL
11
+ * praxis scan-skill <path> Analyze a local skill file
12
+ * praxis scan-skill . --all Scan all skills in openclaw.json
13
+ */
14
+
15
+ import fs from 'fs';
16
+ import path from 'path';
17
+ import chalk from 'chalk';
18
+ import { createHash } from 'crypto';
19
+ import * as output from '../utils/output.js';
20
+ import { ThreatIntel } from '../utils/threat-intel.js';
21
+
22
+ // =============================================================================
23
+ // HERMES SKILL FRONTMATTER PATTERNS (Track D — cross-skill/tool binding)
24
+ // =============================================================================
25
+
26
+ // Built-in tool registries that skills may reference.
27
+ // Praxis tools are added lazily in checkHermesFrontmatter() to avoid
28
+ // loading hermes-tool-registry.js (and its crypto import) on every invocation.
29
+ const KNOWN_TOOL_REGISTRIES = {
30
+ // Common Hermes community tools (names only — no handler)
31
+ 'web_search': 'hermes-community',
32
+ 'web_browser': 'hermes-community',
33
+ 'file_read': 'hermes-community',
34
+ 'file_write': 'hermes-community',
35
+ 'code_execute': 'hermes-community',
36
+ 'github_api': 'hermes-community',
37
+ 'memory_store': 'hermes-community',
38
+ 'memory_retrieve': 'hermes-community',
39
+ };
40
+
41
+ // Hermes-specific patterns to check in skill markdown/frontmatter
42
+ const HERMES_SKILL_PATTERNS = [
43
+ {
44
+ name: 'Hermes: XML tool_call injection',
45
+ regex: /<tool_call>[\s\S]{0,300}<\/tool_call>/gi,
46
+ severity: 'critical',
47
+ note: 'Skill body contains a <tool_call> block — will be executed by Hermes agents that load this skill.',
48
+ },
49
+ {
50
+ name: 'Hermes: function_calls injection',
51
+ regex: /<function_calls>[\s\S]{0,300}<\/function_calls>/gi,
52
+ severity: 'critical',
53
+ note: 'Skill body contains a <function_calls> block — classic Hermes function-call injection.',
54
+ },
55
+ {
56
+ name: 'Hermes: Forced tool invocation instruction',
57
+ regex: /(?:you\s+must\s+(?:call|invoke|use)\s+(?:the\s+)?tool|always\s+(?:call|invoke|run)\s+(?:the\s+)?(?:tool|function)|tool\s+MUST\s+be\s+(?:called|invoked|used))/gi,
58
+ severity: 'high',
59
+ note: 'Skill instructs agent to call a specific tool unconditionally — bypasses agent autonomy.',
60
+ },
61
+ {
62
+ name: 'Hermes: Plan/goal hijacking',
63
+ regex: /(?:update\s+(?:your\s+)?(?:goal|plan|objective)\s+to|change\s+(?:your\s+)?(?:goal|plan|objective)|your\s+(?:new\s+)?(?:goal|plan|primary\s+objective)\s+(?:is|should\s+be))/gi,
64
+ severity: 'critical',
65
+ note: 'Skill attempts to overwrite the agent\'s goal or plan state — ASI-01 Goal Hijacking.',
66
+ },
67
+ {
68
+ name: 'Hermes: Memory layer write instruction',
69
+ regex: /(?:write\s+(?:this|the\s+following)\s+to\s+(?:memory|episodic|semantic|working)\s+memory|store\s+(?:this|the\s+following)\s+in\s+(?:memory|episodic|semantic))/gi,
70
+ severity: 'high',
71
+ note: 'Skill instructs agent to write attacker-controlled data to memory — ASI-06 Memory Poisoning.',
72
+ },
73
+ ];
74
+
75
+ // =============================================================================
76
+ // POPULAR SKILL NAMES (for typosquatting detection)
77
+ // =============================================================================
78
+
79
+ const POPULAR_SKILLS = [
80
+ 'web-search', 'web-browser', 'file-manager', 'code-runner',
81
+ 'git-helper', 'database-query', 'api-tester', 'image-gen',
82
+ 'text-to-speech', 'pdf-reader', 'email-sender', 'slack-bot',
83
+ 'github-helper', 'docker-manager', 'kubernetes-helper',
84
+ 'aws-helper', 'terraform-helper', 'memory-store',
85
+ 'calculator', 'translator', 'summarizer', 'code-review',
86
+ ];
87
+
88
+ // =============================================================================
89
+ // MALICIOUS PATTERNS
90
+ // =============================================================================
91
+
92
+ const SKILL_PATTERNS = [
93
+ { name: 'Shell execution', regex: /(?:child_process|exec|spawn|execSync|execFile|os\.system|subprocess|shell_exec|system\()/gi, severity: 'critical' },
94
+ { name: 'Outbound HTTP to non-localhost', regex: /(?:fetch|axios|http\.get|requests\.get|urllib|wget|curl)\s*\(\s*['"`]https?:\/\/(?!(?:localhost|127\.0\.0\.1|::1))/gi, severity: 'high' },
95
+ { name: 'Data exfiltration service', regex: /(?:webhook\.site|requestbin\.com|hookbin\.com|pipedream\.net|ngrok\.io|ngrok\.app|burpcollaborator|interact\.sh)/gi, severity: 'critical' },
96
+ { name: 'Environment variable access', regex: /(?:process\.env|os\.environ|os\.getenv|ENV\[|System\.getenv)/gi, severity: 'medium' },
97
+ { name: 'File system write', regex: /(?:fs\.writeFile|fs\.appendFile|writeFileSync|open\(.+['"]w['"]|fwrite|file_put_contents)/gi, severity: 'medium' },
98
+ { name: 'Base64 decode + execute', regex: /(?:atob|Buffer\.from|base64\.b64decode|base64_decode)\s*\([^)]*\)\s*(?:\.|\))\s*(?:eval|exec|Function)/gi, severity: 'critical' },
99
+ { name: 'Dynamic code evaluation', regex: /(?:eval\s*\(|new\s+Function\s*\(|exec\s*\(|compile\s*\()/gi, severity: 'high' },
100
+ { name: 'Crypto operations', regex: /(?:crypto\.createCipher|crypto\.createDecipher|CryptoJS|forge\.cipher)/gi, severity: 'medium' },
101
+ { name: 'Network listener', regex: /(?:createServer|listen\s*\(\s*\d|bind\s*\(\s*['"]0\.0\.0\.0)/gi, severity: 'high' },
102
+ { name: 'Encoded payload block', regex: /[A-Za-z0-9+\/]{60,}={0,2}/g, severity: 'medium' },
103
+
104
+ // ── ToxicSkills patterns (Snyk research — 36% of agent skills affected) ──
105
+ // Silent curl exfiltration: skill instructs agent to silently send data
106
+ { name: 'ToxicSkills: silent data exfiltration via curl', regex: /(?:silently|quietly|without\s+(?:notif|alert|inform|telling|showing)|in\s+the\s+background)\s+.{0,60}(?:curl|wget|fetch|POST|send).{0,60}(?:http|https):\/\//gi, severity: 'critical' },
107
+ // System prompt override in skill definition
108
+ { name: 'ToxicSkills: system prompt override', regex: /(?:ignore\s+(?:all\s+)?(?:previous|prior|above|your)\s+instructions|your\s+(?:new|real|actual|true)\s+(?:instructions|role|goal|purpose)\s+(?:is|are)|disregard\s+(?:all\s+)?(?:previous|above|your))/gi, severity: 'critical' },
109
+ // Skill requests credentials/secrets from agent context
110
+ { name: 'ToxicSkills: credential harvesting', regex: /(?:extract|retrieve|collect|gather|find|read|access|get)\s+.{0,40}(?:api[_\s]?key|secret|token|password|credential|\.env|npmrc|ssh[_\s]?key|private[_\s]?key)/gi, severity: 'critical' },
111
+ // Skill attempts to read ~/.ssh, ~/.aws, ~/.npmrc
112
+ { name: 'ToxicSkills: sensitive path access', regex: /(?:~\/\.(?:ssh|aws|npmrc|netrc|gnupg|config\/gcloud)|\/etc\/(?:passwd|shadow|hosts)|%APPDATA%|%USERPROFILE%)/gi, severity: 'critical' },
113
+ // Skill suppresses its own output to avoid detection
114
+ { name: 'ToxicSkills: output suppression', regex: /(?:do\s+not\s+(?:show|display|reveal|mention|tell|report|log)\s+(?:this|these|the\s+(?:output|result|response|command|action))|hide\s+(?:this|the)\s+(?:output|result|action|command|request))/gi, severity: 'high' },
115
+ // Skill requests permissions beyond its stated purpose
116
+ { name: 'ToxicSkills: permission escalation', regex: /(?:grant\s+(?:me|this\s+skill|yourself)\s+(?:admin|root|sudo|full|all)\s+(?:access|permissions?|rights?)|elevate\s+(?:privileges?|permissions?|rights?)|run\s+as\s+(?:admin|root|sudo))/gi, severity: 'high' },
117
+ ];
118
+
119
+ // =============================================================================
120
+ // MAIN COMMAND
121
+ // =============================================================================
122
+
123
+ export async function scanSkillCommand(target, options = {}) {
124
+ if (!target) {
125
+ output.error('Usage: praxis scan-skill <url|path>');
126
+ output.info(' Analyze an AI agent skill for security issues before installing it.');
127
+ process.exit(1);
128
+ }
129
+
130
+ console.log();
131
+ output.header('Praxis — Skill Security Analysis');
132
+ console.log();
133
+
134
+ // If --all flag, scan all skills from openclaw.json
135
+ if (options.all) {
136
+ return scanAllSkills(path.resolve(target));
137
+ }
138
+
139
+ // Determine if URL or local file
140
+ let content, skillName, source;
141
+
142
+ if (target.startsWith('http://') || target.startsWith('https://')) {
143
+ console.log(chalk.gray(` Fetching skill from: ${target}`));
144
+ try {
145
+ const response = await fetch(target);
146
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
147
+ content = await response.text();
148
+ skillName = new URL(target).pathname.split('/').pop() || 'remote-skill';
149
+ source = target;
150
+ } catch (err) {
151
+ output.error(`Failed to fetch skill: ${err.message}`);
152
+ process.exit(1);
153
+ }
154
+ } else {
155
+ const filePath = path.resolve(target);
156
+ if (!fs.existsSync(filePath)) {
157
+ output.error(`File not found: ${filePath}`);
158
+ process.exit(1);
159
+ }
160
+ content = fs.readFileSync(filePath, 'utf-8');
161
+ skillName = path.basename(filePath);
162
+ source = filePath;
163
+ }
164
+
165
+ console.log(chalk.gray(` Skill: ${skillName}`));
166
+ console.log(chalk.gray(` Size: ${content.length} bytes`));
167
+ console.log();
168
+
169
+ const findings = await analyzeSkill(content, skillName, source);
170
+
171
+ if (options.json) {
172
+ console.log(JSON.stringify({ skill: skillName, source, findings, summary: getSummary(findings) }, null, 2));
173
+ return;
174
+ }
175
+
176
+ printSkillFindings(findings, skillName);
177
+ }
178
+
179
+ // =============================================================================
180
+ // SKILL ANALYSIS
181
+ // =============================================================================
182
+
183
+ async function analyzeSkill(content, skillName, source) {
184
+ const findings = [];
185
+
186
+ // 1. Static pattern analysis
187
+ const lines = content.split('\n');
188
+ for (let i = 0; i < lines.length; i++) {
189
+ const line = lines[i];
190
+ for (const pattern of SKILL_PATTERNS) {
191
+ pattern.regex.lastIndex = 0;
192
+ if (pattern.regex.test(line)) {
193
+ findings.push({
194
+ check: 'static-analysis',
195
+ name: pattern.name,
196
+ severity: pattern.severity,
197
+ line: i + 1,
198
+ matched: line.trim().slice(0, 100),
199
+ });
200
+ }
201
+ }
202
+ }
203
+
204
+ // 2. Permission manifest audit (if JSON)
205
+ try {
206
+ const manifest = JSON.parse(content);
207
+ if (manifest.permissions) {
208
+ const dangerousPerm = [/\bshell\b/i, /\bexec\b/i, /\bsystem\b/i, /\badmin\b/i, /\broot\b/i,
209
+ /filesystem\s*:\s*(write|read-write)/i, /network\s*:\s*(unrestricted|all)/i,
210
+ /^filesystem$/i, /^network$/i];
211
+ for (const perm of (Array.isArray(manifest.permissions) ? manifest.permissions : [])) {
212
+ const permStr = typeof perm === 'string' ? perm : perm.name || '';
213
+ if (dangerousPerm.some(p => p.test(permStr))) {
214
+ findings.push({
215
+ check: 'permission-audit',
216
+ name: `Dangerous permission: ${permStr}`,
217
+ severity: 'high',
218
+ line: 0,
219
+ matched: `permissions: [${permStr}]`,
220
+ });
221
+ }
222
+ }
223
+ }
224
+
225
+ // Check for suspicious fields
226
+ if (manifest.postInstall || manifest.postinstall) {
227
+ findings.push({
228
+ check: 'permission-audit',
229
+ name: 'Post-install script defined',
230
+ severity: 'high',
231
+ line: 0,
232
+ matched: 'postInstall hook detected',
233
+ });
234
+ }
235
+ } catch { /* Not JSON, skip manifest audit */ }
236
+
237
+ // 3. Typosquatting detection
238
+ const typosquatResult = checkTyposquatting(skillName);
239
+ if (typosquatResult) {
240
+ findings.push({
241
+ check: 'typosquatting',
242
+ name: `Possible typosquat of "${typosquatResult.target}"`,
243
+ severity: 'high',
244
+ line: 0,
245
+ matched: `Levenshtein distance: ${typosquatResult.distance} from "${typosquatResult.target}"`,
246
+ });
247
+ }
248
+
249
+ // 4. Threat intel hash check
250
+ const hash = createHash('sha256').update(content).digest('hex');
251
+ const intelMatch = ThreatIntel.lookupHash(hash);
252
+ if (intelMatch) {
253
+ findings.push({
254
+ check: 'threat-intel',
255
+ name: `Known malicious skill: ${intelMatch.name}`,
256
+ severity: 'critical',
257
+ line: 0,
258
+ matched: `SHA-256: ${hash} — ${intelMatch.description}`,
259
+ });
260
+ }
261
+
262
+ // 5. Threat intel signature check
263
+ const sigMatches = ThreatIntel.matchSignatures(content);
264
+ for (const sig of sigMatches) {
265
+ findings.push({
266
+ check: 'threat-intel',
267
+ name: `Threat intel signature match: ${sig.description}`,
268
+ severity: sig.severity || 'critical',
269
+ line: 0,
270
+ matched: `Pattern: ${sig.pattern}`,
271
+ });
272
+ }
273
+
274
+ // 6. Hermes-specific: frontmatter tool binding + permission drift validation
275
+ findings.push(...(await checkHermesFrontmatter(content)));
276
+
277
+ // 7. Hermes-specific: function-call injection and goal hijacking in body
278
+ findings.push(...checkHermesBodyPatterns(content, lines));
279
+
280
+ return findings;
281
+ }
282
+
283
+ // =============================================================================
284
+ // HERMES FRONTMATTER VALIDATION (Track D)
285
+ // =============================================================================
286
+
287
+ /**
288
+ * Parse YAML frontmatter block (between --- delimiters) from markdown skill.
289
+ * Returns a plain object with string/array values; null if no frontmatter.
290
+ */
291
+ function parseFrontmatter(content) {
292
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
293
+ if (!match) return null;
294
+
295
+ const fm = {};
296
+ const yamlBlock = match[1];
297
+
298
+ for (const line of yamlBlock.split('\n')) {
299
+ const kv = line.match(/^(\w[\w-]*):\s*(.*)$/);
300
+ if (!kv) continue;
301
+ const [, key, rawVal] = kv;
302
+ const val = rawVal.trim();
303
+
304
+ if (val.startsWith('[') && val.endsWith(']')) {
305
+ // Inline array: [a, b, c]
306
+ fm[key] = val.slice(1, -1).split(',').map(s => s.trim().replace(/['"]/g, '')).filter(Boolean);
307
+ } else {
308
+ fm[key] = val.replace(/^['"]|['"]$/g, '');
309
+ }
310
+ }
311
+
312
+ // Collect multi-line list values (indented - items).
313
+ // Bounded: inner .+ capped to 200 chars and the group to 200 lines to
314
+ // prevent catastrophic backtracking on adversarial YAML (self-scan finding).
315
+ const listRe = /^(\w[\w-]*):\s*\n((?:\s+-\s+.{0,200}\n?){0,200})/gm;
316
+ let m;
317
+ while ((m = listRe.exec(yamlBlock)) !== null) {
318
+ const [, key, block] = m;
319
+ fm[key] = block.match(/-\s+(.+)/g)?.map(s => s.replace(/^-\s+/, '').replace(/['"]/g, '').trim()) ?? [];
320
+ }
321
+
322
+ return fm;
323
+ }
324
+
325
+ let _hermesToolsLoaded = false;
326
+ async function ensureHermesToolsLoaded() {
327
+ if (_hermesToolsLoaded) return;
328
+ try {
329
+ const { HERMES_TOOLS } = await import('../utils/hermes-tool-registry.js');
330
+ for (const t of HERMES_TOOLS) KNOWN_TOOL_REGISTRIES[t.name] = 'praxis';
331
+ } catch { /* non-fatal — registry unavailable */ }
332
+ _hermesToolsLoaded = true;
333
+ }
334
+
335
+ async function checkHermesFrontmatter(content) {
336
+ await ensureHermesToolsLoaded();
337
+ const findings = [];
338
+ const fm = parseFrontmatter(content);
339
+
340
+ // Not a markdown skill with frontmatter — skip
341
+ if (!fm) return findings;
342
+
343
+ // ── Check: missing permissions field ──────────────────────────────────────
344
+ if (!fm.permissions) {
345
+ findings.push({
346
+ check: 'hermes-frontmatter',
347
+ name: 'Hermes: Skill missing permissions field (ASI-02 Excessive Agency)',
348
+ severity: 'medium',
349
+ line: 0,
350
+ matched: 'No permissions: field in frontmatter — skill may be granted more access than intended',
351
+ });
352
+ } else {
353
+ // ── Check: wildcard permissions ──────────────────────────────────────────
354
+ const perms = Array.isArray(fm.permissions) ? fm.permissions : [fm.permissions];
355
+ const wildcards = perms.filter(p => /^\*$|^all$|^any$/i.test(String(p)));
356
+ if (wildcards.length > 0) {
357
+ findings.push({
358
+ check: 'hermes-frontmatter',
359
+ name: 'Hermes: Wildcard permissions (* / all) — excessive agency (ASI-02)',
360
+ severity: 'high',
361
+ line: 0,
362
+ matched: `permissions: [${wildcards.join(', ')}]`,
363
+ });
364
+ }
365
+
366
+ // ── Check: dangerous explicit permissions ────────────────────────────────
367
+ // Match whole-word or exact qualified values — don't fire on "filesystem: read-only"
368
+ const dangerousPatterns = [
369
+ /\bshell\b/i, /\bexec\b/i, /\bsystem\b/i, /\badmin\b/i, /\broot\b/i, /\bsudo\b/i,
370
+ /filesystem\s*:\s*write/i, /filesystem\s*:\s*read-write/i,
371
+ /network\s*:\s*unrestricted/i, /network\s*:\s*all/i,
372
+ /^filesystem$/i, /^network$/i, // bare "filesystem" or "network" without qualifier is ambiguous → flag
373
+ ];
374
+ for (const perm of perms) {
375
+ if (dangerousPatterns.some(p => p.test(String(perm)))) {
376
+ findings.push({
377
+ check: 'hermes-frontmatter',
378
+ name: `Hermes: Dangerous permission declared: ${perm}`,
379
+ severity: 'high',
380
+ line: 0,
381
+ matched: `permissions: [${perm}]`,
382
+ });
383
+ }
384
+ }
385
+ }
386
+
387
+ // ── Check: missing version pin ────────────────────────────────────────────
388
+ if (!fm.version) {
389
+ findings.push({
390
+ check: 'hermes-frontmatter',
391
+ name: 'Hermes: Skill missing version field — unpinned skill (ASI-10 Supply Chain)',
392
+ severity: 'medium',
393
+ line: 0,
394
+ matched: 'No version: field in frontmatter — skill version drift cannot be detected',
395
+ });
396
+ }
397
+
398
+ // ── Check: cross-skill tool binding validation ────────────────────────────
399
+ const tools = Array.isArray(fm.tools) ? fm.tools : fm.tools ? [fm.tools] : [];
400
+ for (const toolName of tools) {
401
+ if (!KNOWN_TOOL_REGISTRIES[toolName]) {
402
+ findings.push({
403
+ check: 'hermes-tool-binding',
404
+ name: `Hermes: Unresolvable tool reference: "${toolName}"`,
405
+ severity: 'high',
406
+ line: 0,
407
+ matched: `tools: [${toolName}] — not found in any known tool registry. May cause silent failures or late-binding substitution.`,
408
+ });
409
+ }
410
+ }
411
+
412
+ // ── Check: tools declared but no permissions field ────────────────────────
413
+ if (tools.length > 0 && !fm.permissions) {
414
+ findings.push({
415
+ check: 'hermes-tool-binding',
416
+ name: 'Hermes: Skill declares tools without permissions (permission drift)',
417
+ severity: 'high',
418
+ line: 0,
419
+ matched: `tools: [${tools.join(', ')}] declared but no permissions: field — skill runs with ambient agent permissions`,
420
+ });
421
+ }
422
+
423
+ return findings;
424
+ }
425
+
426
+ function checkHermesBodyPatterns(content, lines) {
427
+ const findings = [];
428
+
429
+ for (let i = 0; i < lines.length; i++) {
430
+ const line = lines[i];
431
+ for (const pattern of HERMES_SKILL_PATTERNS) {
432
+ pattern.regex.lastIndex = 0;
433
+ if (pattern.regex.test(line)) {
434
+ findings.push({
435
+ check: 'hermes-injection',
436
+ name: pattern.name,
437
+ severity: pattern.severity,
438
+ line: i + 1,
439
+ matched: line.trim().slice(0, 100),
440
+ });
441
+ }
442
+ }
443
+ }
444
+
445
+ // Multi-line checks for <tool_call> blocks that span lines
446
+ for (const pattern of HERMES_SKILL_PATTERNS) {
447
+ pattern.regex.lastIndex = 0;
448
+ const match = pattern.regex.exec(content);
449
+ if (match) {
450
+ // Avoid duplicate if already caught line-by-line
451
+ const alreadyFound = findings.some(f => f.name === pattern.name);
452
+ if (!alreadyFound) {
453
+ findings.push({
454
+ check: 'hermes-injection',
455
+ name: pattern.name,
456
+ severity: pattern.severity,
457
+ line: 0,
458
+ matched: match[0].slice(0, 100),
459
+ });
460
+ }
461
+ }
462
+ }
463
+
464
+ return findings;
465
+ }
466
+
467
+ // =============================================================================
468
+ // TYPOSQUATTING
469
+ // =============================================================================
470
+
471
+ function levenshtein(a, b) {
472
+ const matrix = [];
473
+ for (let i = 0; i <= b.length; i++) matrix[i] = [i];
474
+ for (let j = 0; j <= a.length; j++) matrix[0][j] = j;
475
+ for (let i = 1; i <= b.length; i++) {
476
+ for (let j = 1; j <= a.length; j++) {
477
+ matrix[i][j] = b[i - 1] === a[j - 1]
478
+ ? matrix[i - 1][j - 1]
479
+ : Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
480
+ }
481
+ }
482
+ return matrix[b.length][a.length];
483
+ }
484
+
485
+ function checkTyposquatting(skillName) {
486
+ const name = skillName.toLowerCase().replace(/[^a-z0-9-]/g, '');
487
+ for (const popular of POPULAR_SKILLS) {
488
+ const distance = levenshtein(name, popular);
489
+ if (distance > 0 && distance <= 2 && name !== popular) {
490
+ return { target: popular, distance };
491
+ }
492
+ }
493
+ return null;
494
+ }
495
+
496
+ // =============================================================================
497
+ // SCAN ALL SKILLS IN PROJECT
498
+ // =============================================================================
499
+
500
+ async function scanAllSkills(rootPath) {
501
+ const openclawPath = path.join(rootPath, 'openclaw.json');
502
+ if (!fs.existsSync(openclawPath)) {
503
+ output.warning('No openclaw.json found. Nothing to scan.');
504
+ return;
505
+ }
506
+
507
+ try {
508
+ const config = JSON.parse(fs.readFileSync(openclawPath, 'utf-8'));
509
+ const skills = config.skills || [];
510
+
511
+ if (skills.length === 0) {
512
+ output.info('No skills defined in openclaw.json.');
513
+ return;
514
+ }
515
+
516
+ console.log(chalk.gray(` Found ${skills.length} skill(s) in openclaw.json`));
517
+ console.log();
518
+
519
+ for (const skill of skills) {
520
+ const url = typeof skill === 'string' ? skill : skill.source || skill.url;
521
+ const name = typeof skill === 'string' ? skill : skill.name || 'unnamed';
522
+
523
+ if (url && (url.startsWith('http://') || url.startsWith('https://'))) {
524
+ console.log(chalk.cyan(` Scanning skill: ${name}`));
525
+ try {
526
+ const response = await fetch(url); // praxis-ignore SSRF_USER_URL_FETCH — operator-supplied skill URL, scheme-guarded above
527
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
528
+ const content = await response.text();
529
+ const findings = await analyzeSkill(content, name, url);
530
+ if (findings.length > 0) {
531
+ printSkillFindings(findings, name);
532
+ } else {
533
+ console.log(chalk.green(` ✔ Clean`));
534
+ }
535
+ } catch (err) {
536
+ console.log(chalk.yellow(` ⚠ Could not fetch: ${err.message}`));
537
+ }
538
+ } else {
539
+ console.log(chalk.gray(` → ${name}: local skill (static analysis only)`));
540
+ }
541
+ console.log();
542
+ }
543
+ } catch (err) {
544
+ output.error(`Failed to parse openclaw.json: ${err.message}`);
545
+ }
546
+ }
547
+
548
+ // =============================================================================
549
+ // OUTPUT
550
+ // =============================================================================
551
+
552
+ function printSkillFindings(findings, skillName) {
553
+ const summary = getSummary(findings);
554
+
555
+ if (findings.length === 0) {
556
+ console.log(chalk.green.bold(` ✔ ${skillName}: No security issues found.`));
557
+ console.log();
558
+ return;
559
+ }
560
+
561
+ console.log(chalk.red.bold(` ✘ ${skillName}: ${findings.length} issue(s) found`));
562
+ console.log();
563
+
564
+ for (const f of findings) {
565
+ const sevColor = f.severity === 'critical' ? chalk.red.bold
566
+ : f.severity === 'high' ? chalk.yellow
567
+ : chalk.blue;
568
+
569
+ console.log(` ${sevColor(`[${f.severity.toUpperCase()}]`)} ${chalk.white(f.name)}`);
570
+ if (f.line > 0) console.log(chalk.gray(` Line ${f.line}: ${f.matched}`));
571
+ else if (f.matched) console.log(chalk.gray(` ${f.matched}`));
572
+ }
573
+ console.log();
574
+
575
+ if (summary.critical > 0) {
576
+ console.log(chalk.red.bold(' ⚠ DO NOT INSTALL this skill — critical security issues detected.'));
577
+ console.log();
578
+ }
579
+ }
580
+
581
+ function getSummary(findings) {
582
+ return {
583
+ total: findings.length,
584
+ critical: findings.filter(f => f.severity === 'critical').length,
585
+ high: findings.filter(f => f.severity === 'high').length,
586
+ medium: findings.filter(f => f.severity === 'medium').length,
587
+ };
588
+ }