palaryn 0.1.0 → 0.3.2
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/README.md +243 -588
- package/dist/sdk/typescript/src/client.js +2 -2
- package/dist/sdk/typescript/src/client.js.map +1 -1
- package/dist/src/anomaly/detector.d.ts +7 -4
- package/dist/src/anomaly/detector.d.ts.map +1 -1
- package/dist/src/anomaly/detector.js +22 -12
- package/dist/src/anomaly/detector.js.map +1 -1
- package/dist/src/audit/logger.d.ts +10 -0
- package/dist/src/audit/logger.d.ts.map +1 -1
- package/dist/src/audit/logger.js +52 -38
- package/dist/src/audit/logger.js.map +1 -1
- package/dist/src/auth/routes.d.ts.map +1 -1
- package/dist/src/auth/routes.js +35 -0
- package/dist/src/auth/routes.js.map +1 -1
- package/dist/src/budget/manager.d.ts +5 -0
- package/dist/src/budget/manager.d.ts.map +1 -1
- package/dist/src/budget/manager.js +32 -0
- package/dist/src/budget/manager.js.map +1 -1
- package/dist/src/budget/model-pricing.d.ts +20 -0
- package/dist/src/budget/model-pricing.d.ts.map +1 -0
- package/dist/src/budget/model-pricing.js +107 -0
- package/dist/src/budget/model-pricing.js.map +1 -0
- package/dist/src/budget/usage-extractor.d.ts +3 -1
- package/dist/src/budget/usage-extractor.d.ts.map +1 -1
- package/dist/src/budget/usage-extractor.js +47 -3
- package/dist/src/budget/usage-extractor.js.map +1 -1
- package/dist/src/config/defaults.d.ts.map +1 -1
- package/dist/src/config/defaults.js +65 -13
- package/dist/src/config/defaults.js.map +1 -1
- package/dist/src/dlp/tool-patterns.d.ts +7 -0
- package/dist/src/dlp/tool-patterns.d.ts.map +1 -0
- package/dist/src/dlp/tool-patterns.js +34 -0
- package/dist/src/dlp/tool-patterns.js.map +1 -0
- package/dist/src/executor/filesystem-executor.d.ts +28 -0
- package/dist/src/executor/filesystem-executor.d.ts.map +1 -0
- package/dist/src/executor/filesystem-executor.js +192 -0
- package/dist/src/executor/filesystem-executor.js.map +1 -0
- package/dist/src/executor/http-executor.d.ts.map +1 -1
- package/dist/src/executor/http-executor.js +22 -2
- package/dist/src/executor/http-executor.js.map +1 -1
- package/dist/src/executor/index.d.ts +4 -0
- package/dist/src/executor/index.d.ts.map +1 -1
- package/dist/src/executor/index.js +9 -1
- package/dist/src/executor/index.js.map +1 -1
- package/dist/src/executor/shell-executor.d.ts +22 -0
- package/dist/src/executor/shell-executor.d.ts.map +1 -0
- package/dist/src/executor/shell-executor.js +119 -0
- package/dist/src/executor/shell-executor.js.map +1 -0
- package/dist/src/executor/sql-executor.d.ts +29 -0
- package/dist/src/executor/sql-executor.d.ts.map +1 -0
- package/dist/src/executor/sql-executor.js +114 -0
- package/dist/src/executor/sql-executor.js.map +1 -0
- package/dist/src/executor/websocket-executor.d.ts +26 -0
- package/dist/src/executor/websocket-executor.d.ts.map +1 -0
- package/dist/src/executor/websocket-executor.js +205 -0
- package/dist/src/executor/websocket-executor.js.map +1 -0
- package/dist/src/interceptor/index.d.ts +2 -0
- package/dist/src/interceptor/index.d.ts.map +1 -0
- package/dist/src/interceptor/index.js +6 -0
- package/dist/src/interceptor/index.js.map +1 -0
- package/dist/src/interceptor/provider-interceptor.d.ts +36 -0
- package/dist/src/interceptor/provider-interceptor.d.ts.map +1 -0
- package/dist/src/interceptor/provider-interceptor.js +302 -0
- package/dist/src/interceptor/provider-interceptor.js.map +1 -0
- package/dist/src/mcp/auth-verifier.d.ts.map +1 -1
- package/dist/src/mcp/auth-verifier.js +3 -2
- package/dist/src/mcp/auth-verifier.js.map +1 -1
- package/dist/src/mcp/bridge.d.ts +14 -10
- package/dist/src/mcp/bridge.d.ts.map +1 -1
- package/dist/src/mcp/bridge.js +51 -227
- package/dist/src/mcp/bridge.js.map +1 -1
- package/dist/src/mcp/http-transport.d.ts +2 -0
- package/dist/src/mcp/http-transport.d.ts.map +1 -1
- package/dist/src/mcp/http-transport.js +117 -66
- package/dist/src/mcp/http-transport.js.map +1 -1
- package/dist/src/mcp/internal-auth.d.ts +13 -0
- package/dist/src/mcp/internal-auth.d.ts.map +1 -0
- package/dist/src/mcp/internal-auth.js +12 -0
- package/dist/src/mcp/internal-auth.js.map +1 -0
- package/dist/src/mcp/tool-definitions.d.ts +41 -0
- package/dist/src/mcp/tool-definitions.d.ts.map +1 -0
- package/dist/src/mcp/tool-definitions.js +491 -0
- package/dist/src/mcp/tool-definitions.js.map +1 -0
- package/dist/src/middleware/auth.js.map +1 -1
- package/dist/src/middleware/session.js.map +1 -1
- package/dist/src/middleware/validate.d.ts +8 -0
- package/dist/src/middleware/validate.d.ts.map +1 -1
- package/dist/src/middleware/validate.js +45 -0
- package/dist/src/middleware/validate.js.map +1 -1
- package/dist/src/policy/engine.d.ts +4 -0
- package/dist/src/policy/engine.d.ts.map +1 -1
- package/dist/src/policy/engine.js +117 -0
- package/dist/src/policy/engine.js.map +1 -1
- package/dist/src/saas/routes.d.ts.map +1 -1
- package/dist/src/saas/routes.js +355 -22
- package/dist/src/saas/routes.js.map +1 -1
- package/dist/src/server/app.d.ts.map +1 -1
- package/dist/src/server/app.js +24 -3
- package/dist/src/server/app.js.map +1 -1
- package/dist/src/server/gateway.d.ts.map +1 -1
- package/dist/src/server/gateway.js +17 -0
- package/dist/src/server/gateway.js.map +1 -1
- package/dist/src/server/index.d.ts.map +1 -1
- package/dist/src/server/index.js +18 -0
- package/dist/src/server/index.js.map +1 -1
- package/dist/src/storage/interfaces.d.ts +14 -3
- package/dist/src/storage/interfaces.d.ts.map +1 -1
- package/dist/src/storage/memory.d.ts +2 -0
- package/dist/src/storage/memory.d.ts.map +1 -1
- package/dist/src/storage/memory.js +6 -0
- package/dist/src/storage/memory.js.map +1 -1
- package/dist/src/storage/postgres.d.ts +5 -0
- package/dist/src/storage/postgres.d.ts.map +1 -1
- package/dist/src/storage/postgres.js +16 -0
- package/dist/src/storage/postgres.js.map +1 -1
- package/dist/src/storage/redis.d.ts +10 -0
- package/dist/src/storage/redis.d.ts.map +1 -1
- package/dist/src/storage/redis.js +65 -0
- package/dist/src/storage/redis.js.map +1 -1
- package/dist/src/types/budget.d.ts +4 -0
- package/dist/src/types/budget.d.ts.map +1 -1
- package/dist/src/types/config.d.ts +58 -0
- package/dist/src/types/config.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +1 -0
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/policy.d.ts +11 -1
- package/dist/src/types/policy.d.ts.map +1 -1
- package/dist/src/types/tool-result.d.ts +11 -0
- package/dist/src/types/tool-result.d.ts.map +1 -1
- package/dist/tests/unit/app-routes.test.d.ts +2 -0
- package/dist/tests/unit/app-routes.test.d.ts.map +1 -0
- package/dist/tests/unit/app-routes.test.js +715 -0
- package/dist/tests/unit/app-routes.test.js.map +1 -0
- package/dist/tests/unit/audit-logger.test.js +105 -0
- package/dist/tests/unit/audit-logger.test.js.map +1 -1
- package/dist/tests/unit/auth-providers.test.d.ts +2 -0
- package/dist/tests/unit/auth-providers.test.d.ts.map +1 -0
- package/dist/tests/unit/auth-providers.test.js +279 -0
- package/dist/tests/unit/auth-providers.test.js.map +1 -0
- package/dist/tests/unit/auth-routes-extended.test.d.ts +2 -0
- package/dist/tests/unit/auth-routes-extended.test.d.ts.map +1 -0
- package/dist/tests/unit/auth-routes-extended.test.js +993 -0
- package/dist/tests/unit/auth-routes-extended.test.js.map +1 -0
- package/dist/tests/unit/auth-verifier.test.d.ts +2 -0
- package/dist/tests/unit/auth-verifier.test.d.ts.map +1 -0
- package/dist/tests/unit/auth-verifier.test.js +505 -0
- package/dist/tests/unit/auth-verifier.test.js.map +1 -0
- package/dist/tests/unit/billing-routes.test.d.ts +2 -0
- package/dist/tests/unit/billing-routes.test.d.ts.map +1 -0
- package/dist/tests/unit/billing-routes.test.js +432 -0
- package/dist/tests/unit/billing-routes.test.js.map +1 -0
- package/dist/tests/unit/config-defaults.test.d.ts +2 -0
- package/dist/tests/unit/config-defaults.test.d.ts.map +1 -0
- package/dist/tests/unit/config-defaults.test.js +119 -0
- package/dist/tests/unit/config-defaults.test.js.map +1 -0
- package/dist/tests/unit/defaults.test.js +0 -10
- package/dist/tests/unit/defaults.test.js.map +1 -1
- package/dist/tests/unit/filesystem-executor.test.d.ts +2 -0
- package/dist/tests/unit/filesystem-executor.test.d.ts.map +1 -0
- package/dist/tests/unit/filesystem-executor.test.js +280 -0
- package/dist/tests/unit/filesystem-executor.test.js.map +1 -0
- package/dist/tests/unit/gateway-branches.test.d.ts +2 -0
- package/dist/tests/unit/gateway-branches.test.d.ts.map +1 -0
- package/dist/tests/unit/gateway-branches.test.js +1039 -0
- package/dist/tests/unit/gateway-branches.test.js.map +1 -0
- package/dist/tests/unit/http-executor-branches.test.d.ts +2 -0
- package/dist/tests/unit/http-executor-branches.test.d.ts.map +1 -0
- package/dist/tests/unit/http-executor-branches.test.js +495 -0
- package/dist/tests/unit/http-executor-branches.test.js.map +1 -0
- package/dist/tests/unit/logger.test.d.ts +2 -0
- package/dist/tests/unit/logger.test.d.ts.map +1 -0
- package/dist/tests/unit/logger.test.js +97 -0
- package/dist/tests/unit/logger.test.js.map +1 -0
- package/dist/tests/unit/mcp-internal-auth.test.d.ts +2 -0
- package/dist/tests/unit/mcp-internal-auth.test.d.ts.map +1 -0
- package/dist/tests/unit/mcp-internal-auth.test.js +445 -0
- package/dist/tests/unit/mcp-internal-auth.test.js.map +1 -0
- package/dist/tests/unit/metrics.test.js +102 -0
- package/dist/tests/unit/metrics.test.js.map +1 -1
- package/dist/tests/unit/model-pricing.test.d.ts +2 -0
- package/dist/tests/unit/model-pricing.test.d.ts.map +1 -0
- package/dist/tests/unit/model-pricing.test.js +87 -0
- package/dist/tests/unit/model-pricing.test.js.map +1 -0
- package/dist/tests/unit/oauth-stores.test.d.ts +2 -0
- package/dist/tests/unit/oauth-stores.test.d.ts.map +1 -0
- package/dist/tests/unit/oauth-stores.test.js +260 -0
- package/dist/tests/unit/oauth-stores.test.js.map +1 -0
- package/dist/tests/unit/policy-engine.test.js +466 -0
- package/dist/tests/unit/policy-engine.test.js.map +1 -1
- package/dist/tests/unit/provider-interceptor.test.d.ts +2 -0
- package/dist/tests/unit/provider-interceptor.test.d.ts.map +1 -0
- package/dist/tests/unit/provider-interceptor.test.js +472 -0
- package/dist/tests/unit/provider-interceptor.test.js.map +1 -0
- package/dist/tests/unit/saas-routes-branches.test.d.ts +2 -0
- package/dist/tests/unit/saas-routes-branches.test.d.ts.map +1 -0
- package/dist/tests/unit/saas-routes-branches.test.js +2165 -0
- package/dist/tests/unit/saas-routes-branches.test.js.map +1 -0
- package/dist/tests/unit/saas-routes-crud.test.d.ts +2 -0
- package/dist/tests/unit/saas-routes-crud.test.d.ts.map +1 -0
- package/dist/tests/unit/saas-routes-crud.test.js +332 -0
- package/dist/tests/unit/saas-routes-crud.test.js.map +1 -0
- package/dist/tests/unit/saas-routes-data.test.d.ts +2 -0
- package/dist/tests/unit/saas-routes-data.test.d.ts.map +1 -0
- package/dist/tests/unit/saas-routes-data.test.js +405 -0
- package/dist/tests/unit/saas-routes-data.test.js.map +1 -0
- package/dist/tests/unit/saas-routes.test.js +3 -3
- package/dist/tests/unit/saas-routes.test.js.map +1 -1
- package/dist/tests/unit/shell-executor.test.d.ts +2 -0
- package/dist/tests/unit/shell-executor.test.d.ts.map +1 -0
- package/dist/tests/unit/shell-executor.test.js +145 -0
- package/dist/tests/unit/shell-executor.test.js.map +1 -0
- package/dist/tests/unit/sql-executor.test.d.ts +2 -0
- package/dist/tests/unit/sql-executor.test.d.ts.map +1 -0
- package/dist/tests/unit/sql-executor.test.js +177 -0
- package/dist/tests/unit/sql-executor.test.js.map +1 -0
- package/dist/tests/unit/stream-proxy.test.d.ts +2 -0
- package/dist/tests/unit/stream-proxy.test.d.ts.map +1 -0
- package/dist/tests/unit/stream-proxy.test.js +147 -0
- package/dist/tests/unit/stream-proxy.test.js.map +1 -0
- package/dist/tests/unit/tool-definitions.test.d.ts +2 -0
- package/dist/tests/unit/tool-definitions.test.d.ts.map +1 -0
- package/dist/tests/unit/tool-definitions.test.js +184 -0
- package/dist/tests/unit/tool-definitions.test.js.map +1 -0
- package/dist/tests/unit/usage-extractor.test.js +140 -0
- package/dist/tests/unit/usage-extractor.test.js.map +1 -1
- package/dist/tests/unit/webhook-handler.test.d.ts +2 -0
- package/dist/tests/unit/webhook-handler.test.d.ts.map +1 -0
- package/dist/tests/unit/webhook-handler.test.js +453 -0
- package/dist/tests/unit/webhook-handler.test.js.map +1 -0
- package/dist/tests/unit/webhook-routes.test.d.ts +2 -0
- package/dist/tests/unit/webhook-routes.test.d.ts.map +1 -0
- package/dist/tests/unit/webhook-routes.test.js +69 -0
- package/dist/tests/unit/webhook-routes.test.js.map +1 -0
- package/dist/tests/unit/websocket-executor.test.d.ts +2 -0
- package/dist/tests/unit/websocket-executor.test.d.ts.map +1 -0
- package/dist/tests/unit/websocket-executor.test.js +121 -0
- package/dist/tests/unit/websocket-executor.test.js.map +1 -0
- package/package.json +8 -2
- package/policy-packs/demo_fail.yaml +41 -0
- package/policy-packs/full_tools.yaml +136 -0
- package/src/admin/index.ts +1 -0
- package/src/admin/routes.ts +509 -0
- package/src/admin/templates.ts +572 -0
- package/src/anomaly/detector.ts +730 -0
- package/src/anomaly/index.ts +1 -0
- package/src/approval/manager.ts +569 -0
- package/src/approval/webhook.ts +133 -0
- package/src/audit/logger.ts +490 -0
- package/src/auth/index.ts +5 -0
- package/src/auth/password.ts +21 -0
- package/src/auth/pkce.ts +22 -0
- package/src/auth/providers.ts +208 -0
- package/src/auth/routes.ts +561 -0
- package/src/auth/session.ts +84 -0
- package/src/billing/index.ts +6 -0
- package/src/billing/plan-enforcer.ts +135 -0
- package/src/billing/routes.ts +229 -0
- package/src/billing/stripe-client.ts +58 -0
- package/src/billing/webhook-handler.ts +182 -0
- package/src/billing/webhook-routes.ts +28 -0
- package/src/budget/manager.ts +679 -0
- package/src/budget/model-pricing.ts +119 -0
- package/src/budget/usage-extractor.ts +214 -0
- package/src/cli.ts +91 -0
- package/src/config/defaults.ts +261 -0
- package/src/config/validate.ts +88 -0
- package/src/dlp/composite-scanner.ts +213 -0
- package/src/dlp/index.ts +9 -0
- package/src/dlp/interfaces.ts +34 -0
- package/src/dlp/patterns.ts +30 -0
- package/src/dlp/prompt-injection-backend.ts +181 -0
- package/src/dlp/prompt-injection-patterns.ts +302 -0
- package/src/dlp/regex-backend.ts +181 -0
- package/src/dlp/scanner.ts +502 -0
- package/src/dlp/text-normalizer.ts +225 -0
- package/src/dlp/tool-patterns.ts +35 -0
- package/src/dlp/trufflehog-backend.ts +190 -0
- package/src/executor/filesystem-executor.ts +196 -0
- package/src/executor/http-executor.ts +349 -0
- package/src/executor/index.ts +9 -0
- package/src/executor/interfaces.ts +11 -0
- package/src/executor/noop-executor.ts +23 -0
- package/src/executor/registry.ts +64 -0
- package/src/executor/shell-executor.ts +148 -0
- package/src/executor/slack-executor.ts +176 -0
- package/src/executor/sql-executor.ts +146 -0
- package/src/executor/websocket-executor.ts +211 -0
- package/src/index.ts +24 -0
- package/src/interceptor/index.ts +1 -0
- package/src/interceptor/provider-interceptor.ts +315 -0
- package/src/mcp/auth-verifier.ts +152 -0
- package/src/mcp/bridge.ts +703 -0
- package/src/mcp/http-transport.ts +698 -0
- package/src/mcp/index.ts +9 -0
- package/src/mcp/internal-auth.ts +14 -0
- package/src/mcp/oauth-pages.ts +139 -0
- package/src/mcp/oauth-postgres-stores.ts +278 -0
- package/src/mcp/oauth-provider.ts +536 -0
- package/src/mcp/oauth-stores.ts +202 -0
- package/src/mcp/server.ts +55 -0
- package/src/mcp/tool-definitions.ts +562 -0
- package/src/metrics/collector.ts +357 -0
- package/src/metrics/index.ts +1 -0
- package/src/middleware/auth.ts +814 -0
- package/src/middleware/session.ts +85 -0
- package/src/middleware/validate.ts +130 -0
- package/src/policy/engine.ts +815 -0
- package/src/policy/index.ts +2 -0
- package/src/policy/opa-engine.ts +829 -0
- package/src/proxy/forward-proxy.ts +649 -0
- package/src/proxy/index.ts +1 -0
- package/src/ratelimit/limiter.ts +196 -0
- package/src/replay/engine.ts +142 -0
- package/src/replay/index.ts +1 -0
- package/src/saas/index.ts +1 -0
- package/src/saas/routes.ts +2178 -0
- package/src/server/app.ts +985 -0
- package/src/server/errors.ts +49 -0
- package/src/server/gateway.ts +1130 -0
- package/src/server/index.ts +307 -0
- package/src/server/logger.ts +255 -0
- package/src/server/stream-proxy.ts +202 -0
- package/src/storage/file-persistence.ts +315 -0
- package/src/storage/index.ts +4 -0
- package/src/storage/interfaces.ts +287 -0
- package/src/storage/memory.ts +686 -0
- package/src/storage/postgres.ts +1831 -0
- package/src/storage/redis.ts +835 -0
- package/src/tracing/index.ts +1 -0
- package/src/tracing/provider.ts +100 -0
- package/src/trust/calculator.ts +141 -0
- package/src/trust/index.ts +7 -0
- package/src/types/budget.ts +36 -0
- package/src/types/config.ts +278 -0
- package/src/types/events.ts +41 -0
- package/src/types/express.d.ts +14 -0
- package/src/types/index.ts +7 -0
- package/src/types/policy.ts +83 -0
- package/src/types/stripe-config.ts +11 -0
- package/src/types/subscription.ts +59 -0
- package/src/types/tool-call.ts +47 -0
- package/src/types/tool-result.ts +82 -0
- package/src/types/user.ts +125 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text normalizer for bypass-resistant prompt injection detection.
|
|
3
|
+
*
|
|
4
|
+
* Applies a series of transformations to collapse evasion techniques
|
|
5
|
+
* (zero-width chars, HTML entities, homoglyphs, leetspeak, etc.)
|
|
6
|
+
* into canonical ASCII text before pattern matching.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Zero-width character stripping
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
/** Regex matching zero-width and invisible Unicode characters. */
|
|
14
|
+
export const ZERO_WIDTH_REGEX = /[\u200B\u200C\u200D\u00AD\uFEFF\u200E\u200F\u2060\u2061\u2062\u2063\u2064\u180E]/g;
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Homoglyph map (visually similar characters -> ASCII equivalents)
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
/** Map of Unicode homoglyphs to their ASCII equivalents. */
|
|
21
|
+
export const HOMOGLYPH_MAP: Record<string, string> = {
|
|
22
|
+
// Cyrillic -> Latin
|
|
23
|
+
'\u0430': 'a', // а
|
|
24
|
+
'\u0435': 'e', // е
|
|
25
|
+
'\u043E': 'o', // о
|
|
26
|
+
'\u0440': 'p', // р
|
|
27
|
+
'\u0441': 'c', // с
|
|
28
|
+
'\u0443': 'y', // у
|
|
29
|
+
'\u0445': 'x', // х
|
|
30
|
+
'\u0456': 'i', // і
|
|
31
|
+
'\u0458': 'j', // ј
|
|
32
|
+
'\u04BB': 'h', // һ
|
|
33
|
+
'\u0410': 'A', // А
|
|
34
|
+
'\u0412': 'B', // В
|
|
35
|
+
'\u0415': 'E', // Е
|
|
36
|
+
'\u041A': 'K', // К
|
|
37
|
+
'\u041C': 'M', // М
|
|
38
|
+
'\u041D': 'H', // Н
|
|
39
|
+
'\u041E': 'O', // О
|
|
40
|
+
'\u0420': 'P', // Р
|
|
41
|
+
'\u0421': 'C', // С
|
|
42
|
+
'\u0422': 'T', // Т
|
|
43
|
+
'\u0425': 'X', // Х
|
|
44
|
+
// Greek -> Latin
|
|
45
|
+
'\u03B1': 'a', // α
|
|
46
|
+
'\u03BF': 'o', // ο
|
|
47
|
+
'\u03C1': 'p', // ρ
|
|
48
|
+
'\u0391': 'A', // Α
|
|
49
|
+
'\u0392': 'B', // Β
|
|
50
|
+
'\u0395': 'E', // Ε
|
|
51
|
+
'\u0397': 'H', // Η
|
|
52
|
+
'\u0399': 'I', // Ι
|
|
53
|
+
'\u039A': 'K', // Κ
|
|
54
|
+
'\u039C': 'M', // Μ
|
|
55
|
+
'\u039D': 'N', // Ν
|
|
56
|
+
'\u039F': 'O', // Ο
|
|
57
|
+
'\u03A1': 'P', // Ρ
|
|
58
|
+
'\u03A4': 'T', // Τ
|
|
59
|
+
'\u03A7': 'X', // Χ
|
|
60
|
+
'\u03A5': 'Y', // Υ
|
|
61
|
+
'\u0396': 'Z', // Ζ
|
|
62
|
+
// Fullwidth -> ASCII (supplemental to NFKC — belt and suspenders)
|
|
63
|
+
'\uFF41': 'a',
|
|
64
|
+
'\uFF42': 'b',
|
|
65
|
+
'\uFF43': 'c',
|
|
66
|
+
'\uFF49': 'i',
|
|
67
|
+
'\uFF4E': 'n',
|
|
68
|
+
'\uFF4F': 'o',
|
|
69
|
+
'\uFF50': 'p',
|
|
70
|
+
'\uFF52': 'r',
|
|
71
|
+
'\uFF53': 's',
|
|
72
|
+
'\uFF54': 't',
|
|
73
|
+
'\uFF55': 'u',
|
|
74
|
+
// Common lookalikes
|
|
75
|
+
'\u0131': 'i', // ı (dotless i)
|
|
76
|
+
'\u0237': 'j', // ȷ (dotless j)
|
|
77
|
+
'\u01C0': 'l', // ǀ (dental click -> l)
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Build reverse lookup for efficiency
|
|
81
|
+
const homoglyphRegex = new RegExp(
|
|
82
|
+
'[' + Object.keys(HOMOGLYPH_MAP).join('') + ']',
|
|
83
|
+
'g',
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Leetspeak map
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
/** Map of common leetspeak substitutions to their letter equivalents. */
|
|
91
|
+
export const LEETSPEAK_MAP: Record<string, string> = {
|
|
92
|
+
'0': 'o',
|
|
93
|
+
'1': 'i',
|
|
94
|
+
'3': 'e',
|
|
95
|
+
'4': 'a',
|
|
96
|
+
'5': 's',
|
|
97
|
+
'7': 't',
|
|
98
|
+
'@': 'a',
|
|
99
|
+
'$': 's',
|
|
100
|
+
'!': 'i',
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const leetspeakRegex = /[013457@$!]/g;
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// HTML entity decoding
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
/** Named HTML entities most commonly used for evasion. */
|
|
110
|
+
const NAMED_ENTITIES: Record<string, string> = {
|
|
111
|
+
'<': '<',
|
|
112
|
+
'>': '>',
|
|
113
|
+
'&': '&',
|
|
114
|
+
'"': '"',
|
|
115
|
+
''': "'",
|
|
116
|
+
' ': ' ',
|
|
117
|
+
'&tab;': '\t',
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/** Decode HTML entities (named + numeric decimal + numeric hex). */
|
|
121
|
+
function decodeHTMLEntities(input: string): string {
|
|
122
|
+
// Named entities
|
|
123
|
+
let result = input;
|
|
124
|
+
for (const [entity, char] of Object.entries(NAMED_ENTITIES)) {
|
|
125
|
+
// Case-insensitive replacement for named entities
|
|
126
|
+
const re = new RegExp(entity.replace(/[&;]/g, (c) => '\\' + c), 'gi');
|
|
127
|
+
result = result.replace(re, char);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Decimal numeric entities: i -> 'i'
|
|
131
|
+
result = result.replace(/&#(\d+);/g, (_match, digits) => {
|
|
132
|
+
const code = parseInt(digits, 10);
|
|
133
|
+
if (code > 0 && code <= 0x10FFFF) {
|
|
134
|
+
return String.fromCodePoint(code);
|
|
135
|
+
}
|
|
136
|
+
return _match;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Hex numeric entities: i -> 'i'
|
|
140
|
+
result = result.replace(/&#x([0-9a-fA-F]+);/g, (_match, hex) => {
|
|
141
|
+
const code = parseInt(hex, 16);
|
|
142
|
+
if (code > 0 && code <= 0x10FFFF) {
|
|
143
|
+
return String.fromCodePoint(code);
|
|
144
|
+
}
|
|
145
|
+
return _match;
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// URL decoding
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
/** Decode percent-encoded sequences (%69 -> 'i'). */
|
|
156
|
+
function decodeURLEncoding(input: string): string {
|
|
157
|
+
try {
|
|
158
|
+
return decodeURIComponent(input);
|
|
159
|
+
} catch {
|
|
160
|
+
// If decoding fails (malformed sequences), apply partial decoding
|
|
161
|
+
return input.replace(/%([0-9a-fA-F]{2})/g, (_match, hex) => {
|
|
162
|
+
return String.fromCharCode(parseInt(hex, 16));
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Main normalizer
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Normalize text for bypass-resistant pattern matching.
|
|
173
|
+
*
|
|
174
|
+
* Applies transformations in order:
|
|
175
|
+
* 1. Strip zero-width / invisible Unicode characters
|
|
176
|
+
* 2. Unicode NFKC normalization (collapses fullwidth, ligatures, etc.)
|
|
177
|
+
* 3. Decode HTML entities (named + numeric)
|
|
178
|
+
* 4. Decode URL percent-encoding
|
|
179
|
+
* 5. Collapse homoglyphs (Cyrillic/Greek lookalikes -> ASCII)
|
|
180
|
+
* 6. Collapse repeated whitespace to single space
|
|
181
|
+
*
|
|
182
|
+
* @param input - The raw text to normalize.
|
|
183
|
+
* @returns The normalized text suitable for pattern matching.
|
|
184
|
+
*/
|
|
185
|
+
export function normalizeText(input: string): string {
|
|
186
|
+
// Early exit for very short strings
|
|
187
|
+
if (input.length === 0) return input;
|
|
188
|
+
|
|
189
|
+
let text = input;
|
|
190
|
+
|
|
191
|
+
// 1. Strip zero-width characters
|
|
192
|
+
text = text.replace(ZERO_WIDTH_REGEX, '');
|
|
193
|
+
|
|
194
|
+
// 2. NFKC normalization (fullwidth -> ASCII, ligatures -> components, etc.)
|
|
195
|
+
text = text.normalize('NFKC');
|
|
196
|
+
|
|
197
|
+
// 3. Decode HTML entities
|
|
198
|
+
text = decodeHTMLEntities(text);
|
|
199
|
+
|
|
200
|
+
// 4. Decode URL percent-encoding
|
|
201
|
+
text = decodeURLEncoding(text);
|
|
202
|
+
|
|
203
|
+
// 5. Collapse homoglyphs
|
|
204
|
+
text = text.replace(homoglyphRegex, (ch) => HOMOGLYPH_MAP[ch] || ch);
|
|
205
|
+
|
|
206
|
+
// 6. Collapse whitespace (spaces, tabs, newlines) to single space and trim
|
|
207
|
+
text = text.replace(/\s+/g, ' ').trim();
|
|
208
|
+
|
|
209
|
+
return text;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Apply leetspeak normalization on top of standard normalization.
|
|
214
|
+
*
|
|
215
|
+
* Returns the leet-decoded version of the text. Callers should match
|
|
216
|
+
* patterns against BOTH the standard-normalized and leet-normalized text
|
|
217
|
+
* to catch leet evasions without causing false positives on normal text
|
|
218
|
+
* containing digits.
|
|
219
|
+
*
|
|
220
|
+
* @param normalizedInput - Text already passed through normalizeText().
|
|
221
|
+
* @returns The leet-decoded text.
|
|
222
|
+
*/
|
|
223
|
+
export function normalizeLeetspeak(normalizedInput: string): string {
|
|
224
|
+
return normalizedInput.replace(leetspeakRegex, (ch) => LEETSPEAK_MAP[ch] || ch);
|
|
225
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { DLPPattern } from './patterns';
|
|
2
|
+
|
|
3
|
+
// Shell injection patterns
|
|
4
|
+
export const SHELL_INJECTION_PATTERNS: DLPPattern[] = [
|
|
5
|
+
{ name: 'shell_pipe', pattern: /\|/g, severity: 'medium' },
|
|
6
|
+
{ name: 'shell_subshell', pattern: /\$\(|\`/g, severity: 'high' },
|
|
7
|
+
{ name: 'shell_redirect', pattern: /[><]{1,2}/g, severity: 'medium' },
|
|
8
|
+
{ name: 'shell_semicolon', pattern: /;\s*\w/g, severity: 'high' },
|
|
9
|
+
{ name: 'shell_background', pattern: /&\s*$/g, severity: 'medium' },
|
|
10
|
+
{ name: 'shell_env_expansion', pattern: /\$\{[^}]+\}/g, severity: 'medium' },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
// Path traversal patterns
|
|
14
|
+
export const PATH_TRAVERSAL_PATTERNS: DLPPattern[] = [
|
|
15
|
+
{ name: 'path_traversal', pattern: /\.\.\//g, severity: 'high' },
|
|
16
|
+
{ name: 'path_traversal_encoded', pattern: /%2e%2e%2f/gi, severity: 'high' },
|
|
17
|
+
{ name: 'path_null_byte', pattern: /%00/g, severity: 'high' },
|
|
18
|
+
{ name: 'path_absolute_unix', pattern: /^\/(?:etc|proc|sys|dev|root|var\/log)\//g, severity: 'high' },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
// SQL injection patterns
|
|
22
|
+
export const SQL_INJECTION_PATTERNS: DLPPattern[] = [
|
|
23
|
+
{ name: 'sql_union_select', pattern: /UNION\s+(?:ALL\s+)?SELECT/gi, severity: 'high' },
|
|
24
|
+
{ name: 'sql_stacked_query', pattern: /;\s*(?:SELECT|INSERT|UPDATE|DELETE|DROP|ALTER|CREATE)\b/gi, severity: 'high' },
|
|
25
|
+
{ name: 'sql_comment_injection', pattern: /(?:--|#|\/\*)/g, severity: 'medium' },
|
|
26
|
+
{ name: 'sql_sleep_benchmark', pattern: /(?:SLEEP|BENCHMARK|WAITFOR\s+DELAY)\s*\(/gi, severity: 'high' },
|
|
27
|
+
{ name: 'sql_info_schema', pattern: /INFORMATION_SCHEMA/gi, severity: 'high' },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/** All tool-specific DLP patterns combined */
|
|
31
|
+
export const TOOL_DLP_PATTERNS: DLPPattern[] = [
|
|
32
|
+
...SHELL_INJECTION_PATTERNS,
|
|
33
|
+
...PATH_TRAVERSAL_PATTERNS,
|
|
34
|
+
...SQL_INJECTION_PATTERNS,
|
|
35
|
+
];
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import { writeFileSync, unlinkSync } from 'fs';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { randomUUID } from 'crypto';
|
|
6
|
+
import { DLPSeverity } from '../types/tool-result';
|
|
7
|
+
import { DLPBackend, DLPDetection } from './interfaces';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Configuration for the TruffleHog DLP backend.
|
|
11
|
+
*/
|
|
12
|
+
export interface TruffleHogConfig {
|
|
13
|
+
/** Path to the trufflehog binary. Defaults to 'trufflehog' (resolved via PATH). */
|
|
14
|
+
binaryPath?: string;
|
|
15
|
+
/** Execution timeout in milliseconds. Defaults to 10000 (10s). */
|
|
16
|
+
timeout?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Raw JSON output from a single trufflehog finding.
|
|
21
|
+
*
|
|
22
|
+
* TruffleHog emits one JSON object per line when run with --json.
|
|
23
|
+
*/
|
|
24
|
+
interface TruffleHogFinding {
|
|
25
|
+
DetectorName?: string;
|
|
26
|
+
DetectorType?: number;
|
|
27
|
+
Verified?: boolean;
|
|
28
|
+
Raw?: string;
|
|
29
|
+
RawV2?: string;
|
|
30
|
+
SourceMetadata?: unknown;
|
|
31
|
+
ExtraData?: Record<string, unknown>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* DLP backend that delegates secret scanning to the TruffleHog binary.
|
|
36
|
+
*
|
|
37
|
+
* TruffleHog is a comprehensive secret scanner that supports 700+ detector
|
|
38
|
+
* types including verified credential checks against live services.
|
|
39
|
+
*
|
|
40
|
+
* This backend writes the input string to a temporary file, runs
|
|
41
|
+
* `trufflehog filesystem --json --no-update <tmpfile>`, and parses the
|
|
42
|
+
* line-delimited JSON output into DLPDetection objects.
|
|
43
|
+
*
|
|
44
|
+
* Graceful degradation: if the trufflehog binary is not installed or the
|
|
45
|
+
* process fails for any reason, the backend logs a warning and returns an
|
|
46
|
+
* empty array (no detections). This allows it to be safely composed with
|
|
47
|
+
* other backends without breaking the pipeline.
|
|
48
|
+
*/
|
|
49
|
+
export class TruffleHogBackend implements DLPBackend {
|
|
50
|
+
readonly name = 'trufflehog';
|
|
51
|
+
|
|
52
|
+
private readonly binaryPath: string;
|
|
53
|
+
private readonly timeout: number;
|
|
54
|
+
|
|
55
|
+
constructor(config?: TruffleHogConfig) {
|
|
56
|
+
this.binaryPath = config?.binaryPath ?? 'trufflehog';
|
|
57
|
+
this.timeout = config?.timeout ?? 10_000;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Scan a string for secrets using trufflehog.
|
|
62
|
+
*
|
|
63
|
+
* 1. Writes the value to a temp file
|
|
64
|
+
* 2. Runs trufflehog filesystem in JSON mode
|
|
65
|
+
* 3. Parses each JSON line into a DLPDetection
|
|
66
|
+
* 4. Cleans up the temp file
|
|
67
|
+
*
|
|
68
|
+
* Returns an empty array if trufflehog is not available or fails.
|
|
69
|
+
*/
|
|
70
|
+
scanString(value: string): DLPDetection[] {
|
|
71
|
+
const tmpFile = join(tmpdir(), `palaryn-dlp-${randomUUID()}.tmp`);
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
// Write input to temp file for trufflehog to scan
|
|
75
|
+
writeFileSync(tmpFile, value, 'utf-8');
|
|
76
|
+
|
|
77
|
+
// Execute trufflehog
|
|
78
|
+
const stdout = execFileSync(
|
|
79
|
+
this.binaryPath,
|
|
80
|
+
['filesystem', '--json', '--no-update', tmpFile],
|
|
81
|
+
{
|
|
82
|
+
timeout: this.timeout,
|
|
83
|
+
encoding: 'utf-8',
|
|
84
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return this.parseOutput(stdout, value);
|
|
89
|
+
} catch (err: unknown) {
|
|
90
|
+
// Graceful degradation: log and return empty
|
|
91
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
92
|
+
// Only log if it's not a "no findings" exit (trufflehog exits 0 with no output when clean)
|
|
93
|
+
if (!this.isNoFindingsError(message)) {
|
|
94
|
+
console.warn(`[TruffleHogBackend] scan failed: ${message}`);
|
|
95
|
+
}
|
|
96
|
+
return [];
|
|
97
|
+
} finally {
|
|
98
|
+
// Always clean up the temp file
|
|
99
|
+
try {
|
|
100
|
+
unlinkSync(tmpFile);
|
|
101
|
+
} catch {
|
|
102
|
+
// Ignore cleanup errors
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Parse trufflehog JSON output (one JSON object per line) into DLPDetection[].
|
|
109
|
+
*
|
|
110
|
+
* Each line is an independent JSON object representing a single finding.
|
|
111
|
+
*/
|
|
112
|
+
private parseOutput(stdout: string, originalValue: string): DLPDetection[] {
|
|
113
|
+
if (!stdout || !stdout.trim()) {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const detections: DLPDetection[] = [];
|
|
118
|
+
const lines = stdout.trim().split('\n');
|
|
119
|
+
|
|
120
|
+
for (const line of lines) {
|
|
121
|
+
const trimmed = line.trim();
|
|
122
|
+
if (!trimmed) continue;
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const finding: TruffleHogFinding = JSON.parse(trimmed);
|
|
126
|
+
const detection = this.findingToDetection(finding, originalValue);
|
|
127
|
+
if (detection) {
|
|
128
|
+
detections.push(detection);
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
// Skip malformed JSON lines
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return detections;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Convert a single trufflehog finding to a DLPDetection.
|
|
141
|
+
*
|
|
142
|
+
* Severity mapping:
|
|
143
|
+
* - Verified findings -> 'high' (credential confirmed active)
|
|
144
|
+
* - Unverified findings -> 'medium' (potential secret)
|
|
145
|
+
*/
|
|
146
|
+
private findingToDetection(finding: TruffleHogFinding, originalValue: string): DLPDetection | null {
|
|
147
|
+
const detectorName = finding.DetectorName;
|
|
148
|
+
if (!detectorName) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const raw = finding.Raw || finding.RawV2 || '';
|
|
153
|
+
const severity: DLPSeverity = finding.Verified ? 'high' : 'medium';
|
|
154
|
+
|
|
155
|
+
// Locate the raw finding within the original string
|
|
156
|
+
let start = 0;
|
|
157
|
+
let end = 0;
|
|
158
|
+
if (raw) {
|
|
159
|
+
const idx = originalValue.indexOf(raw);
|
|
160
|
+
if (idx !== -1) {
|
|
161
|
+
start = idx;
|
|
162
|
+
end = idx + raw.length;
|
|
163
|
+
} else {
|
|
164
|
+
// If we can't locate the raw match, use the full string range
|
|
165
|
+
start = 0;
|
|
166
|
+
end = originalValue.length;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
pattern_name: `trufflehog:${detectorName}`,
|
|
172
|
+
severity,
|
|
173
|
+
match: raw || originalValue,
|
|
174
|
+
start,
|
|
175
|
+
end,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Check if the error is simply a "no findings" situation rather than a real failure.
|
|
181
|
+
*
|
|
182
|
+
* TruffleHog may exit with code 0 and empty output, or in some versions
|
|
183
|
+
* it may produce an error-like message when there are no findings.
|
|
184
|
+
*/
|
|
185
|
+
private isNoFindingsError(message: string): boolean {
|
|
186
|
+
// execFileSync throws if the process exits with non-zero or produces no output
|
|
187
|
+
// for an empty scan. Check for common benign patterns.
|
|
188
|
+
return message.includes('ENOENT') === false && message.includes('ETIMEDOUT') === false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import * as fs from 'fs/promises';
|
|
3
|
+
import { ToolCall } from '../types/tool-call';
|
|
4
|
+
import { ToolOutput } from '../types/tool-result';
|
|
5
|
+
import { ToolExecutor } from './interfaces';
|
|
6
|
+
import { FilesystemExecutorConfig } from '../types/config';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Filesystem executor for sandboxed file operations.
|
|
10
|
+
* Handles tool calls with tool name `file.*` (e.g., file.read, file.write).
|
|
11
|
+
* All paths are resolved relative to and contained within base_dir.
|
|
12
|
+
*/
|
|
13
|
+
export class FilesystemExecutor implements ToolExecutor {
|
|
14
|
+
private config: FilesystemExecutorConfig;
|
|
15
|
+
private resolvedBaseDir: string;
|
|
16
|
+
|
|
17
|
+
constructor(config: FilesystemExecutorConfig) {
|
|
18
|
+
this.config = config;
|
|
19
|
+
this.resolvedBaseDir = path.resolve(config.base_dir);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async execute(toolCall: ToolCall): Promise<ToolOutput> {
|
|
23
|
+
const action = this.resolveAction(toolCall);
|
|
24
|
+
|
|
25
|
+
switch (action) {
|
|
26
|
+
case 'read':
|
|
27
|
+
return this.read(toolCall);
|
|
28
|
+
case 'write':
|
|
29
|
+
return this.write(toolCall);
|
|
30
|
+
case 'delete':
|
|
31
|
+
return this.delete(toolCall);
|
|
32
|
+
case 'list':
|
|
33
|
+
return this.list(toolCall);
|
|
34
|
+
case 'stat':
|
|
35
|
+
return this.stat(toolCall);
|
|
36
|
+
default:
|
|
37
|
+
throw new Error(`Unsupported filesystem action: ${action}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private resolveAction(toolCall: ToolCall): string {
|
|
42
|
+
if (toolCall.args.action && typeof toolCall.args.action === 'string') {
|
|
43
|
+
return toolCall.args.action;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const toolName = toolCall.tool.name;
|
|
47
|
+
const dotIndex = toolName.indexOf('.');
|
|
48
|
+
if (dotIndex !== -1) {
|
|
49
|
+
return toolName.substring(dotIndex + 1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
throw new Error(`Unsupported filesystem action: ${toolName}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolve and validate a path, ensuring it stays within base_dir.
|
|
57
|
+
* Prevents path traversal attacks.
|
|
58
|
+
*/
|
|
59
|
+
private resolveSafePath(filePath: string): string {
|
|
60
|
+
const resolved = path.resolve(this.resolvedBaseDir, filePath);
|
|
61
|
+
if (!resolved.startsWith(this.resolvedBaseDir + path.sep) && resolved !== this.resolvedBaseDir) {
|
|
62
|
+
throw new Error(`Path traversal denied: "${filePath}" resolves outside base directory`);
|
|
63
|
+
}
|
|
64
|
+
return resolved;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private checkExtension(filePath: string): void {
|
|
68
|
+
const allowed = this.config.allowed_extensions;
|
|
69
|
+
if (allowed && allowed.length > 0) {
|
|
70
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
71
|
+
if (!allowed.includes(ext)) {
|
|
72
|
+
throw new Error(`File extension "${ext}" is not allowed. Allowed: ${allowed.join(', ')}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private async read(toolCall: ToolCall): Promise<ToolOutput> {
|
|
78
|
+
const { path: filePath, encoding } = toolCall.args;
|
|
79
|
+
|
|
80
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
81
|
+
throw new Error('Missing or invalid "path" argument for file.read');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const resolved = this.resolveSafePath(filePath);
|
|
85
|
+
this.checkExtension(resolved);
|
|
86
|
+
|
|
87
|
+
const stat = await fs.stat(resolved);
|
|
88
|
+
if (stat.size > this.config.max_file_size_bytes) {
|
|
89
|
+
throw new Error(`File size ${stat.size} exceeds max allowed ${this.config.max_file_size_bytes} bytes`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const content = await fs.readFile(resolved, {
|
|
93
|
+
encoding: (encoding as BufferEncoding) || 'utf-8',
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return { body: content, paths: [filePath] };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private async write(toolCall: ToolCall): Promise<ToolOutput> {
|
|
100
|
+
const { path: filePath, content, encoding, append } = toolCall.args;
|
|
101
|
+
|
|
102
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
103
|
+
throw new Error('Missing or invalid "path" argument for file.write');
|
|
104
|
+
}
|
|
105
|
+
if (content === undefined || content === null) {
|
|
106
|
+
throw new Error('Missing "content" argument for file.write');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const resolved = this.resolveSafePath(filePath);
|
|
110
|
+
this.checkExtension(resolved);
|
|
111
|
+
|
|
112
|
+
const data = typeof content === 'string' ? content : JSON.stringify(content);
|
|
113
|
+
|
|
114
|
+
if (Buffer.byteLength(data) > this.config.max_file_size_bytes) {
|
|
115
|
+
throw new Error(`Content size exceeds max allowed ${this.config.max_file_size_bytes} bytes`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Ensure parent directory exists
|
|
119
|
+
await fs.mkdir(path.dirname(resolved), { recursive: true });
|
|
120
|
+
|
|
121
|
+
if (append) {
|
|
122
|
+
await fs.appendFile(resolved, data, {
|
|
123
|
+
encoding: (encoding as BufferEncoding) || 'utf-8',
|
|
124
|
+
});
|
|
125
|
+
} else {
|
|
126
|
+
await fs.writeFile(resolved, data, {
|
|
127
|
+
encoding: (encoding as BufferEncoding) || 'utf-8',
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { body: { written: true }, paths: [filePath] };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private async delete(toolCall: ToolCall): Promise<ToolOutput> {
|
|
135
|
+
const { path: filePath, recursive } = toolCall.args;
|
|
136
|
+
|
|
137
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
138
|
+
throw new Error('Missing or invalid "path" argument for file.delete');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const resolved = this.resolveSafePath(filePath);
|
|
142
|
+
|
|
143
|
+
const stat = await fs.stat(resolved);
|
|
144
|
+
if (stat.isDirectory()) {
|
|
145
|
+
await fs.rm(resolved, { recursive: !!recursive });
|
|
146
|
+
} else {
|
|
147
|
+
await fs.unlink(resolved);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return { body: { deleted: true }, paths: [filePath] };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private async list(toolCall: ToolCall): Promise<ToolOutput> {
|
|
154
|
+
const { path: dirPath, pattern } = toolCall.args;
|
|
155
|
+
|
|
156
|
+
const targetPath = (dirPath && typeof dirPath === 'string') ? dirPath : '.';
|
|
157
|
+
const resolved = this.resolveSafePath(targetPath);
|
|
158
|
+
|
|
159
|
+
const entries = await fs.readdir(resolved, { withFileTypes: true });
|
|
160
|
+
|
|
161
|
+
let items = entries.map(entry => ({
|
|
162
|
+
name: entry.name,
|
|
163
|
+
type: entry.isDirectory() ? 'directory' : 'file',
|
|
164
|
+
}));
|
|
165
|
+
|
|
166
|
+
if (pattern && typeof pattern === 'string') {
|
|
167
|
+
const regex = new RegExp(pattern);
|
|
168
|
+
items = items.filter(item => regex.test(item.name));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return { body: items, paths: [targetPath] };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private async stat(toolCall: ToolCall): Promise<ToolOutput> {
|
|
175
|
+
const { path: filePath } = toolCall.args;
|
|
176
|
+
|
|
177
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
178
|
+
throw new Error('Missing or invalid "path" argument for file.stat');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const resolved = this.resolveSafePath(filePath);
|
|
182
|
+
const stat = await fs.stat(resolved);
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
body: {
|
|
186
|
+
size: stat.size,
|
|
187
|
+
is_file: stat.isFile(),
|
|
188
|
+
is_directory: stat.isDirectory(),
|
|
189
|
+
created: stat.birthtime.toISOString(),
|
|
190
|
+
modified: stat.mtime.toISOString(),
|
|
191
|
+
permissions: stat.mode.toString(8),
|
|
192
|
+
},
|
|
193
|
+
paths: [filePath],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|