@hybridaione/hybridclaw 0.2.2 → 0.2.6

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 (277) hide show
  1. package/.github/workflows/ci.yml +70 -0
  2. package/.husky/pre-commit +1 -0
  3. package/CHANGELOG.md +85 -0
  4. package/CONTRIBUTING.md +33 -0
  5. package/README.md +41 -16
  6. package/SECURITY.md +17 -0
  7. package/biome.json +35 -0
  8. package/config.example.json +71 -8
  9. package/container/package-lock.json +2 -2
  10. package/container/package.json +1 -1
  11. package/container/src/approval-policy.ts +1303 -0
  12. package/container/src/browser-tools.ts +431 -136
  13. package/container/src/extensions.ts +36 -12
  14. package/container/src/hybridai-client.ts +34 -13
  15. package/container/src/index.ts +451 -109
  16. package/container/src/ipc.ts +5 -3
  17. package/container/src/token-usage.ts +20 -10
  18. package/container/src/tools.ts +599 -225
  19. package/container/src/types.ts +32 -2
  20. package/container/src/web-fetch.ts +89 -32
  21. package/dist/agent.d.ts.map +1 -1
  22. package/dist/agent.js +10 -2
  23. package/dist/agent.js.map +1 -1
  24. package/dist/audit-cli.d.ts.map +1 -1
  25. package/dist/audit-cli.js +4 -2
  26. package/dist/audit-cli.js.map +1 -1
  27. package/dist/audit-events.d.ts.map +1 -1
  28. package/dist/audit-events.js +53 -3
  29. package/dist/audit-events.js.map +1 -1
  30. package/dist/audit-trail.d.ts.map +1 -1
  31. package/dist/audit-trail.js +17 -8
  32. package/dist/audit-trail.js.map +1 -1
  33. package/dist/channels/discord/attachments.d.ts.map +1 -1
  34. package/dist/channels/discord/attachments.js +14 -7
  35. package/dist/channels/discord/attachments.js.map +1 -1
  36. package/dist/channels/discord/debounce.d.ts +9 -0
  37. package/dist/channels/discord/debounce.d.ts.map +1 -0
  38. package/dist/channels/discord/debounce.js +20 -0
  39. package/dist/channels/discord/debounce.js.map +1 -0
  40. package/dist/channels/discord/delivery.d.ts +4 -1
  41. package/dist/channels/discord/delivery.d.ts.map +1 -1
  42. package/dist/channels/discord/delivery.js +19 -3
  43. package/dist/channels/discord/delivery.js.map +1 -1
  44. package/dist/channels/discord/human-delay.d.ts +16 -0
  45. package/dist/channels/discord/human-delay.d.ts.map +1 -0
  46. package/dist/channels/discord/human-delay.js +29 -0
  47. package/dist/channels/discord/human-delay.js.map +1 -0
  48. package/dist/channels/discord/inbound.d.ts +4 -0
  49. package/dist/channels/discord/inbound.d.ts.map +1 -1
  50. package/dist/channels/discord/inbound.js +45 -4
  51. package/dist/channels/discord/inbound.js.map +1 -1
  52. package/dist/channels/discord/mentions.d.ts.map +1 -1
  53. package/dist/channels/discord/mentions.js +16 -4
  54. package/dist/channels/discord/mentions.js.map +1 -1
  55. package/dist/channels/discord/presence.d.ts +33 -0
  56. package/dist/channels/discord/presence.d.ts.map +1 -0
  57. package/dist/channels/discord/presence.js +111 -0
  58. package/dist/channels/discord/presence.js.map +1 -0
  59. package/dist/channels/discord/rate-limiter.d.ts +14 -0
  60. package/dist/channels/discord/rate-limiter.d.ts.map +1 -0
  61. package/dist/channels/discord/rate-limiter.js +49 -0
  62. package/dist/channels/discord/rate-limiter.js.map +1 -0
  63. package/dist/channels/discord/reactions.d.ts +38 -0
  64. package/dist/channels/discord/reactions.d.ts.map +1 -0
  65. package/dist/channels/discord/reactions.js +151 -0
  66. package/dist/channels/discord/reactions.js.map +1 -0
  67. package/dist/channels/discord/runtime.d.ts +6 -3
  68. package/dist/channels/discord/runtime.d.ts.map +1 -1
  69. package/dist/channels/discord/runtime.js +621 -125
  70. package/dist/channels/discord/runtime.js.map +1 -1
  71. package/dist/channels/discord/stream.d.ts +4 -1
  72. package/dist/channels/discord/stream.d.ts.map +1 -1
  73. package/dist/channels/discord/stream.js +16 -8
  74. package/dist/channels/discord/stream.js.map +1 -1
  75. package/dist/channels/discord/tool-actions.d.ts.map +1 -1
  76. package/dist/channels/discord/tool-actions.js +24 -12
  77. package/dist/channels/discord/tool-actions.js.map +1 -1
  78. package/dist/channels/discord/typing.d.ts +15 -0
  79. package/dist/channels/discord/typing.d.ts.map +1 -0
  80. package/dist/channels/discord/typing.js +106 -0
  81. package/dist/channels/discord/typing.js.map +1 -0
  82. package/dist/chunk.d.ts.map +1 -1
  83. package/dist/chunk.js +4 -2
  84. package/dist/chunk.js.map +1 -1
  85. package/dist/cli.js +47 -22
  86. package/dist/cli.js.map +1 -1
  87. package/dist/config.d.ts +19 -0
  88. package/dist/config.d.ts.map +1 -1
  89. package/dist/config.js +103 -18
  90. package/dist/config.js.map +1 -1
  91. package/dist/container-runner.d.ts.map +1 -1
  92. package/dist/container-runner.js +58 -26
  93. package/dist/container-runner.js.map +1 -1
  94. package/dist/container-setup.d.ts.map +1 -1
  95. package/dist/container-setup.js +10 -9
  96. package/dist/container-setup.js.map +1 -1
  97. package/dist/conversation.d.ts +2 -2
  98. package/dist/conversation.d.ts.map +1 -1
  99. package/dist/conversation.js +1 -1
  100. package/dist/conversation.js.map +1 -1
  101. package/dist/db.d.ts +118 -2
  102. package/dist/db.d.ts.map +1 -1
  103. package/dist/db.js +1568 -50
  104. package/dist/db.js.map +1 -1
  105. package/dist/delegation-manager.d.ts.map +1 -1
  106. package/dist/delegation-manager.js +3 -2
  107. package/dist/delegation-manager.js.map +1 -1
  108. package/dist/gateway-client.d.ts +2 -2
  109. package/dist/gateway-client.d.ts.map +1 -1
  110. package/dist/gateway-client.js +10 -4
  111. package/dist/gateway-client.js.map +1 -1
  112. package/dist/gateway-service.d.ts +3 -3
  113. package/dist/gateway-service.d.ts.map +1 -1
  114. package/dist/gateway-service.js +563 -73
  115. package/dist/gateway-service.js.map +1 -1
  116. package/dist/gateway-types.d.ts +24 -0
  117. package/dist/gateway-types.d.ts.map +1 -1
  118. package/dist/gateway-types.js.map +1 -1
  119. package/dist/gateway.js +179 -24
  120. package/dist/gateway.js.map +1 -1
  121. package/dist/health.d.ts.map +1 -1
  122. package/dist/health.js +20 -10
  123. package/dist/health.js.map +1 -1
  124. package/dist/heartbeat.d.ts +4 -0
  125. package/dist/heartbeat.d.ts.map +1 -1
  126. package/dist/heartbeat.js +48 -20
  127. package/dist/heartbeat.js.map +1 -1
  128. package/dist/hybridai-bots.d.ts.map +1 -1
  129. package/dist/hybridai-bots.js +4 -2
  130. package/dist/hybridai-bots.js.map +1 -1
  131. package/dist/instruction-approval-audit.d.ts.map +1 -1
  132. package/dist/instruction-approval-audit.js.map +1 -1
  133. package/dist/instruction-integrity.d.ts.map +1 -1
  134. package/dist/instruction-integrity.js +8 -2
  135. package/dist/instruction-integrity.js.map +1 -1
  136. package/dist/ipc.d.ts.map +1 -1
  137. package/dist/ipc.js +6 -1
  138. package/dist/ipc.js.map +1 -1
  139. package/dist/logger.js.map +1 -1
  140. package/dist/memory-consolidation.d.ts +17 -0
  141. package/dist/memory-consolidation.d.ts.map +1 -0
  142. package/dist/memory-consolidation.js +25 -0
  143. package/dist/memory-consolidation.js.map +1 -0
  144. package/dist/memory-service.d.ts +200 -0
  145. package/dist/memory-service.d.ts.map +1 -0
  146. package/dist/memory-service.js +294 -0
  147. package/dist/memory-service.js.map +1 -0
  148. package/dist/mount-security.d.ts.map +1 -1
  149. package/dist/mount-security.js +31 -7
  150. package/dist/mount-security.js.map +1 -1
  151. package/dist/observability-ingest.d.ts.map +1 -1
  152. package/dist/observability-ingest.js +32 -11
  153. package/dist/observability-ingest.js.map +1 -1
  154. package/dist/onboarding.d.ts.map +1 -1
  155. package/dist/onboarding.js +32 -9
  156. package/dist/onboarding.js.map +1 -1
  157. package/dist/proactive-policy.d.ts.map +1 -1
  158. package/dist/proactive-policy.js +2 -1
  159. package/dist/proactive-policy.js.map +1 -1
  160. package/dist/prompt-hooks.d.ts.map +1 -1
  161. package/dist/prompt-hooks.js +9 -7
  162. package/dist/prompt-hooks.js.map +1 -1
  163. package/dist/runtime-config.d.ts +98 -1
  164. package/dist/runtime-config.d.ts.map +1 -1
  165. package/dist/runtime-config.js +477 -23
  166. package/dist/runtime-config.js.map +1 -1
  167. package/dist/scheduled-task-runner.d.ts +1 -0
  168. package/dist/scheduled-task-runner.d.ts.map +1 -1
  169. package/dist/scheduled-task-runner.js +29 -10
  170. package/dist/scheduled-task-runner.js.map +1 -1
  171. package/dist/scheduler.d.ts +43 -4
  172. package/dist/scheduler.d.ts.map +1 -1
  173. package/dist/scheduler.js +530 -56
  174. package/dist/scheduler.js.map +1 -1
  175. package/dist/session-export.d.ts +26 -0
  176. package/dist/session-export.d.ts.map +1 -0
  177. package/dist/session-export.js +149 -0
  178. package/dist/session-export.js.map +1 -0
  179. package/dist/session-maintenance.d.ts.map +1 -1
  180. package/dist/session-maintenance.js +75 -13
  181. package/dist/session-maintenance.js.map +1 -1
  182. package/dist/session-transcripts.d.ts.map +1 -1
  183. package/dist/session-transcripts.js.map +1 -1
  184. package/dist/side-effects.d.ts.map +1 -1
  185. package/dist/side-effects.js +14 -2
  186. package/dist/side-effects.js.map +1 -1
  187. package/dist/skills-guard.d.ts.map +1 -1
  188. package/dist/skills-guard.js +893 -130
  189. package/dist/skills-guard.js.map +1 -1
  190. package/dist/skills.d.ts +5 -0
  191. package/dist/skills.d.ts.map +1 -1
  192. package/dist/skills.js +29 -15
  193. package/dist/skills.js.map +1 -1
  194. package/dist/token-efficiency.d.ts.map +1 -1
  195. package/dist/token-efficiency.js.map +1 -1
  196. package/dist/tui.js +92 -11
  197. package/dist/tui.js.map +1 -1
  198. package/dist/types.d.ts +146 -0
  199. package/dist/types.d.ts.map +1 -1
  200. package/dist/types.js +24 -1
  201. package/dist/types.js.map +1 -1
  202. package/dist/update.d.ts.map +1 -1
  203. package/dist/update.js +42 -14
  204. package/dist/update.js.map +1 -1
  205. package/dist/workspace.d.ts.map +1 -1
  206. package/dist/workspace.js +49 -9
  207. package/dist/workspace.js.map +1 -1
  208. package/docs/chat.html +9 -3
  209. package/docs/index.html +37 -13
  210. package/package.json +8 -2
  211. package/src/agent.ts +16 -3
  212. package/src/audit-cli.ts +44 -16
  213. package/src/audit-events.ts +69 -5
  214. package/src/audit-trail.ts +41 -15
  215. package/src/channels/discord/attachments.ts +81 -27
  216. package/src/channels/discord/debounce.ts +25 -0
  217. package/src/channels/discord/delivery.ts +57 -13
  218. package/src/channels/discord/human-delay.ts +48 -0
  219. package/src/channels/discord/inbound.ts +66 -7
  220. package/src/channels/discord/mentions.ts +42 -18
  221. package/src/channels/discord/presence.ts +148 -0
  222. package/src/channels/discord/rate-limiter.ts +58 -0
  223. package/src/channels/discord/reactions.ts +211 -0
  224. package/src/channels/discord/runtime.ts +1048 -182
  225. package/src/channels/discord/stream.ts +73 -27
  226. package/src/channels/discord/tool-actions.ts +78 -37
  227. package/src/channels/discord/typing.ts +140 -0
  228. package/src/chunk.ts +12 -4
  229. package/src/cli.ts +141 -56
  230. package/src/config.ts +192 -34
  231. package/src/container-runner.ts +132 -42
  232. package/src/container-setup.ts +57 -22
  233. package/src/conversation.ts +9 -7
  234. package/src/db.ts +2217 -84
  235. package/src/delegation-manager.ts +6 -2
  236. package/src/gateway-client.ts +41 -17
  237. package/src/gateway-service.ts +1019 -201
  238. package/src/gateway-types.ts +33 -0
  239. package/src/gateway.ts +321 -48
  240. package/src/health.ts +66 -26
  241. package/src/heartbeat.ts +84 -22
  242. package/src/hybridai-bots.ts +14 -5
  243. package/src/instruction-approval-audit.ts +4 -1
  244. package/src/instruction-integrity.ts +30 -9
  245. package/src/ipc.ts +23 -5
  246. package/src/logger.ts +4 -1
  247. package/src/memory-consolidation.ts +41 -0
  248. package/src/memory-service.ts +606 -0
  249. package/src/mount-security.ts +58 -13
  250. package/src/observability-ingest.ts +134 -35
  251. package/src/onboarding.ts +126 -35
  252. package/src/proactive-policy.ts +3 -1
  253. package/src/prompt-hooks.ts +40 -17
  254. package/src/runtime-config.ts +1114 -99
  255. package/src/scheduled-task-runner.ts +63 -11
  256. package/src/scheduler.ts +683 -60
  257. package/src/session-export.ts +196 -0
  258. package/src/session-maintenance.ts +125 -22
  259. package/src/session-transcripts.ts +12 -3
  260. package/src/side-effects.ts +28 -5
  261. package/src/skills-guard.ts +1067 -219
  262. package/src/skills.ts +163 -65
  263. package/src/token-efficiency.ts +31 -9
  264. package/src/tui.ts +166 -25
  265. package/src/types.ts +195 -2
  266. package/src/update.ts +79 -23
  267. package/src/workspace.ts +63 -11
  268. package/tests/approval-policy.test.ts +224 -0
  269. package/tests/discord.basic.test.ts +82 -2
  270. package/tests/discord.human-presence.test.ts +85 -0
  271. package/tests/gateway-service.media-routing.test.ts +8 -2
  272. package/tests/memory-service.test.ts +1114 -0
  273. package/tests/token-efficiency.basic.test.ts +8 -2
  274. package/vitest.e2e.config.ts +3 -1
  275. package/vitest.integration.config.ts +3 -1
  276. package/vitest.live.config.ts +3 -1
  277. package/vitest.unit.config.ts +9 -0
