@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.
- package/.github/workflows/ci.yml +70 -0
- package/.husky/pre-commit +1 -0
- package/CHANGELOG.md +85 -0
- package/CONTRIBUTING.md +33 -0
- package/README.md +41 -16
- package/SECURITY.md +17 -0
- package/biome.json +35 -0
- package/config.example.json +71 -8
- package/container/package-lock.json +2 -2
- package/container/package.json +1 -1
- package/container/src/approval-policy.ts +1303 -0
- package/container/src/browser-tools.ts +431 -136
- package/container/src/extensions.ts +36 -12
- package/container/src/hybridai-client.ts +34 -13
- package/container/src/index.ts +451 -109
- package/container/src/ipc.ts +5 -3
- package/container/src/token-usage.ts +20 -10
- package/container/src/tools.ts +599 -225
- package/container/src/types.ts +32 -2
- package/container/src/web-fetch.ts +89 -32
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +10 -2
- package/dist/agent.js.map +1 -1
- package/dist/audit-cli.d.ts.map +1 -1
- package/dist/audit-cli.js +4 -2
- package/dist/audit-cli.js.map +1 -1
- package/dist/audit-events.d.ts.map +1 -1
- package/dist/audit-events.js +53 -3
- package/dist/audit-events.js.map +1 -1
- package/dist/audit-trail.d.ts.map +1 -1
- package/dist/audit-trail.js +17 -8
- package/dist/audit-trail.js.map +1 -1
- package/dist/channels/discord/attachments.d.ts.map +1 -1
- package/dist/channels/discord/attachments.js +14 -7
- package/dist/channels/discord/attachments.js.map +1 -1
- package/dist/channels/discord/debounce.d.ts +9 -0
- package/dist/channels/discord/debounce.d.ts.map +1 -0
- package/dist/channels/discord/debounce.js +20 -0
- package/dist/channels/discord/debounce.js.map +1 -0
- package/dist/channels/discord/delivery.d.ts +4 -1
- package/dist/channels/discord/delivery.d.ts.map +1 -1
- package/dist/channels/discord/delivery.js +19 -3
- package/dist/channels/discord/delivery.js.map +1 -1
- package/dist/channels/discord/human-delay.d.ts +16 -0
- package/dist/channels/discord/human-delay.d.ts.map +1 -0
- package/dist/channels/discord/human-delay.js +29 -0
- package/dist/channels/discord/human-delay.js.map +1 -0
- package/dist/channels/discord/inbound.d.ts +4 -0
- package/dist/channels/discord/inbound.d.ts.map +1 -1
- package/dist/channels/discord/inbound.js +45 -4
- package/dist/channels/discord/inbound.js.map +1 -1
- package/dist/channels/discord/mentions.d.ts.map +1 -1
- package/dist/channels/discord/mentions.js +16 -4
- package/dist/channels/discord/mentions.js.map +1 -1
- package/dist/channels/discord/presence.d.ts +33 -0
- package/dist/channels/discord/presence.d.ts.map +1 -0
- package/dist/channels/discord/presence.js +111 -0
- package/dist/channels/discord/presence.js.map +1 -0
- package/dist/channels/discord/rate-limiter.d.ts +14 -0
- package/dist/channels/discord/rate-limiter.d.ts.map +1 -0
- package/dist/channels/discord/rate-limiter.js +49 -0
- package/dist/channels/discord/rate-limiter.js.map +1 -0
- package/dist/channels/discord/reactions.d.ts +38 -0
- package/dist/channels/discord/reactions.d.ts.map +1 -0
- package/dist/channels/discord/reactions.js +151 -0
- package/dist/channels/discord/reactions.js.map +1 -0
- package/dist/channels/discord/runtime.d.ts +6 -3
- package/dist/channels/discord/runtime.d.ts.map +1 -1
- package/dist/channels/discord/runtime.js +621 -125
- package/dist/channels/discord/runtime.js.map +1 -1
- package/dist/channels/discord/stream.d.ts +4 -1
- package/dist/channels/discord/stream.d.ts.map +1 -1
- package/dist/channels/discord/stream.js +16 -8
- package/dist/channels/discord/stream.js.map +1 -1
- package/dist/channels/discord/tool-actions.d.ts.map +1 -1
- package/dist/channels/discord/tool-actions.js +24 -12
- package/dist/channels/discord/tool-actions.js.map +1 -1
- package/dist/channels/discord/typing.d.ts +15 -0
- package/dist/channels/discord/typing.d.ts.map +1 -0
- package/dist/channels/discord/typing.js +106 -0
- package/dist/channels/discord/typing.js.map +1 -0
- package/dist/chunk.d.ts.map +1 -1
- package/dist/chunk.js +4 -2
- package/dist/chunk.js.map +1 -1
- package/dist/cli.js +47 -22
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +103 -18
- package/dist/config.js.map +1 -1
- package/dist/container-runner.d.ts.map +1 -1
- package/dist/container-runner.js +58 -26
- package/dist/container-runner.js.map +1 -1
- package/dist/container-setup.d.ts.map +1 -1
- package/dist/container-setup.js +10 -9
- package/dist/container-setup.js.map +1 -1
- package/dist/conversation.d.ts +2 -2
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +1 -1
- package/dist/conversation.js.map +1 -1
- package/dist/db.d.ts +118 -2
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +1568 -50
- package/dist/db.js.map +1 -1
- package/dist/delegation-manager.d.ts.map +1 -1
- package/dist/delegation-manager.js +3 -2
- package/dist/delegation-manager.js.map +1 -1
- package/dist/gateway-client.d.ts +2 -2
- package/dist/gateway-client.d.ts.map +1 -1
- package/dist/gateway-client.js +10 -4
- package/dist/gateway-client.js.map +1 -1
- package/dist/gateway-service.d.ts +3 -3
- package/dist/gateway-service.d.ts.map +1 -1
- package/dist/gateway-service.js +563 -73
- package/dist/gateway-service.js.map +1 -1
- package/dist/gateway-types.d.ts +24 -0
- package/dist/gateway-types.d.ts.map +1 -1
- package/dist/gateway-types.js.map +1 -1
- package/dist/gateway.js +179 -24
- package/dist/gateway.js.map +1 -1
- package/dist/health.d.ts.map +1 -1
- package/dist/health.js +20 -10
- package/dist/health.js.map +1 -1
- package/dist/heartbeat.d.ts +4 -0
- package/dist/heartbeat.d.ts.map +1 -1
- package/dist/heartbeat.js +48 -20
- package/dist/heartbeat.js.map +1 -1
- package/dist/hybridai-bots.d.ts.map +1 -1
- package/dist/hybridai-bots.js +4 -2
- package/dist/hybridai-bots.js.map +1 -1
- package/dist/instruction-approval-audit.d.ts.map +1 -1
- package/dist/instruction-approval-audit.js.map +1 -1
- package/dist/instruction-integrity.d.ts.map +1 -1
- package/dist/instruction-integrity.js +8 -2
- package/dist/instruction-integrity.js.map +1 -1
- package/dist/ipc.d.ts.map +1 -1
- package/dist/ipc.js +6 -1
- package/dist/ipc.js.map +1 -1
- package/dist/logger.js.map +1 -1
- package/dist/memory-consolidation.d.ts +17 -0
- package/dist/memory-consolidation.d.ts.map +1 -0
- package/dist/memory-consolidation.js +25 -0
- package/dist/memory-consolidation.js.map +1 -0
- package/dist/memory-service.d.ts +200 -0
- package/dist/memory-service.d.ts.map +1 -0
- package/dist/memory-service.js +294 -0
- package/dist/memory-service.js.map +1 -0
- package/dist/mount-security.d.ts.map +1 -1
- package/dist/mount-security.js +31 -7
- package/dist/mount-security.js.map +1 -1
- package/dist/observability-ingest.d.ts.map +1 -1
- package/dist/observability-ingest.js +32 -11
- package/dist/observability-ingest.js.map +1 -1
- package/dist/onboarding.d.ts.map +1 -1
- package/dist/onboarding.js +32 -9
- package/dist/onboarding.js.map +1 -1
- package/dist/proactive-policy.d.ts.map +1 -1
- package/dist/proactive-policy.js +2 -1
- package/dist/proactive-policy.js.map +1 -1
- package/dist/prompt-hooks.d.ts.map +1 -1
- package/dist/prompt-hooks.js +9 -7
- package/dist/prompt-hooks.js.map +1 -1
- package/dist/runtime-config.d.ts +98 -1
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +477 -23
- package/dist/runtime-config.js.map +1 -1
- package/dist/scheduled-task-runner.d.ts +1 -0
- package/dist/scheduled-task-runner.d.ts.map +1 -1
- package/dist/scheduled-task-runner.js +29 -10
- package/dist/scheduled-task-runner.js.map +1 -1
- package/dist/scheduler.d.ts +43 -4
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +530 -56
- package/dist/scheduler.js.map +1 -1
- package/dist/session-export.d.ts +26 -0
- package/dist/session-export.d.ts.map +1 -0
- package/dist/session-export.js +149 -0
- package/dist/session-export.js.map +1 -0
- package/dist/session-maintenance.d.ts.map +1 -1
- package/dist/session-maintenance.js +75 -13
- package/dist/session-maintenance.js.map +1 -1
- package/dist/session-transcripts.d.ts.map +1 -1
- package/dist/session-transcripts.js.map +1 -1
- package/dist/side-effects.d.ts.map +1 -1
- package/dist/side-effects.js +14 -2
- package/dist/side-effects.js.map +1 -1
- package/dist/skills-guard.d.ts.map +1 -1
- package/dist/skills-guard.js +893 -130
- package/dist/skills-guard.js.map +1 -1
- package/dist/skills.d.ts +5 -0
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +29 -15
- package/dist/skills.js.map +1 -1
- package/dist/token-efficiency.d.ts.map +1 -1
- package/dist/token-efficiency.js.map +1 -1
- package/dist/tui.js +92 -11
- package/dist/tui.js.map +1 -1
- package/dist/types.d.ts +146 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +24 -1
- package/dist/types.js.map +1 -1
- package/dist/update.d.ts.map +1 -1
- package/dist/update.js +42 -14
- package/dist/update.js.map +1 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +49 -9
- package/dist/workspace.js.map +1 -1
- package/docs/chat.html +9 -3
- package/docs/index.html +37 -13
- package/package.json +8 -2
- package/src/agent.ts +16 -3
- package/src/audit-cli.ts +44 -16
- package/src/audit-events.ts +69 -5
- package/src/audit-trail.ts +41 -15
- package/src/channels/discord/attachments.ts +81 -27
- package/src/channels/discord/debounce.ts +25 -0
- package/src/channels/discord/delivery.ts +57 -13
- package/src/channels/discord/human-delay.ts +48 -0
- package/src/channels/discord/inbound.ts +66 -7
- package/src/channels/discord/mentions.ts +42 -18
- package/src/channels/discord/presence.ts +148 -0
- package/src/channels/discord/rate-limiter.ts +58 -0
- package/src/channels/discord/reactions.ts +211 -0
- package/src/channels/discord/runtime.ts +1048 -182
- package/src/channels/discord/stream.ts +73 -27
- package/src/channels/discord/tool-actions.ts +78 -37
- package/src/channels/discord/typing.ts +140 -0
- package/src/chunk.ts +12 -4
- package/src/cli.ts +141 -56
- package/src/config.ts +192 -34
- package/src/container-runner.ts +132 -42
- package/src/container-setup.ts +57 -22
- package/src/conversation.ts +9 -7
- package/src/db.ts +2217 -84
- package/src/delegation-manager.ts +6 -2
- package/src/gateway-client.ts +41 -17
- package/src/gateway-service.ts +1019 -201
- package/src/gateway-types.ts +33 -0
- package/src/gateway.ts +321 -48
- package/src/health.ts +66 -26
- package/src/heartbeat.ts +84 -22
- package/src/hybridai-bots.ts +14 -5
- package/src/instruction-approval-audit.ts +4 -1
- package/src/instruction-integrity.ts +30 -9
- package/src/ipc.ts +23 -5
- package/src/logger.ts +4 -1
- package/src/memory-consolidation.ts +41 -0
- package/src/memory-service.ts +606 -0
- package/src/mount-security.ts +58 -13
- package/src/observability-ingest.ts +134 -35
- package/src/onboarding.ts +126 -35
- package/src/proactive-policy.ts +3 -1
- package/src/prompt-hooks.ts +40 -17
- package/src/runtime-config.ts +1114 -99
- package/src/scheduled-task-runner.ts +63 -11
- package/src/scheduler.ts +683 -60
- package/src/session-export.ts +196 -0
- package/src/session-maintenance.ts +125 -22
- package/src/session-transcripts.ts +12 -3
- package/src/side-effects.ts +28 -5
- package/src/skills-guard.ts +1067 -219
- package/src/skills.ts +163 -65
- package/src/token-efficiency.ts +31 -9
- package/src/tui.ts +166 -25
- package/src/types.ts +195 -2
- package/src/update.ts +79 -23
- package/src/workspace.ts +63 -11
- package/tests/approval-policy.test.ts +224 -0
- package/tests/discord.basic.test.ts +82 -2
- package/tests/discord.human-presence.test.ts +85 -0
- package/tests/gateway-service.media-routing.test.ts +8 -2
- package/tests/memory-service.test.ts +1114 -0
- package/tests/token-efficiency.basic.test.ts +8 -2
- package/vitest.e2e.config.ts +3 -1
- package/vitest.integration.config.ts +3 -1
- package/vitest.live.config.ts +3 -1
- package/vitest.unit.config.ts +9 -0
package/src/skills-guard.ts
CHANGED
|
@@ -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 =
|
|
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',
|
|
85
|
-
'.
|
|
86
|
-
'.
|
|
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',
|
|
91
|
-
'.
|
|
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>([
|
|
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<
|
|
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
|
-
{
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
{
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
{
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
{
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
{
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
{
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
{
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
{
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
{
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
{
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
{
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
{
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
{
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
{
|
|
264
|
-
|
|
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
|
-
{
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
{
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
{
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
{
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
{
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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(
|
|
1189
|
+
state.signatureParts.push(
|
|
1190
|
+
`L:${relativePath}:${Math.trunc(stat.mtimeMs)}:${resolved || 'BROKEN'}`,
|
|
1191
|
+
);
|
|
384
1192
|
|
|
385
1193
|
if (!resolved) {
|
|
386
|
-
state.findings.push(
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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(
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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(
|
|
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(
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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(
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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(
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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(
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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(
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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 (
|
|
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(
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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 =
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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'))
|
|
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(
|
|
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
|
|
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(
|
|
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')
|
|
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): {
|
|
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 {
|