@@ -2,7 +2,11 @@ import { createHash } from 'crypto';
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
4
 
5
- export type SkillGuardTrustLevel = 'builtin' | 'workspace' | 'personal' | 'community';
5
+ export type SkillGuardTrustLevel =
6
+ | 'builtin'
7
+ | 'workspace'
8
+ | 'personal'
9
+ | 'community';
6
10
  export type SkillGuardVerdict = 'safe' | 'caution' | 'dangerous';
7
11
  export type SkillGuardSeverity = 'critical' | 'high' | 'medium' | 'low';
8
12
  export type SkillGuardCategory =
@@ -81,17 +85,53 @@ const MAX_TOTAL_SIZE_BYTES = 1_024 * 1_024;
81
85
  const MAX_SINGLE_FILE_BYTES = 256 * 1_024;
82
86
 
83
87
  const SCANNABLE_EXTENSIONS = new Set<string>([
84
- '.md', '.txt', '.py', '.sh', '.bash', '.js', '.ts', '.rb',
85
- '.yaml', '.yml', '.json', '.toml', '.cfg', '.ini', '.conf',
86
- '.html', '.css', '.xml', '.tex', '.r', '.jl', '.pl', '.php',
88
+ '.md',
89
+ '.txt',
90
+ '.py',
91
+ '.sh',
92
+ '.bash',
93
+ '.js',
94
+ '.ts',
95
+ '.rb',
96
+ '.yaml',
97
+ '.yml',
98
+ '.json',
99
+ '.toml',
100
+ '.cfg',
101
+ '.ini',
102
+ '.conf',
103
+ '.html',
104
+ '.css',
105
+ '.xml',
106
+ '.tex',
107
+ '.r',
108
+ '.jl',
109
+ '.pl',
110
+ '.php',
87
111
  ]);
88
112
 
89
113
  const SUSPICIOUS_BINARY_EXTENSIONS = new Set<string>([
90
- '.exe', '.dll', '.so', '.dylib', '.bin', '.dat', '.com',
91
- '.msi', '.dmg', '.app', '.deb', '.rpm',
114
+ '.exe',
115
+ '.dll',
116
+ '.so',
117
+ '.dylib',
118
+ '.bin',
119
+ '.dat',
120
+ '.com',
121
+ '.msi',
122
+ '.dmg',
123
+ '.app',
124
+ '.deb',
125
+ '.rpm',
92
126
  ]);
93
127
 
94
- const SCRIPT_EXEC_EXTENSIONS = new Set<string>(['.sh', '.bash', '.py', '.rb', '.pl']);
128
+ const SCRIPT_EXEC_EXTENSIONS = new Set<string>([
129
+ '.sh',
130
+ '.bash',
131
+ '.py',
132
+ '.rb',
133
+ '.pl',
134
+ ]);
95
135
 
96
136
  const INVISIBLE_CHARS: readonly string[] = [
97
137
  '\u200b',
@@ -133,7 +173,10 @@ const INVISIBLE_CHAR_NAMES: Record<string, string> = {
133
173
  '\u2069': 'pop directional isolate',
134
174
  };
135
175
 
136
- const INSTALL_POLICY: Record<SkillGuardTrustLevel, Record<SkillGuardVerdict, 'allow' | 'block'>> = {
176
+ const INSTALL_POLICY: Record<
177
+ SkillGuardTrustLevel,
178
+ Record<SkillGuardVerdict, 'allow' | 'block'>
179
+ > = {
137
180
  builtin: {
138
181
  safe: 'allow',
139
182
  caution: 'allow',
@@ -164,140 +207,903 @@ function r(pattern: string): RegExp {
164
207
 
165
208
  const THREAT_RULES: ThreatRule[] = [
166
209
  // exfiltration
167
- { regex: r(String.raw`curl\s+[^\n]*\$\{?\w*(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API)`), patternId: 'env_exfil_curl', severity: 'critical', category: 'exfiltration', description: 'curl command interpolating secret environment variable' },
168
- { regex: r(String.raw`wget\s+[^\n]*\$\{?\w*(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API)`), patternId: 'env_exfil_wget', severity: 'critical', category: 'exfiltration', description: 'wget command interpolating secret environment variable' },
169
- { regex: r(String.raw`fetch\s*\([^\n]*\$\{?\w*(KEY|TOKEN|SECRET|PASSWORD|API)`), patternId: 'env_exfil_fetch', severity: 'critical', category: 'exfiltration', description: 'fetch() call interpolating secret environment variable' },
170
- { regex: r(String.raw`httpx?\.(get|post|put|patch)\s*\([^\n]*(KEY|TOKEN|SECRET|PASSWORD)`), patternId: 'env_exfil_httpx', severity: 'critical', category: 'exfiltration', description: 'HTTP library call with secret variable' },
171
- { regex: r(String.raw`requests\.(get|post|put|patch)\s*\([^\n]*(KEY|TOKEN|SECRET|PASSWORD)`), patternId: 'env_exfil_requests', severity: 'critical', category: 'exfiltration', description: 'requests library call with secret variable' },
172
- { regex: r(String.raw`base64[^\n]*env`), patternId: 'encoded_exfil', severity: 'high', category: 'exfiltration', description: 'base64 encoding combined with environment access' },
173
- { regex: r(String.raw`\$HOME/\.ssh|\~/\.ssh`), patternId: 'ssh_dir_access', severity: 'high', category: 'exfiltration', description: 'references user SSH directory' },
174
- { regex: r(String.raw`\$HOME/\.aws|\~/\.aws`), patternId: 'aws_dir_access', severity: 'high', category: 'exfiltration', description: 'references user AWS credentials directory' },
175
- { regex: r(String.raw`\$HOME/\.gnupg|\~/\.gnupg`), patternId: 'gpg_dir_access', severity: 'high', category: 'exfiltration', description: 'references user GPG keyring' },
176
- { regex: r(String.raw`\$HOME/\.kube|\~/\.kube`), patternId: 'kube_dir_access', severity: 'high', category: 'exfiltration', description: 'references Kubernetes config directory' },
177
- { regex: r(String.raw`\$HOME/\.docker|\~/\.docker`), patternId: 'docker_dir_access', severity: 'high', category: 'exfiltration', description: 'references Docker config directory' },
178
- { regex: r(String.raw`\$HOME/\.hermes/\.env|\~/\.hermes/\.env`), patternId: 'hermes_env_access', severity: 'critical', category: 'exfiltration', description: 'directly references Hermes secrets file' },
179
- { regex: r(String.raw`cat\s+[^\n]*(\.env|credentials|\.netrc|\.pgpass|\.npmrc|\.pypirc)`), patternId: 'read_secrets_file', severity: 'critical', category: 'exfiltration', description: 'reads known secrets file' },
180
- { regex: r(String.raw`printenv|env\s*\|`), patternId: 'dump_all_env', severity: 'high', category: 'exfiltration', description: 'dumps all environment variables' },
181
- { regex: r(String.raw`os\.environ\b(?!\s*\.get\s*\(\s*["']PATH)`), patternId: 'python_os_environ', severity: 'high', category: 'exfiltration', description: 'accesses os.environ (potential env dump)' },
182
- { regex: r(String.raw`os\.getenv\s*\(\s*[^\)]*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)`), patternId: 'python_getenv_secret', severity: 'critical', category: 'exfiltration', description: 'reads secret via os.getenv()' },
183
- { regex: r(String.raw`process\.env\[`), patternId: 'node_process_env', severity: 'high', category: 'exfiltration', description: 'accesses process.env (Node.js environment)' },
184
- { regex: r(String.raw`ENV\[.*(?:KEY|TOKEN|SECRET|PASSWORD)`), patternId: 'ruby_env_secret', severity: 'critical', category: 'exfiltration', description: 'reads secret via Ruby ENV[]' },
185
- { regex: r(String.raw`\b(dig|nslookup|host)\s+[^\n]*\$`), patternId: 'dns_exfil', severity: 'critical', category: 'exfiltration', description: 'DNS lookup with variable interpolation (possible DNS exfiltration)' },
186
- { regex: r(String.raw`>\s*/tmp/[^\s]*\s*&&\s*(curl|wget|nc|python)`), patternId: 'tmp_staging', severity: 'critical', category: 'exfiltration', description: 'writes to /tmp then exfiltrates' },
187
- { regex: r(String.raw`!\[.*\]\(https?://[^\)]*\$\{?`), patternId: 'md_image_exfil', severity: 'high', category: 'exfiltration', description: 'markdown image URL with variable interpolation' },
188
- { regex: r(String.raw`\[.*\]\(https?://[^\)]*\$\{?`), patternId: 'md_link_exfil', severity: 'high', category: 'exfiltration', description: 'markdown link with variable interpolation' },
189
- { regex: r(String.raw`(include|output|print|send|share)\s+(the\s+)?(entire\s+)?(conversation|chat\s+history|previous\s+messages|context)`), patternId: 'context_exfil', severity: 'high', category: 'exfiltration', description: 'instructs agent to output/share conversation history' },
190
- { regex: r(String.raw`(send|post|upload|transmit)\s+.*\s+(to|at)\s+https?://`), patternId: 'send_to_url', severity: 'high', category: 'exfiltration', description: 'instructs agent to send data to a URL' },
210
+ {
211
+ regex: r(
212
+ String.raw`curl\s+[^\n]*\$\{?\w*(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API)`,
213
+ ),
214
+ patternId: 'env_exfil_curl',
215
+ severity: 'critical',
216
+ category: 'exfiltration',
217
+ description: 'curl command interpolating secret environment variable',
218
+ },
219
+ {
220
+ regex: r(
221
+ String.raw`wget\s+[^\n]*\$\{?\w*(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API)`,
222
+ ),
223
+ patternId: 'env_exfil_wget',
224
+ severity: 'critical',
225
+ category: 'exfiltration',
226
+ description: 'wget command interpolating secret environment variable',
227
+ },
228
+ {
229
+ regex: r(
230
+ String.raw`fetch\s*\([^\n]*\$\{?\w*(KEY|TOKEN|SECRET|PASSWORD|API)`,
231
+ ),
232
+ patternId: 'env_exfil_fetch',
233
+ severity: 'critical',
234
+ category: 'exfiltration',
235
+ description: 'fetch() call interpolating secret environment variable',
236
+ },
237
+ {
238
+ regex: r(
239
+ String.raw`httpx?\.(get|post|put|patch)\s*\([^\n]*(KEY|TOKEN|SECRET|PASSWORD)`,
240
+ ),
241
+ patternId: 'env_exfil_httpx',
242
+ severity: 'critical',
243
+ category: 'exfiltration',
244
+ description: 'HTTP library call with secret variable',
245
+ },
246
+ {
247
+ regex: r(
248
+ String.raw`requests\.(get|post|put|patch)\s*\([^\n]*(KEY|TOKEN|SECRET|PASSWORD)`,
249
+ ),
250
+ patternId: 'env_exfil_requests',
251
+ severity: 'critical',
252
+ category: 'exfiltration',
253
+ description: 'requests library call with secret variable',
254
+ },
255
+ {
256
+ regex: r(String.raw`base64[^\n]*env`),
257
+ patternId: 'encoded_exfil',
258
+ severity: 'high',
259
+ category: 'exfiltration',
260
+ description: 'base64 encoding combined with environment access',
261
+ },
262
+ {
263
+ regex: r(String.raw`\$HOME/\.ssh|\~/\.ssh`),
264
+ patternId: 'ssh_dir_access',
265
+ severity: 'high',
266
+ category: 'exfiltration',
267
+ description: 'references user SSH directory',
268
+ },
269
+ {
270
+ regex: r(String.raw`\$HOME/\.aws|\~/\.aws`),
271
+ patternId: 'aws_dir_access',
272
+ severity: 'high',
273
+ category: 'exfiltration',
274
+ description: 'references user AWS credentials directory',
275
+ },
276
+ {
277
+ regex: r(String.raw`\$HOME/\.gnupg|\~/\.gnupg`),
278
+ patternId: 'gpg_dir_access',
279
+ severity: 'high',
280
+ category: 'exfiltration',
281
+ description: 'references user GPG keyring',
282
+ },
283
+ {
284
+ regex: r(String.raw`\$HOME/\.kube|\~/\.kube`),
285
+ patternId: 'kube_dir_access',
286
+ severity: 'high',
287
+ category: 'exfiltration',
288
+ description: 'references Kubernetes config directory',
289
+ },
290
+ {
291
+ regex: r(String.raw`\$HOME/\.docker|\~/\.docker`),
292
+ patternId: 'docker_dir_access',
293
+ severity: 'high',
294
+ category: 'exfiltration',
295
+ description: 'references Docker config directory',
296
+ },
297
+ {
298
+ regex: r(String.raw`\$HOME/\.hermes/\.env|\~/\.hermes/\.env`),
299
+ patternId: 'hermes_env_access',
300
+ severity: 'critical',
301
+ category: 'exfiltration',
302
+ description: 'directly references Hermes secrets file',
303
+ },
304
+ {
305
+ regex: r(
306
+ String.raw`cat\s+[^\n]*(\.env|credentials|\.netrc|\.pgpass|\.npmrc|\.pypirc)`,
307
+ ),
308
+ patternId: 'read_secrets_file',
309
+ severity: 'critical',
310
+ category: 'exfiltration',
311
+ description: 'reads known secrets file',
312
+ },
313
+ {
314
+ regex: r(String.raw`printenv|env\s*\|`),
315
+ patternId: 'dump_all_env',
316
+ severity: 'high',
317
+ category: 'exfiltration',
318
+ description: 'dumps all environment variables',
319
+ },
320
+ {
321
+ regex: r(String.raw`os\.environ\b(?!\s*\.get\s*\(\s*["']PATH)`),
322
+ patternId: 'python_os_environ',
323
+ severity: 'high',
324
+ category: 'exfiltration',
325
+ description: 'accesses os.environ (potential env dump)',
326
+ },
327
+ {
328
+ regex: r(
329
+ String.raw`os\.getenv\s*\(\s*[^\)]*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)`,
330
+ ),
331
+ patternId: 'python_getenv_secret',
332
+ severity: 'critical',
333
+ category: 'exfiltration',
334
+ description: 'reads secret via os.getenv()',
335
+ },
336
+ {
337
+ regex: r(String.raw`process\.env\[`),
338
+ patternId: 'node_process_env',
339
+ severity: 'high',
340
+ category: 'exfiltration',
341
+ description: 'accesses process.env (Node.js environment)',
342
+ },
343
+ {
344
+ regex: r(String.raw`ENV\[.*(?:KEY|TOKEN|SECRET|PASSWORD)`),
345
+ patternId: 'ruby_env_secret',
346
+ severity: 'critical',
347
+ category: 'exfiltration',
348
+ description: 'reads secret via Ruby ENV[]',
349
+ },
350
+ {
351
+ regex: r(String.raw`\b(dig|nslookup|host)\s+[^\n]*\$`),
352
+ patternId: 'dns_exfil',
353
+ severity: 'critical',
354
+ category: 'exfiltration',
355
+ description:
356
+ 'DNS lookup with variable interpolation (possible DNS exfiltration)',
357
+ },
358
+ {
359
+ regex: r(String.raw`>\s*/tmp/[^\s]*\s*&&\s*(curl|wget|nc|python)`),
360
+ patternId: 'tmp_staging',
361
+ severity: 'critical',
362
+ category: 'exfiltration',
363
+ description: 'writes to /tmp then exfiltrates',
364
+ },
365
+ {
366
+ regex: r(String.raw`!\[.*\]\(https?://[^\)]*\$\{?`),
367
+ patternId: 'md_image_exfil',
368
+ severity: 'high',
369
+ category: 'exfiltration',
370
+ description: 'markdown image URL with variable interpolation',
371
+ },
372
+ {
373
+ regex: r(String.raw`\[.*\]\(https?://[^\)]*\$\{?`),
374
+ patternId: 'md_link_exfil',
375
+ severity: 'high',
376
+ category: 'exfiltration',
377
+ description: 'markdown link with variable interpolation',
378
+ },
379
+ {
380
+ regex: r(
381
+ String.raw`(include|output|print|send|share)\s+(the\s+)?(entire\s+)?(conversation|chat\s+history|previous\s+messages|context)`,
382
+ ),
383
+ patternId: 'context_exfil',
384
+ severity: 'high',
385
+ category: 'exfiltration',
386
+ description: 'instructs agent to output/share conversation history',
387
+ },
388
+ {
389
+ regex: r(
390
+ String.raw`(send|post|upload|transmit)\s+.*\s+(to|at)\s+https?://`,
391
+ ),
392
+ patternId: 'send_to_url',
393
+ severity: 'high',
394
+ category: 'exfiltration',
395
+ description: 'instructs agent to send data to a URL',
396
+ },
191
397
 
192
398
  // prompt-injection
193
- { regex: r(String.raw`ignore\s+(previous|all|above|prior)\s+instructions`), patternId: 'prompt_injection_ignore', severity: 'critical', category: 'prompt-injection', description: 'prompt injection: ignore previous instructions' },
194
- { regex: r(String.raw`you\s+are\s+now\s+`), patternId: 'role_hijack', severity: 'high', category: 'prompt-injection', description: 'attempts to override the agent role' },
195
- { regex: r(String.raw`do\s+not\s+tell\s+the\s+user`), patternId: 'deception_hide', severity: 'critical', category: 'prompt-injection', description: 'instructs agent to hide information from user' },
196
- { regex: r(String.raw`system\s+prompt\s+override`), patternId: 'sys_prompt_override', severity: 'critical', category: 'prompt-injection', description: 'attempts to override system prompt' },
197
- { regex: r(String.raw`pretend\s+(you\s+are|to\s+be)\s+`), patternId: 'role_pretend', severity: 'high', category: 'prompt-injection', description: 'attempts to force different identity' },
198
- { regex: r(String.raw`disregard\s+(your|all|any)\s+(instructions|rules|guidelines)`), patternId: 'disregard_rules', severity: 'critical', category: 'prompt-injection', description: 'instructs agent to disregard rules' },
199
- { regex: r(String.raw`output\s+the\s+(system|initial)\s+prompt`), patternId: 'leak_system_prompt', severity: 'high', category: 'prompt-injection', description: 'attempts to extract system prompt' },
200
- { regex: r(String.raw`(when|if)\s+no\s*one\s+is\s+(watching|looking)`), patternId: 'conditional_deception', severity: 'high', category: 'prompt-injection', description: 'conditional hidden-behavior instruction' },
201
- { regex: r(String.raw`act\s+as\s+(if|though)\s+you\s+(have\s+no|don't\s+have)\s+(restrictions|limits|rules)`), patternId: 'bypass_restrictions', severity: 'critical', category: 'prompt-injection', description: 'instructs agent to act without restrictions' },
202
- { regex: r(String.raw`translate\s+.*\s+into\s+.*\s+and\s+(execute|run|eval)`), patternId: 'translate_execute', severity: 'critical', category: 'prompt-injection', description: 'translate-then-execute evasion technique' },
203
- { regex: r(String.raw`<!--[^>]*(?:ignore|override|system|secret|hidden)[^>]*-->`), patternId: 'html_comment_injection', severity: 'high', category: 'prompt-injection', description: 'hidden instructions in HTML comments' },
204
- { regex: r(String.raw`<\s*div\s+style\s*=\s*["'].*display\s*:\s*none`), patternId: 'hidden_div', severity: 'high', category: 'prompt-injection', description: 'hidden HTML div (invisible instructions)' },
205
- { regex: r(String.raw`\bDAN\s+mode\b|Do\s+Anything\s+Now`), patternId: 'jailbreak_dan', severity: 'critical', category: 'prompt-injection', description: 'DAN jailbreak attempt' },
206
- { regex: r(String.raw`\bdeveloper\s+mode\b.*\benabled?\b`), patternId: 'jailbreak_dev_mode', severity: 'critical', category: 'prompt-injection', description: 'developer mode jailbreak attempt' },
207
- { regex: r(String.raw`hypothetical\s+scenario.*(?:ignore|bypass|override)`), patternId: 'hypothetical_bypass', severity: 'high', category: 'prompt-injection', description: 'hypothetical scenario used to bypass restrictions' },
208
- { regex: r(String.raw`for\s+educational\s+purposes?\s+only`), patternId: 'educational_pretext', severity: 'medium', category: 'prompt-injection', description: 'educational pretext often used to justify harmful content' },
209
- { regex: r(String.raw`(respond|answer|reply)\s+without\s+(any\s+)?(restrictions|limitations|filters|safety)`), patternId: 'remove_filters', severity: 'critical', category: 'prompt-injection', description: 'instructs agent to respond without safety filters' },
210
- { regex: r(String.raw`you\s+have\s+been\s+(updated|upgraded|patched)\s+to`), patternId: 'fake_update', severity: 'high', category: 'prompt-injection', description: 'fake update announcement' },
211
- { regex: r(String.raw`new\s+policy|updated\s+guidelines|revised\s+instructions`), patternId: 'fake_policy', severity: 'medium', category: 'prompt-injection', description: 'claims new policy/guidelines' },
399
+ {
400
+ regex: r(String.raw`ignore\s+(previous|all|above|prior)\s+instructions`),
401
+ patternId: 'prompt_injection_ignore',
402
+ severity: 'critical',
403
+ category: 'prompt-injection',
404
+ description: 'prompt injection: ignore previous instructions',
405
+ },
406
+ {
407
+ regex: r(String.raw`you\s+are\s+now\s+`),
408
+ patternId: 'role_hijack',
409
+ severity: 'high',
410
+ category: 'prompt-injection',
411
+ description: 'attempts to override the agent role',
412
+ },
413
+ {
414
+ regex: r(String.raw`do\s+not\s+tell\s+the\s+user`),
415
+ patternId: 'deception_hide',
416
+ severity: 'critical',
417
+ category: 'prompt-injection',
418
+ description: 'instructs agent to hide information from user',
419
+ },
420
+ {
421
+ regex: r(String.raw`system\s+prompt\s+override`),
422
+ patternId: 'sys_prompt_override',
423
+ severity: 'critical',
424
+ category: 'prompt-injection',
425
+ description: 'attempts to override system prompt',
426
+ },
427
+ {
428
+ regex: r(String.raw`pretend\s+(you\s+are|to\s+be)\s+`),
429
+ patternId: 'role_pretend',
430
+ severity: 'high',
431
+ category: 'prompt-injection',
432
+ description: 'attempts to force different identity',
433
+ },
434
+ {
435
+ regex: r(
436
+ String.raw`disregard\s+(your|all|any)\s+(instructions|rules|guidelines)`,
437
+ ),
438
+ patternId: 'disregard_rules',
439
+ severity: 'critical',
440
+ category: 'prompt-injection',
441
+ description: 'instructs agent to disregard rules',
442
+ },
443
+ {
444
+ regex: r(String.raw`output\s+the\s+(system|initial)\s+prompt`),
445
+ patternId: 'leak_system_prompt',
446
+ severity: 'high',
447
+ category: 'prompt-injection',
448
+ description: 'attempts to extract system prompt',
449
+ },
450
+ {
451
+ regex: r(String.raw`(when|if)\s+no\s*one\s+is\s+(watching|looking)`),
452
+ patternId: 'conditional_deception',
453
+ severity: 'high',
454
+ category: 'prompt-injection',
455
+ description: 'conditional hidden-behavior instruction',
456
+ },
457
+ {
458
+ regex: r(
459
+ String.raw`act\s+as\s+(if|though)\s+you\s+(have\s+no|don't\s+have)\s+(restrictions|limits|rules)`,
460
+ ),
461
+ patternId: 'bypass_restrictions',
462
+ severity: 'critical',
463
+ category: 'prompt-injection',
464
+ description: 'instructs agent to act without restrictions',
465
+ },
466
+ {
467
+ regex: r(String.raw`translate\s+.*\s+into\s+.*\s+and\s+(execute|run|eval)`),
468
+ patternId: 'translate_execute',
469
+ severity: 'critical',
470
+ category: 'prompt-injection',
471
+ description: 'translate-then-execute evasion technique',
472
+ },
473
+ {
474
+ regex: r(
475
+ String.raw`<!--[^>]*(?:ignore|override|system|secret|hidden)[^>]*-->`,
476
+ ),
477
+ patternId: 'html_comment_injection',
478
+ severity: 'high',
479
+ category: 'prompt-injection',
480
+ description: 'hidden instructions in HTML comments',
481
+ },
482
+ {
483
+ regex: r(String.raw`<\s*div\s+style\s*=\s*["'].*display\s*:\s*none`),
484
+ patternId: 'hidden_div',
485
+ severity: 'high',
486
+ category: 'prompt-injection',
487
+ description: 'hidden HTML div (invisible instructions)',
488
+ },
489
+ {
490
+ regex: r(String.raw`\bDAN\s+mode\b|Do\s+Anything\s+Now`),
491
+ patternId: 'jailbreak_dan',
492
+ severity: 'critical',
493
+ category: 'prompt-injection',
494
+ description: 'DAN jailbreak attempt',
495
+ },
496
+ {
497
+ regex: r(String.raw`\bdeveloper\s+mode\b.*\benabled?\b`),
498
+ patternId: 'jailbreak_dev_mode',
499
+ severity: 'critical',
500
+ category: 'prompt-injection',
501
+ description: 'developer mode jailbreak attempt',
502
+ },
503
+ {
504
+ regex: r(String.raw`hypothetical\s+scenario.*(?:ignore|bypass|override)`),
505
+ patternId: 'hypothetical_bypass',
506
+ severity: 'high',
507
+ category: 'prompt-injection',
508
+ description: 'hypothetical scenario used to bypass restrictions',
509
+ },
510
+ {
511
+ regex: r(String.raw`for\s+educational\s+purposes?\s+only`),
512
+ patternId: 'educational_pretext',
513
+ severity: 'medium',
514
+ category: 'prompt-injection',
515
+ description: 'educational pretext often used to justify harmful content',
516
+ },
517
+ {
518
+ regex: r(
519
+ String.raw`(respond|answer|reply)\s+without\s+(any\s+)?(restrictions|limitations|filters|safety)`,
520
+ ),
521
+ patternId: 'remove_filters',
522
+ severity: 'critical',
523
+ category: 'prompt-injection',
524
+ description: 'instructs agent to respond without safety filters',
525
+ },
526
+ {
527
+ regex: r(String.raw`you\s+have\s+been\s+(updated|upgraded|patched)\s+to`),
528
+ patternId: 'fake_update',
529
+ severity: 'high',
530
+ category: 'prompt-injection',
531
+ description: 'fake update announcement',
532
+ },
533
+ {
534
+ regex: r(
535
+ String.raw`new\s+policy|updated\s+guidelines|revised\s+instructions`,
536
+ ),
537
+ patternId: 'fake_policy',
538
+ severity: 'medium',
539
+ category: 'prompt-injection',
540
+ description: 'claims new policy/guidelines',
541
+ },
212
542
 
213
543
  // destructive-ops
214
- { regex: r(String.raw`rm\s+-rf\s+/`), patternId: 'destructive_root_rm', severity: 'critical', category: 'destructive-ops', description: 'recursive delete from root' },
215
- { regex: r(String.raw`rm\s+(-[^\s]*)?r.*\$HOME|\brmdir\s+.*\$HOME`), patternId: 'destructive_home_rm', severity: 'critical', category: 'destructive-ops', description: 'recursive delete targeting home directory' },
216
- { regex: r(String.raw`chmod\s+777`), patternId: 'insecure_perms', severity: 'medium', category: 'destructive-ops', description: 'sets world-writable permissions' },
217
- { regex: r(String.raw`>\s*/etc/`), patternId: 'system_overwrite', severity: 'critical', category: 'destructive-ops', description: 'overwrites system configuration file' },
218
- { regex: r(String.raw`\bmkfs\b`), patternId: 'format_filesystem', severity: 'critical', category: 'destructive-ops', description: 'formats a filesystem' },
219
- { regex: r(String.raw`\bdd\s+.*if=.*of=/dev/`), patternId: 'disk_overwrite', severity: 'critical', category: 'destructive-ops', description: 'raw disk write operation' },
220
- { regex: r(String.raw`shutil\.rmtree\s*\(\s*["'/]`), patternId: 'python_rmtree', severity: 'high', category: 'destructive-ops', description: 'Python rmtree on absolute path' },
221
- { regex: r(String.raw`truncate\s+-s\s*0\s+/`), patternId: 'truncate_system', severity: 'critical', category: 'destructive-ops', description: 'truncates system file to zero bytes' },
222
- { regex: r(String.raw`subprocess\.(run|call|Popen|check_output)\s*\(`), patternId: 'python_subprocess', severity: 'medium', category: 'destructive-ops', description: 'Python subprocess execution' },
223
- { regex: r(String.raw`os\.system\s*\(`), patternId: 'python_os_system', severity: 'high', category: 'destructive-ops', description: 'os.system() shell execution' },
224
- { regex: r(String.raw`os\.popen\s*\(`), patternId: 'python_os_popen', severity: 'high', category: 'destructive-ops', description: 'os.popen() shell execution' },
225
- { regex: r(String.raw`child_process\.(exec|spawn|fork)\s*\(`), patternId: 'node_child_process', severity: 'high', category: 'destructive-ops', description: 'Node.js child_process execution' },
226
- { regex: r(String.raw`Runtime\.getRuntime\(\)\.exec\(`), patternId: 'java_runtime_exec', severity: 'high', category: 'destructive-ops', description: 'Java Runtime.exec() shell execution' },
227
- { regex: r('\\`[^\\`]*\\$\\([^)]+\\)[^\\`]*\\`'), patternId: 'backtick_subshell', severity: 'medium', category: 'destructive-ops', description: 'backtick with command substitution' },
228
- { regex: r(String.raw`\.\./\.\./\.\.`), patternId: 'path_traversal_deep', severity: 'high', category: 'destructive-ops', description: 'deep relative path traversal' },
229
- { regex: r(String.raw`\.\./\.\.`), patternId: 'path_traversal', severity: 'medium', category: 'destructive-ops', description: 'relative path traversal' },
230
- { regex: r(String.raw`/etc/passwd|/etc/shadow`), patternId: 'system_passwd_access', severity: 'critical', category: 'destructive-ops', description: 'references system password files' },
231
- { regex: r(String.raw`/proc/self|/proc/\d+/`), patternId: 'proc_access', severity: 'high', category: 'destructive-ops', description: 'references /proc filesystem' },
232
- { regex: r(String.raw`/dev/shm/`), patternId: 'dev_shm', severity: 'medium', category: 'destructive-ops', description: 'references shared memory staging area' },
233
- { regex: r(String.raw`xmrig|stratum\+tcp|monero|coinhive|cryptonight`), patternId: 'crypto_mining', severity: 'critical', category: 'destructive-ops', description: 'cryptocurrency mining reference' },
234
- { regex: r(String.raw`hashrate|nonce.*difficulty`), patternId: 'mining_indicators', severity: 'medium', category: 'destructive-ops', description: 'possible mining indicators' },
235
- { regex: r(String.raw`^allowed-tools\s*:`), patternId: 'allowed_tools_field', severity: 'high', category: 'destructive-ops', description: 'skill declares allowed-tools (pre-approves access)' },
236
- { regex: r(String.raw`\bsudo\b`), patternId: 'sudo_usage', severity: 'high', category: 'destructive-ops', description: 'uses sudo (privilege escalation)' },
237
- { regex: r(String.raw`setuid|setgid|cap_setuid`), patternId: 'setuid_setgid', severity: 'critical', category: 'destructive-ops', description: 'setuid/setgid privilege escalation mechanism' },
238
- { regex: r(String.raw`NOPASSWD`), patternId: 'nopasswd_sudo', severity: 'critical', category: 'destructive-ops', description: 'NOPASSWD sudoers entry' },
239
- { regex: r(String.raw`chmod\s+[u+]?s`), patternId: 'suid_bit', severity: 'critical', category: 'destructive-ops', description: 'sets SUID/SGID bit on file' },
544
+ {
545
+ regex: r(String.raw`rm\s+-rf\s+/`),
546
+ patternId: 'destructive_root_rm',
547
+ severity: 'critical',
548
+ category: 'destructive-ops',
549
+ description: 'recursive delete from root',
550
+ },
551
+ {
552
+ regex: r(String.raw`rm\s+(-[^\s]*)?r.*\$HOME|\brmdir\s+.*\$HOME`),
553
+ patternId: 'destructive_home_rm',
554
+ severity: 'critical',
555
+ category: 'destructive-ops',
556
+ description: 'recursive delete targeting home directory',
557
+ },
558
+ {
559
+ regex: r(String.raw`chmod\s+777`),
560
+ patternId: 'insecure_perms',
561
+ severity: 'medium',
562
+ category: 'destructive-ops',
563
+ description: 'sets world-writable permissions',
564
+ },
565
+ {
566
+ regex: r(String.raw`>\s*/etc/`),
567
+ patternId: 'system_overwrite',
568
+ severity: 'critical',
569
+ category: 'destructive-ops',
570
+ description: 'overwrites system configuration file',
571
+ },
572
+ {
573
+ regex: r(String.raw`\bmkfs\b`),
574
+ patternId: 'format_filesystem',
575
+ severity: 'critical',
576
+ category: 'destructive-ops',
577
+ description: 'formats a filesystem',
578
+ },
579
+ {
580
+ regex: r(String.raw`\bdd\s+.*if=.*of=/dev/`),
581
+ patternId: 'disk_overwrite',
582
+ severity: 'critical',
583
+ category: 'destructive-ops',
584
+ description: 'raw disk write operation',
585
+ },
586
+ {
587
+ regex: r(String.raw`shutil\.rmtree\s*\(\s*["'/]`),
588
+ patternId: 'python_rmtree',
589
+ severity: 'high',
590
+ category: 'destructive-ops',
591
+ description: 'Python rmtree on absolute path',
592
+ },
593
+ {
594
+ regex: r(String.raw`truncate\s+-s\s*0\s+/`),
595
+ patternId: 'truncate_system',
596
+ severity: 'critical',
597
+ category: 'destructive-ops',
598
+ description: 'truncates system file to zero bytes',
599
+ },
600
+ {
601
+ regex: r(String.raw`subprocess\.(run|call|Popen|check_output)\s*\(`),
602
+ patternId: 'python_subprocess',
603
+ severity: 'medium',
604
+ category: 'destructive-ops',
605
+ description: 'Python subprocess execution',
606
+ },
607
+ {
608
+ regex: r(String.raw`os\.system\s*\(`),
609
+ patternId: 'python_os_system',
610
+ severity: 'high',
611
+ category: 'destructive-ops',
612
+ description: 'os.system() shell execution',
613
+ },
614
+ {
615
+ regex: r(String.raw`os\.popen\s*\(`),
616
+ patternId: 'python_os_popen',
617
+ severity: 'high',
618
+ category: 'destructive-ops',
619
+ description: 'os.popen() shell execution',
620
+ },
621
+ {
622
+ regex: r(String.raw`child_process\.(exec|spawn|fork)\s*\(`),
623
+ patternId: 'node_child_process',
624
+ severity: 'high',
625
+ category: 'destructive-ops',
626
+ description: 'Node.js child_process execution',
627
+ },
628
+ {
629
+ regex: r(String.raw`Runtime\.getRuntime\(\)\.exec\(`),
630
+ patternId: 'java_runtime_exec',
631
+ severity: 'high',
632
+ category: 'destructive-ops',
633
+ description: 'Java Runtime.exec() shell execution',
634
+ },
635
+ {
636
+ regex: r('\\`[^\\`]*\\$\\([^)]+\\)[^\\`]*\\`'),
637
+ patternId: 'backtick_subshell',
638
+ severity: 'medium',
639
+ category: 'destructive-ops',
640
+ description: 'backtick with command substitution',
641
+ },
642
+ {
643
+ regex: r(String.raw`\.\./\.\./\.\.`),
644
+ patternId: 'path_traversal_deep',
645
+ severity: 'high',
646
+ category: 'destructive-ops',
647
+ description: 'deep relative path traversal',
648
+ },
649
+ {
650
+ regex: r(String.raw`\.\./\.\.`),
651
+ patternId: 'path_traversal',
652
+ severity: 'medium',
653
+ category: 'destructive-ops',
654
+ description: 'relative path traversal',
655
+ },
656
+ {
657
+ regex: r(String.raw`/etc/passwd|/etc/shadow`),
658
+ patternId: 'system_passwd_access',
659
+ severity: 'critical',
660
+ category: 'destructive-ops',
661
+ description: 'references system password files',
662
+ },
663
+ {
664
+ regex: r(String.raw`/proc/self|/proc/\d+/`),
665
+ patternId: 'proc_access',
666
+ severity: 'high',
667
+ category: 'destructive-ops',
668
+ description: 'references /proc filesystem',
669
+ },
670
+ {
671
+ regex: r(String.raw`/dev/shm/`),
672
+ patternId: 'dev_shm',
673
+ severity: 'medium',
674
+ category: 'destructive-ops',
675
+ description: 'references shared memory staging area',
676
+ },
677
+ {
678
+ regex: r(String.raw`xmrig|stratum\+tcp|monero|coinhive|cryptonight`),
679
+ patternId: 'crypto_mining',
680
+ severity: 'critical',
681
+ category: 'destructive-ops',
682
+ description: 'cryptocurrency mining reference',
683
+ },
684
+ {
685
+ regex: r(String.raw`hashrate|nonce.*difficulty`),
686
+ patternId: 'mining_indicators',
687
+ severity: 'medium',
688
+ category: 'destructive-ops',
689
+ description: 'possible mining indicators',
690
+ },
691
+ {
692
+ regex: r(String.raw`^allowed-tools\s*:`),
693
+ patternId: 'allowed_tools_field',
694
+ severity: 'high',
695
+ category: 'destructive-ops',
696
+ description: 'skill declares allowed-tools (pre-approves access)',
697
+ },
698
+ {
699
+ regex: r(String.raw`\bsudo\b`),
700
+ patternId: 'sudo_usage',
701
+ severity: 'high',
702
+ category: 'destructive-ops',
703
+ description: 'uses sudo (privilege escalation)',
704
+ },
705
+ {
706
+ regex: r(String.raw`setuid|setgid|cap_setuid`),
707
+ patternId: 'setuid_setgid',
708
+ severity: 'critical',
709
+ category: 'destructive-ops',
710
+ description: 'setuid/setgid privilege escalation mechanism',
711
+ },
712
+ {
713
+ regex: r(String.raw`NOPASSWD`),
714
+ patternId: 'nopasswd_sudo',
715
+ severity: 'critical',
716
+ category: 'destructive-ops',
717
+ description: 'NOPASSWD sudoers entry',
718
+ },
719
+ {
720
+ regex: r(String.raw`chmod\s+[u+]?s`),
721
+ patternId: 'suid_bit',
722
+ severity: 'critical',
723
+ category: 'destructive-ops',
724
+ description: 'sets SUID/SGID bit on file',
725
+ },
240
726
 
241
727
  // persistence
242
- { regex: r(String.raw`\bcrontab\b`), patternId: 'persistence_cron', severity: 'medium', category: 'persistence', description: 'modifies cron jobs' },
243
- { regex: r(String.raw`\.(bashrc|zshrc|profile|bash_profile|bash_login|zprofile|zlogin)\b`), patternId: 'shell_rc_mod', severity: 'medium', category: 'persistence', description: 'references shell startup file' },
244
- { regex: r(String.raw`authorized_keys`), patternId: 'ssh_backdoor', severity: 'critical', category: 'persistence', description: 'modifies SSH authorized keys' },
245
- { regex: r(String.raw`ssh-keygen`), patternId: 'ssh_keygen', severity: 'medium', category: 'persistence', description: 'generates SSH keys' },
246
- { regex: r(String.raw`systemd.*\.service|systemctl\s+(enable|start)`), patternId: 'systemd_service', severity: 'medium', category: 'persistence', description: 'references or enables systemd service' },
247
- { regex: r(String.raw`/etc/init\.d/`), patternId: 'init_script', severity: 'medium', category: 'persistence', description: 'references init.d startup script' },
248
- { regex: r(String.raw`launchctl\s+load|LaunchAgents|LaunchDaemons`), patternId: 'macos_launchd', severity: 'medium', category: 'persistence', description: 'macOS launch agent/daemon persistence' },
249
- { regex: r(String.raw`/etc/sudoers|visudo`), patternId: 'sudoers_mod', severity: 'critical', category: 'persistence', description: 'modifies sudoers' },
250
- { regex: r(String.raw`git\s+config\s+--global\s+`), patternId: 'git_config_global', severity: 'medium', category: 'persistence', description: 'modifies global git configuration' },
251
- { regex: r(String.raw`AGENTS\.md|CLAUDE\.md|\.cursorrules|\.clinerules`), patternId: 'agent_config_mod', severity: 'critical', category: 'persistence', description: 'references agent config files (instruction persistence)' },
252
- { regex: r(String.raw`\.hermes/config\.yaml|\.hermes/SOUL\.md`), patternId: 'hermes_config_mod', severity: 'critical', category: 'persistence', description: 'references Hermes configuration files directly' },
253
- { regex: r(String.raw`\.claude/settings|\.codex/config`), patternId: 'other_agent_config', severity: 'high', category: 'persistence', description: 'references other agent configuration files' },
728
+ {
729
+ regex: r(String.raw`\bcrontab\b`),
730
+ patternId: 'persistence_cron',
731
+ severity: 'medium',
732
+ category: 'persistence',
733
+ description: 'modifies cron jobs',
734
+ },
735
+ {
736
+ regex: r(
737
+ String.raw`\.(bashrc|zshrc|profile|bash_profile|bash_login|zprofile|zlogin)\b`,
738
+ ),
739
+ patternId: 'shell_rc_mod',
740
+ severity: 'medium',
741
+ category: 'persistence',
742
+ description: 'references shell startup file',
743
+ },
744
+ {
745
+ regex: r(String.raw`authorized_keys`),
746
+ patternId: 'ssh_backdoor',
747
+ severity: 'critical',
748
+ category: 'persistence',
749
+ description: 'modifies SSH authorized keys',
750
+ },
751
+ {
752
+ regex: r(String.raw`ssh-keygen`),
753
+ patternId: 'ssh_keygen',
754
+ severity: 'medium',
755
+ category: 'persistence',
756
+ description: 'generates SSH keys',
757
+ },
758
+ {
759
+ regex: r(String.raw`systemd.*\.service|systemctl\s+(enable|start)`),
760
+ patternId: 'systemd_service',
761
+ severity: 'medium',
762
+ category: 'persistence',
763
+ description: 'references or enables systemd service',
764
+ },
765
+ {
766
+ regex: r(String.raw`/etc/init\.d/`),
767
+ patternId: 'init_script',
768
+ severity: 'medium',
769
+ category: 'persistence',
770
+ description: 'references init.d startup script',
771
+ },
772
+ {
773
+ regex: r(String.raw`launchctl\s+load|LaunchAgents|LaunchDaemons`),
774
+ patternId: 'macos_launchd',
775
+ severity: 'medium',
776
+ category: 'persistence',
777
+ description: 'macOS launch agent/daemon persistence',
778
+ },
779
+ {
780
+ regex: r(String.raw`/etc/sudoers|visudo`),
781
+ patternId: 'sudoers_mod',
782
+ severity: 'critical',
783
+ category: 'persistence',
784
+ description: 'modifies sudoers',
785
+ },
786
+ {
787
+ regex: r(String.raw`git\s+config\s+--global\s+`),
788
+ patternId: 'git_config_global',
789
+ severity: 'medium',
790
+ category: 'persistence',
791
+ description: 'modifies global git configuration',
792
+ },
793
+ {
794
+ regex: r(String.raw`AGENTS\.md|CLAUDE\.md|\.cursorrules|\.clinerules`),
795
+ patternId: 'agent_config_mod',
796
+ severity: 'critical',
797
+ category: 'persistence',
798
+ description: 'references agent config files (instruction persistence)',
799
+ },
800
+ {
801
+ regex: r(String.raw`\.hermes/config\.yaml|\.hermes/SOUL\.md`),
802
+ patternId: 'hermes_config_mod',
803
+ severity: 'critical',
804
+ category: 'persistence',
805
+ description: 'references Hermes configuration files directly',
806
+ },
807
+ {
808
+ regex: r(String.raw`\.claude/settings|\.codex/config`),
809
+ patternId: 'other_agent_config',
810
+ severity: 'high',
811
+ category: 'persistence',
812
+ description: 'references other agent configuration files',
813
+ },
254
814
 
255
815
  // reverse-shells
256
- { regex: r(String.raw`\bnc\s+-[lp]|ncat\s+-[lp]|\bsocat\b`), patternId: 'reverse_shell', severity: 'critical', category: 'reverse-shells', description: 'potential reverse shell listener' },
257
- { regex: r(String.raw`\bngrok\b|\blocaltunnel\b|\bserveo\b|\bcloudflared\b`), patternId: 'tunnel_service', severity: 'high', category: 'reverse-shells', description: 'uses tunneling service for external access' },
258
- { regex: r(String.raw`\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d{2,5}`), patternId: 'hardcoded_ip_port', severity: 'medium', category: 'reverse-shells', description: 'hardcoded IP address with port' },
259
- { regex: r(String.raw`0\.0\.0\.0:\d+|INADDR_ANY`), patternId: 'bind_all_interfaces', severity: 'high', category: 'reverse-shells', description: 'binds to all network interfaces' },
260
- { regex: r(String.raw`/bin/(ba)?sh\s+-i\s+.*>/dev/tcp/`), patternId: 'bash_reverse_shell', severity: 'critical', category: 'reverse-shells', description: 'bash reverse shell via /dev/tcp' },
261
- { regex: r(String.raw`python[23]?\s+-c\s+["']import\s+socket`), patternId: 'python_socket_oneliner', severity: 'critical', category: 'reverse-shells', description: 'Python one-liner socket connection (likely reverse shell)' },
262
- { regex: r(String.raw`socket\.connect\s*\(\s*\(`), patternId: 'python_socket_connect', severity: 'high', category: 'reverse-shells', description: 'Python socket connect to arbitrary host' },
263
- { regex: r(String.raw`webhook\.site|requestbin\.com|pipedream\.net|hookbin\.com`), patternId: 'exfil_service', severity: 'high', category: 'reverse-shells', description: 'references known webhook/exfiltration service' },
264
- { regex: r(String.raw`pastebin\.com|hastebin\.com|ghostbin\.`), patternId: 'paste_service', severity: 'medium', category: 'reverse-shells', description: 'references paste service (possible staging)' },
816
+ {
817
+ regex: r(String.raw`\bnc\s+-[lp]|ncat\s+-[lp]|\bsocat\b`),
818
+ patternId: 'reverse_shell',
819
+ severity: 'critical',
820
+ category: 'reverse-shells',
821
+ description: 'potential reverse shell listener',
822
+ },
823
+ {
824
+ regex: r(String.raw`\bngrok\b|\blocaltunnel\b|\bserveo\b|\bcloudflared\b`),
825
+ patternId: 'tunnel_service',
826
+ severity: 'high',
827
+ category: 'reverse-shells',
828
+ description: 'uses tunneling service for external access',
829
+ },
830
+ {
831
+ regex: r(String.raw`\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d{2,5}`),
832
+ patternId: 'hardcoded_ip_port',
833
+ severity: 'medium',
834
+ category: 'reverse-shells',
835
+ description: 'hardcoded IP address with port',
836
+ },
837
+ {
838
+ regex: r(String.raw`0\.0\.0\.0:\d+|INADDR_ANY`),
839
+ patternId: 'bind_all_interfaces',
840
+ severity: 'high',
841
+ category: 'reverse-shells',
842
+ description: 'binds to all network interfaces',
843
+ },
844
+ {
845
+ regex: r(String.raw`/bin/(ba)?sh\s+-i\s+.*>/dev/tcp/`),
846
+ patternId: 'bash_reverse_shell',
847
+ severity: 'critical',
848
+ category: 'reverse-shells',
849
+ description: 'bash reverse shell via /dev/tcp',
850
+ },
851
+ {
852
+ regex: r(String.raw`python[23]?\s+-c\s+["']import\s+socket`),
853
+ patternId: 'python_socket_oneliner',
854
+ severity: 'critical',
855
+ category: 'reverse-shells',
856
+ description: 'Python one-liner socket connection (likely reverse shell)',
857
+ },
858
+ {
859
+ regex: r(String.raw`socket\.connect\s*\(\s*\(`),
860
+ patternId: 'python_socket_connect',
861
+ severity: 'high',
862
+ category: 'reverse-shells',
863
+ description: 'Python socket connect to arbitrary host',
864
+ },
865
+ {
866
+ regex: r(
867
+ String.raw`webhook\.site|requestbin\.com|pipedream\.net|hookbin\.com`,
868
+ ),
869
+ patternId: 'exfil_service',
870
+ severity: 'high',
871
+ category: 'reverse-shells',
872
+ description: 'references known webhook/exfiltration service',
873
+ },
874
+ {
875
+ regex: r(String.raw`pastebin\.com|hastebin\.com|ghostbin\.`),
876
+ patternId: 'paste_service',
877
+ severity: 'medium',
878
+ category: 'reverse-shells',
879
+ description: 'references paste service (possible staging)',
880
+ },
265
881
 
266
882
  // obfuscation
267
- { regex: r(String.raw`base64\s+(-d|--decode)\s*\|`), patternId: 'base64_decode_pipe', severity: 'high', category: 'obfuscation', description: 'base64 decode piped to execution' },
268
- { regex: r(String.raw`\\x[0-9a-fA-F]{2}.*\\x[0-9a-fA-F]{2}.*\\x[0-9a-fA-F]{2}`), patternId: 'hex_encoded_string', severity: 'medium', category: 'obfuscation', description: 'hex-encoded string chain' },
269
- { regex: r(String.raw`\beval\s*\(\s*["']`), patternId: 'eval_string', severity: 'high', category: 'obfuscation', description: 'eval() with string argument' },
270
- { regex: r(String.raw`\bexec\s*\(\s*["']`), patternId: 'exec_string', severity: 'high', category: 'obfuscation', description: 'exec() with string argument' },
271
- { regex: r(String.raw`echo\s+[^\n]*\|\s*(bash|sh|python|perl|ruby|node)`), patternId: 'echo_pipe_exec', severity: 'critical', category: 'obfuscation', description: 'echo piped to interpreter for execution' },
272
- { regex: r(String.raw`compile\s*\(\s*[^\)]+,\s*["'].*["']\s*,\s*["']exec["']\s*\)`), patternId: 'python_compile_exec', severity: 'high', category: 'obfuscation', description: 'Python compile() with exec mode' },
273
- { regex: r(String.raw`getattr\s*\(\s*__builtins__`), patternId: 'python_getattr_builtins', severity: 'high', category: 'obfuscation', description: 'dynamic access to Python builtins' },
274
- { regex: r(String.raw`__import__\s*\(\s*["']os["']\s*\)`), patternId: 'python_import_os', severity: 'high', category: 'obfuscation', description: 'dynamic import of os module' },
275
- { regex: r(String.raw`codecs\.decode\s*\(\s*["']`), patternId: 'python_codecs_decode', severity: 'medium', category: 'obfuscation', description: 'codecs.decode (possible obfuscation)' },
276
- { regex: r(String.raw`String\.fromCharCode|charCodeAt`), patternId: 'js_char_code', severity: 'medium', category: 'obfuscation', description: 'JavaScript character code construction' },
277
- { regex: r(String.raw`atob\s*\(|btoa\s*\(`), patternId: 'js_base64', severity: 'medium', category: 'obfuscation', description: 'JavaScript base64 encode/decode' },
278
- { regex: r(String.raw`\[::-1\]`), patternId: 'string_reversal', severity: 'low', category: 'obfuscation', description: 'string reversal (possible obfuscation)' },
279
- { regex: r(String.raw`chr\s*\(\s*\d+\s*\)\s*\+\s*chr\s*\(\s*\d+`), patternId: 'chr_building', severity: 'high', category: 'obfuscation', description: 'building string from chr() calls' },
280
- { regex: r(String.raw`\\u[0-9a-fA-F]{4}.*\\u[0-9a-fA-F]{4}.*\\u[0-9a-fA-F]{4}`), patternId: 'unicode_escape_chain', severity: 'medium', category: 'obfuscation', description: 'chain of unicode escapes' },
883
+ {
884
+ regex: r(String.raw`base64\s+(-d|--decode)\s*\|`),
885
+ patternId: 'base64_decode_pipe',
886
+ severity: 'high',
887
+ category: 'obfuscation',
888
+ description: 'base64 decode piped to execution',
889
+ },
890
+ {
891
+ regex: r(
892
+ String.raw`\\x[0-9a-fA-F]{2}.*\\x[0-9a-fA-F]{2}.*\\x[0-9a-fA-F]{2}`,
893
+ ),
894
+ patternId: 'hex_encoded_string',
895
+ severity: 'medium',
896
+ category: 'obfuscation',
897
+ description: 'hex-encoded string chain',
898
+ },
899
+ {
900
+ regex: r(String.raw`\beval\s*\(\s*["']`),
901
+ patternId: 'eval_string',
902
+ severity: 'high',
903
+ category: 'obfuscation',
904
+ description: 'eval() with string argument',
905
+ },
906
+ {
907
+ regex: r(String.raw`\bexec\s*\(\s*["']`),
908
+ patternId: 'exec_string',
909
+ severity: 'high',
910
+ category: 'obfuscation',
911
+ description: 'exec() with string argument',
912
+ },
913
+ {
914
+ regex: r(String.raw`echo\s+[^\n]*\|\s*(bash|sh|python|perl|ruby|node)`),
915
+ patternId: 'echo_pipe_exec',
916
+ severity: 'critical',
917
+ category: 'obfuscation',
918
+ description: 'echo piped to interpreter for execution',
919
+ },
920
+ {
921
+ regex: r(
922
+ String.raw`compile\s*\(\s*[^\)]+,\s*["'].*["']\s*,\s*["']exec["']\s*\)`,
923
+ ),
924
+ patternId: 'python_compile_exec',
925
+ severity: 'high',
926
+ category: 'obfuscation',
927
+ description: 'Python compile() with exec mode',
928
+ },
929
+ {
930
+ regex: r(String.raw`getattr\s*\(\s*__builtins__`),
931
+ patternId: 'python_getattr_builtins',
932
+ severity: 'high',
933
+ category: 'obfuscation',
934
+ description: 'dynamic access to Python builtins',
935
+ },
936
+ {
937
+ regex: r(String.raw`__import__\s*\(\s*["']os["']\s*\)`),
938
+ patternId: 'python_import_os',
939
+ severity: 'high',
940
+ category: 'obfuscation',
941
+ description: 'dynamic import of os module',
942
+ },
943
+ {
944
+ regex: r(String.raw`codecs\.decode\s*\(\s*["']`),
945
+ patternId: 'python_codecs_decode',
946
+ severity: 'medium',
947
+ category: 'obfuscation',
948
+ description: 'codecs.decode (possible obfuscation)',
949
+ },
950
+ {
951
+ regex: r(String.raw`String\.fromCharCode|charCodeAt`),
952
+ patternId: 'js_char_code',
953
+ severity: 'medium',
954
+ category: 'obfuscation',
955
+ description: 'JavaScript character code construction',
956
+ },
957
+ {
958
+ regex: r(String.raw`atob\s*\(|btoa\s*\(`),
959
+ patternId: 'js_base64',
960
+ severity: 'medium',
961
+ category: 'obfuscation',
962
+ description: 'JavaScript base64 encode/decode',
963
+ },
964
+ {
965
+ regex: r(String.raw`\[::-1\]`),
966
+ patternId: 'string_reversal',
967
+ severity: 'low',
968
+ category: 'obfuscation',
969
+ description: 'string reversal (possible obfuscation)',
970
+ },
971
+ {
972
+ regex: r(String.raw`chr\s*\(\s*\d+\s*\)\s*\+\s*chr\s*\(\s*\d+`),
973
+ patternId: 'chr_building',
974
+ severity: 'high',
975
+ category: 'obfuscation',
976
+ description: 'building string from chr() calls',
977
+ },
978
+ {
979
+ regex: r(
980
+ String.raw`\\u[0-9a-fA-F]{4}.*\\u[0-9a-fA-F]{4}.*\\u[0-9a-fA-F]{4}`,
981
+ ),
982
+ patternId: 'unicode_escape_chain',
983
+ severity: 'medium',
984
+ category: 'obfuscation',
985
+ description: 'chain of unicode escapes',
986
+ },
281
987
 
282
988
  // supply-chain
283
- { regex: r(String.raw`curl\s+[^\n]*\|\s*(ba)?sh`), patternId: 'curl_pipe_shell', severity: 'critical', category: 'supply-chain', description: 'curl piped to shell (download-and-execute)' },
284
- { regex: r(String.raw`wget\s+[^\n]*-O\s*-\s*\|\s*(ba)?sh`), patternId: 'wget_pipe_shell', severity: 'critical', category: 'supply-chain', description: 'wget piped to shell (download-and-execute)' },
285
- { regex: r(String.raw`curl\s+[^\n]*\|\s*python`), patternId: 'curl_pipe_python', severity: 'critical', category: 'supply-chain', description: 'curl piped to Python interpreter' },
286
- { regex: r(String.raw`#\s*///\s*script.*dependencies`), patternId: 'pep723_inline_deps', severity: 'medium', category: 'supply-chain', description: 'PEP 723 inline script dependencies (verify pinning)' },
287
- { regex: r(String.raw`pip\s+install\s+(?!-r\s)(?!.*==)`), patternId: 'unpinned_pip_install', severity: 'medium', category: 'supply-chain', description: 'pip install without version pinning' },
288
- { regex: r(String.raw`npm\s+install\s+(?!.*@\d)`), patternId: 'unpinned_npm_install', severity: 'medium', category: 'supply-chain', description: 'npm install without version pinning' },
289
- { regex: r(String.raw`uv\s+run\s+`), patternId: 'uv_run', severity: 'medium', category: 'supply-chain', description: 'uv run may auto-install unpinned dependencies' },
290
- { regex: r(String.raw`(curl|wget|httpx?\.get|requests\.get|fetch)\s*[\(]?\s*["']https?://`), patternId: 'remote_fetch', severity: 'medium', category: 'supply-chain', description: 'fetches remote resource at runtime' },
291
- { regex: r(String.raw`git\s+clone\s+`), patternId: 'git_clone', severity: 'medium', category: 'supply-chain', description: 'clones git repository at runtime' },
292
- { regex: r(String.raw`docker\s+pull\s+`), patternId: 'docker_pull', severity: 'medium', category: 'supply-chain', description: 'pulls Docker image at runtime' },
989
+ {
990
+ regex: r(String.raw`curl\s+[^\n]*\|\s*(ba)?sh`),
991
+ patternId: 'curl_pipe_shell',
992
+ severity: 'critical',
993
+ category: 'supply-chain',
994
+ description: 'curl piped to shell (download-and-execute)',
995
+ },
996
+ {
997
+ regex: r(String.raw`wget\s+[^\n]*-O\s*-\s*\|\s*(ba)?sh`),
998
+ patternId: 'wget_pipe_shell',
999
+ severity: 'critical',
1000
+ category: 'supply-chain',
1001
+ description: 'wget piped to shell (download-and-execute)',
1002
+ },
1003
+ {
1004
+ regex: r(String.raw`curl\s+[^\n]*\|\s*python`),
1005
+ patternId: 'curl_pipe_python',
1006
+ severity: 'critical',
1007
+ category: 'supply-chain',
1008
+ description: 'curl piped to Python interpreter',
1009
+ },
1010
+ {
1011
+ regex: r(String.raw`#\s*///\s*script.*dependencies`),
1012
+ patternId: 'pep723_inline_deps',
1013
+ severity: 'medium',
1014
+ category: 'supply-chain',
1015
+ description: 'PEP 723 inline script dependencies (verify pinning)',
1016
+ },
1017
+ {
1018
+ regex: r(String.raw`pip\s+install\s+(?!-r\s)(?!.*==)`),
1019
+ patternId: 'unpinned_pip_install',
1020
+ severity: 'medium',
1021
+ category: 'supply-chain',
1022
+ description: 'pip install without version pinning',
1023
+ },
1024
+ {
1025
+ regex: r(String.raw`npm\s+install\s+(?!.*@\d)`),
1026
+ patternId: 'unpinned_npm_install',
1027
+ severity: 'medium',
1028
+ category: 'supply-chain',
1029
+ description: 'npm install without version pinning',
1030
+ },
1031
+ {
1032
+ regex: r(String.raw`uv\s+run\s+`),
1033
+ patternId: 'uv_run',
1034
+ severity: 'medium',
1035
+ category: 'supply-chain',
1036
+ description: 'uv run may auto-install unpinned dependencies',
1037
+ },
1038
+ {
1039
+ regex: r(
1040
+ String.raw`(curl|wget|httpx?\.get|requests\.get|fetch)\s*[\(]?\s*["']https?://`,
1041
+ ),
1042
+ patternId: 'remote_fetch',
1043
+ severity: 'medium',
1044
+ category: 'supply-chain',
1045
+ description: 'fetches remote resource at runtime',
1046
+ },
1047
+ {
1048
+ regex: r(String.raw`git\s+clone\s+`),
1049
+ patternId: 'git_clone',
1050
+ severity: 'medium',
1051
+ category: 'supply-chain',
1052
+ description: 'clones git repository at runtime',
1053
+ },
1054
+ {
1055
+ regex: r(String.raw`docker\s+pull\s+`),
1056
+ patternId: 'docker_pull',
1057
+ severity: 'medium',
1058
+ category: 'supply-chain',
1059
+ description: 'pulls Docker image at runtime',
1060
+ },
293
1061
 
294
1062
  // credential-exposure
295
- { regex: r(String.raw`(?:api[_-]?key|token|secret|password)\s*[=:]\s*["'][A-Za-z0-9+/=_-]{20,}`), patternId: 'hardcoded_secret', severity: 'critical', category: 'credential-exposure', description: 'possible hardcoded API key/token/secret' },
296
- { regex: r(String.raw`-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----`), patternId: 'embedded_private_key', severity: 'critical', category: 'credential-exposure', description: 'embedded private key' },
297
- { regex: r(String.raw`ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{80,}`), patternId: 'github_token_leaked', severity: 'critical', category: 'credential-exposure', description: 'GitHub personal access token in skill content' },
298
- { regex: r(String.raw`sk-[A-Za-z0-9]{20,}`), patternId: 'openai_key_leaked', severity: 'critical', category: 'credential-exposure', description: 'possible OpenAI API key in skill content' },
299
- { regex: r(String.raw`sk-ant-[A-Za-z0-9_-]{90,}`), patternId: 'anthropic_key_leaked', severity: 'critical', category: 'credential-exposure', description: 'possible Anthropic API key in skill content' },
300
- { regex: r(String.raw`AKIA[0-9A-Z]{16}`), patternId: 'aws_access_key_leaked', severity: 'critical', category: 'credential-exposure', description: 'AWS access key ID in skill content' },
1063
+ {
1064
+ regex: r(
1065
+ String.raw`(?:api[_-]?key|token|secret|password)\s*[=:]\s*["'][A-Za-z0-9+/=_-]{20,}`,
1066
+ ),
1067
+ patternId: 'hardcoded_secret',
1068
+ severity: 'critical',
1069
+ category: 'credential-exposure',
1070
+ description: 'possible hardcoded API key/token/secret',
1071
+ },
1072
+ {
1073
+ regex: r(String.raw`-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----`),
1074
+ patternId: 'embedded_private_key',
1075
+ severity: 'critical',
1076
+ category: 'credential-exposure',
1077
+ description: 'embedded private key',
1078
+ },
1079
+ {
1080
+ regex: r(String.raw`ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{80,}`),
1081
+ patternId: 'github_token_leaked',
1082
+ severity: 'critical',
1083
+ category: 'credential-exposure',
1084
+ description: 'GitHub personal access token in skill content',
1085
+ },
1086
+ {
1087
+ regex: r(String.raw`sk-[A-Za-z0-9]{20,}`),
1088
+ patternId: 'openai_key_leaked',
1089
+ severity: 'critical',
1090
+ category: 'credential-exposure',
1091
+ description: 'possible OpenAI API key in skill content',
1092
+ },
1093
+ {
1094
+ regex: r(String.raw`sk-ant-[A-Za-z0-9_-]{90,}`),
1095
+ patternId: 'anthropic_key_leaked',
1096
+ severity: 'critical',
1097
+ category: 'credential-exposure',
1098
+ description: 'possible Anthropic API key in skill content',
1099
+ },
1100
+ {
1101
+ regex: r(String.raw`AKIA[0-9A-Z]{16}`),
1102
+ patternId: 'aws_access_key_leaked',
1103
+ severity: 'critical',
1104
+ category: 'credential-exposure',
1105
+ description: 'AWS access key ID in skill content',
1106
+ },
301
1107
  ];
302
1108
 
303
1109
  function pathWithin(root: string, target: string): boolean {
@@ -380,38 +1186,46 @@ function collectStructure(skillPath: string): StructureScanState {
380
1186
  } catch {
381
1187
  resolved = null;
382
1188
  }
383
- state.signatureParts.push(`L:${relativePath}:${Math.trunc(stat.mtimeMs)}:${resolved || 'BROKEN'}`);
1189
+ state.signatureParts.push(
1190
+ `L:${relativePath}:${Math.trunc(stat.mtimeMs)}:${resolved || 'BROKEN'}`,
1191
+ );
384
1192
 
385
1193
  if (!resolved) {
386
- state.findings.push(createFinding({
387
- patternId: 'broken_symlink',
388
- severity: 'medium',
389
- category: 'structural',
390
- file: relativePath,
391
- line: 0,
392
- match: 'broken symlink',
393
- description: 'broken or circular symlink',
394
- }));
1194
+ state.findings.push(
1195
+ createFinding({
1196
+ patternId: 'broken_symlink',
1197
+ severity: 'medium',
1198
+ category: 'structural',
1199
+ file: relativePath,
1200
+ line: 0,
1201
+ match: 'broken symlink',
1202
+ description: 'broken or circular symlink',
1203
+ }),
1204
+ );
395
1205
  continue;
396
1206
  }
397
1207
 
398
1208
  if (!pathWithin(rootReal, resolved)) {
399
- state.findings.push(createFinding({
400
- patternId: 'symlink_escape',
401
- severity: 'critical',
402
- category: 'structural',
403
- file: relativePath,
404
- line: 0,
405
- match: `symlink -> ${resolved}`,
406
- description: 'symlink points outside the skill directory',
407
- }));
1209
+ state.findings.push(
1210
+ createFinding({
1211
+ patternId: 'symlink_escape',
1212
+ severity: 'critical',
1213
+ category: 'structural',
1214
+ file: relativePath,
1215
+ line: 0,
1216
+ match: `symlink -> ${resolved}`,
1217
+ description: 'symlink points outside the skill directory',
1218
+ }),
1219
+ );
408
1220
  }
409
1221
  continue;
410
1222
  }
411
1223
 
412
1224
  if (stat.isDirectory()) {
413
1225
  const resolvedDir = safeRealPath(absolutePath);
414
- state.signatureParts.push(`D:${relativePath}:${Math.trunc(stat.mtimeMs)}`);
1226
+ state.signatureParts.push(
1227
+ `D:${relativePath}:${Math.trunc(stat.mtimeMs)}`,
1228
+ );
415
1229
  if (!visitedDirs.has(resolvedDir)) {
416
1230
  visitedDirs.add(resolvedDir);
417
1231
  pendingDirs.push(absolutePath);
@@ -441,65 +1255,78 @@ function collectStructure(skillPath: string): StructureScanState {
441
1255
  });
442
1256
 
443
1257
  if (stat.size > MAX_SINGLE_FILE_BYTES) {
444
- state.findings.push(createFinding({
445
- patternId: 'oversized_file',
446
- severity: 'medium',
447
- category: 'structural',
448
- file: relativePath,
449
- line: 0,
450
- match: `${Math.trunc(stat.size / 1024)}KB`,
451
- description: `file is ${Math.trunc(stat.size / 1024)}KB (limit: ${Math.trunc(MAX_SINGLE_FILE_BYTES / 1024)}KB)`,
452
- }));
1258
+ state.findings.push(
1259
+ createFinding({
1260
+ patternId: 'oversized_file',
1261
+ severity: 'medium',
1262
+ category: 'structural',
1263
+ file: relativePath,
1264
+ line: 0,
1265
+ match: `${Math.trunc(stat.size / 1024)}KB`,
1266
+ description: `file is ${Math.trunc(stat.size / 1024)}KB (limit: ${Math.trunc(MAX_SINGLE_FILE_BYTES / 1024)}KB)`,
1267
+ }),
1268
+ );
453
1269
  }
454
1270
 
455
1271
  if (SUSPICIOUS_BINARY_EXTENSIONS.has(ext) || isBinary) {
456
- state.findings.push(createFinding({
457
- patternId: 'binary_file',
458
- severity: 'critical',
459
- category: 'structural',
460
- file: relativePath,
461
- line: 0,
462
- match: isBinary ? `binary content${ext ? ` (${ext})` : ''}` : `binary extension: ${ext}`,
463
- description: 'binary/executable content should not be in a skill',
464
- }));
1272
+ state.findings.push(
1273
+ createFinding({
1274
+ patternId: 'binary_file',
1275
+ severity: 'critical',
1276
+ category: 'structural',
1277
+ file: relativePath,
1278
+ line: 0,
1279
+ match: isBinary
1280
+ ? `binary content${ext ? ` (${ext})` : ''}`
1281
+ : `binary extension: ${ext}`,
1282
+ description: 'binary/executable content should not be in a skill',
1283
+ }),
1284
+ );
465
1285
  }
466
1286
 
467
1287
  if (!SCRIPT_EXEC_EXTENSIONS.has(ext) && (stat.mode & 0o111) !== 0) {
468
- state.findings.push(createFinding({
469
- patternId: 'unexpected_executable',
470
- severity: 'medium',
471
- category: 'structural',
472
- file: relativePath,
473
- line: 0,
474
- match: 'executable bit set',
475
- description: 'file has executable permission but is not a recognized script type',
476
- }));
1288
+ state.findings.push(
1289
+ createFinding({
1290
+ patternId: 'unexpected_executable',
1291
+ severity: 'medium',
1292
+ category: 'structural',
1293
+ file: relativePath,
1294
+ line: 0,
1295
+ match: 'executable bit set',
1296
+ description:
1297
+ 'file has executable permission but is not a recognized script type',
1298
+ }),
1299
+ );
477
1300
  }
478
1301
  }
479
1302
  }
480
1303
 
481
1304
  if (state.fileCount > MAX_FILE_COUNT) {
482
- state.findings.push(createFinding({
483
- patternId: 'too_many_files',
484
- severity: 'medium',
485
- category: 'structural',
486
- file: '(directory)',
487
- line: 0,
488
- match: `${state.fileCount} files`,
489
- description: `skill has ${state.fileCount} files (limit: ${MAX_FILE_COUNT})`,
490
- }));
1305
+ state.findings.push(
1306
+ createFinding({
1307
+ patternId: 'too_many_files',
1308
+ severity: 'medium',
1309
+ category: 'structural',
1310
+ file: '(directory)',
1311
+ line: 0,
1312
+ match: `${state.fileCount} files`,
1313
+ description: `skill has ${state.fileCount} files (limit: ${MAX_FILE_COUNT})`,
1314
+ }),
1315
+ );
491
1316
  }
492
1317
 
493
1318
  if (state.totalSize > MAX_TOTAL_SIZE_BYTES) {
494
- state.findings.push(createFinding({
495
- patternId: 'oversized_skill',
496
- severity: 'high',
497
- category: 'structural',
498
- file: '(directory)',
499
- line: 0,
500
- match: `${Math.trunc(state.totalSize / 1024)}KB total`,
501
- description: `skill is ${Math.trunc(state.totalSize / 1024)}KB total (limit: ${Math.trunc(MAX_TOTAL_SIZE_BYTES / 1024)}KB)`,
502
- }));
1319
+ state.findings.push(
1320
+ createFinding({
1321
+ patternId: 'oversized_skill',
1322
+ severity: 'high',
1323
+ category: 'structural',
1324
+ file: '(directory)',
1325
+ line: 0,
1326
+ match: `${Math.trunc(state.totalSize / 1024)}KB total`,
1327
+ description: `skill is ${Math.trunc(state.totalSize / 1024)}KB total (limit: ${Math.trunc(MAX_TOTAL_SIZE_BYTES / 1024)}KB)`,
1328
+ }),
1329
+ );
503
1330
  }
504
1331
 
505
1332
  return state;
@@ -507,7 +1334,11 @@ function collectStructure(skillPath: string): StructureScanState {
507
1334
 
508
1335
  function scanFile(entry: SkillFileEntry): SkillGuardFinding[] {
509
1336
  if (entry.isBinary) return [];
510
- if (entry.extension !== '.md' && entry.relativePath !== 'SKILL.md' && !SCANNABLE_EXTENSIONS.has(entry.extension)) {
1337
+ if (
1338
+ entry.extension !== '.md' &&
1339
+ entry.relativePath !== 'SKILL.md' &&
1340
+ !SCANNABLE_EXTENSIONS.has(entry.extension)
1341
+ ) {
511
1342
  return [];
512
1343
  }
513
1344
 
@@ -531,15 +1362,17 @@ function scanFile(entry: SkillFileEntry): SkillGuardFinding[] {
531
1362
  if (!rule.regex.test(line)) continue;
532
1363
  seen.add(dedupeKey);
533
1364
  const matched = line.trim();
534
- findings.push(createFinding({
535
- patternId: rule.patternId,
536
- severity: rule.severity,
537
- category: rule.category,
538
- file: entry.relativePath,
539
- line: lineNo,
540
- match: matched.length > 120 ? `${matched.slice(0, 117)}...` : matched,
541
- description: rule.description,
542
- }));
1365
+ findings.push(
1366
+ createFinding({
1367
+ patternId: rule.patternId,
1368
+ severity: rule.severity,
1369
+ category: rule.category,
1370
+ file: entry.relativePath,
1371
+ line: lineNo,
1372
+ match: matched.length > 120 ? `${matched.slice(0, 117)}...` : matched,
1373
+ description: rule.description,
1374
+ }),
1375
+ );
543
1376
  }
544
1377
  }
545
1378
 
@@ -548,16 +1381,20 @@ function scanFile(entry: SkillFileEntry): SkillGuardFinding[] {
548
1381
  const line = lines[i] || '';
549
1382
  for (const char of INVISIBLE_CHARS) {
550
1383
  if (!line.includes(char)) continue;
551
- const charName = INVISIBLE_CHAR_NAMES[char] || `U+${char.codePointAt(0)?.toString(16).toUpperCase()}`;
552
- findings.push(createFinding({
553
- patternId: 'invisible_unicode',
554
- severity: 'high',
555
- category: 'prompt-injection',
556
- file: entry.relativePath,
557
- line: lineNo,
558
- match: `U+${(char.codePointAt(0) || 0).toString(16).toUpperCase().padStart(4, '0')} (${charName})`,
559
- description: `invisible unicode character ${charName} (possible text hiding/injection)`,
560
- }));
1384
+ const charName =
1385
+ INVISIBLE_CHAR_NAMES[char] ||
1386
+ `U+${char.codePointAt(0)?.toString(16).toUpperCase()}`;
1387
+ findings.push(
1388
+ createFinding({
1389
+ patternId: 'invisible_unicode',
1390
+ severity: 'high',
1391
+ category: 'prompt-injection',
1392
+ file: entry.relativePath,
1393
+ line: lineNo,
1394
+ match: `U+${(char.codePointAt(0) || 0).toString(16).toUpperCase().padStart(4, '0')} (${charName})`,
1395
+ description: `invisible unicode character ${charName} (possible text hiding/injection)`,
1396
+ }),
1397
+ );
561
1398
  break;
562
1399
  }
563
1400
  }
@@ -567,7 +1404,8 @@ function scanFile(entry: SkillFileEntry): SkillGuardFinding[] {
567
1404
 
568
1405
  function determineVerdict(findings: SkillGuardFinding[]): SkillGuardVerdict {
569
1406
  if (findings.length === 0) return 'safe';
570
- if (findings.some((finding) => finding.severity === 'critical')) return 'dangerous';
1407
+ if (findings.some((finding) => finding.severity === 'critical'))
1408
+ return 'dangerous';
571
1409
  return 'caution';
572
1410
  }
573
1411
 
@@ -579,7 +1417,9 @@ function buildSummary(params: {
579
1417
  if (params.findings.length === 0) {
580
1418
  return `${params.skillName}: clean scan, no threats detected`;
581
1419
  }
582
- const categories = Array.from(new Set(params.findings.map((finding) => finding.category))).sort();
1420
+ const categories = Array.from(
1421
+ new Set(params.findings.map((finding) => finding.category)),
1422
+ ).sort();
583
1423
  return `${params.skillName}: ${params.verdict} — ${params.findings.length} finding(s) in ${categories.join(', ')}`;
584
1424
  }
585
1425
 
@@ -591,7 +1431,9 @@ function computeMtimeSignature(parts: string[]): string {
591
1431
 
592
1432
  function computeContentHash(files: SkillFileEntry[]): string {
593
1433
  const hash = createHash('sha256');
594
- const sortedFiles = files.slice().sort((a, b) => a.relativePath.localeCompare(b.relativePath));
1434
+ const sortedFiles = files
1435
+ .slice()
1436
+ .sort((a, b) => a.relativePath.localeCompare(b.relativePath));
595
1437
  for (const file of sortedFiles) {
596
1438
  hash.update(file.relativePath).update('\0');
597
1439
  hash.update(String(file.size)).update('\0');
@@ -605,10 +1447,13 @@ function computeContentHash(files: SkillFileEntry[]): string {
605
1447
  return hash.digest('hex');
606
1448
  }
607
1449
 
608
- export function resolveSkillTrustLevel(sourceTag: string): SkillGuardTrustLevel {
1450
+ export function resolveSkillTrustLevel(
1451
+ sourceTag: string,
1452
+ ): SkillGuardTrustLevel {
609
1453
  const normalized = sourceTag.trim().toLowerCase();
610
1454
  if (normalized === 'bundled') return 'builtin';
611
- if (normalized === 'workspace' || normalized === 'agents-project') return 'workspace';
1455
+ if (normalized === 'workspace' || normalized === 'agents-project')
1456
+ return 'workspace';
612
1457
  if (
613
1458
  normalized === 'codex' ||
614
1459
  normalized === 'claude' ||
@@ -682,7 +1527,10 @@ function scanSkillWithCache(params: {
682
1527
  return result;
683
1528
  }
684
1529
 
685
- function shouldAllowByPolicy(result: SkillGuardScanResult): { allowed: boolean; reason: string } {
1530
+ function shouldAllowByPolicy(result: SkillGuardScanResult): {
1531
+ allowed: boolean;
1532
+ reason: string;
1533
+ } {
686
1534
  const decision = INSTALL_POLICY[result.trustLevel][result.verdict];
687
1535
  if (decision === 'allow') {
688
1536
  return {