palaryn 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +716 -0
- package/dist/sdk/typescript/src/client.d.ts +71 -0
- package/dist/sdk/typescript/src/client.d.ts.map +1 -0
- package/dist/sdk/typescript/src/client.js +176 -0
- package/dist/sdk/typescript/src/client.js.map +1 -0
- package/dist/sdk/typescript/src/errors.d.ts +50 -0
- package/dist/sdk/typescript/src/errors.d.ts.map +1 -0
- package/dist/sdk/typescript/src/errors.js +103 -0
- package/dist/sdk/typescript/src/errors.js.map +1 -0
- package/dist/sdk/typescript/src/index.d.ts +4 -0
- package/dist/sdk/typescript/src/index.d.ts.map +1 -0
- package/dist/sdk/typescript/src/index.js +15 -0
- package/dist/sdk/typescript/src/index.js.map +1 -0
- package/dist/sdk/typescript/src/types.d.ts +101 -0
- package/dist/sdk/typescript/src/types.d.ts.map +1 -0
- package/dist/sdk/typescript/src/types.js +6 -0
- package/dist/sdk/typescript/src/types.js.map +1 -0
- package/dist/src/admin/index.d.ts +2 -0
- package/dist/src/admin/index.d.ts.map +1 -0
- package/dist/src/admin/index.js +6 -0
- package/dist/src/admin/index.js.map +1 -0
- package/dist/src/admin/routes.d.ts +5 -0
- package/dist/src/admin/routes.d.ts.map +1 -0
- package/dist/src/admin/routes.js +471 -0
- package/dist/src/admin/routes.js.map +1 -0
- package/dist/src/admin/templates.d.ts +51 -0
- package/dist/src/admin/templates.d.ts.map +1 -0
- package/dist/src/admin/templates.js +500 -0
- package/dist/src/admin/templates.js.map +1 -0
- package/dist/src/anomaly/detector.d.ts +141 -0
- package/dist/src/anomaly/detector.d.ts.map +1 -0
- package/dist/src/anomaly/detector.js +554 -0
- package/dist/src/anomaly/detector.js.map +1 -0
- package/dist/src/anomaly/index.d.ts +2 -0
- package/dist/src/anomaly/index.d.ts.map +1 -0
- package/dist/src/anomaly/index.js +7 -0
- package/dist/src/anomaly/index.js.map +1 -0
- package/dist/src/approval/manager.d.ts +147 -0
- package/dist/src/approval/manager.d.ts.map +1 -0
- package/dist/src/approval/manager.js +511 -0
- package/dist/src/approval/manager.js.map +1 -0
- package/dist/src/approval/webhook.d.ts +36 -0
- package/dist/src/approval/webhook.d.ts.map +1 -0
- package/dist/src/approval/webhook.js +135 -0
- package/dist/src/approval/webhook.js.map +1 -0
- package/dist/src/audit/logger.d.ts +70 -0
- package/dist/src/audit/logger.d.ts.map +1 -0
- package/dist/src/audit/logger.js +440 -0
- package/dist/src/audit/logger.js.map +1 -0
- package/dist/src/auth/index.d.ts +6 -0
- package/dist/src/auth/index.d.ts.map +1 -0
- package/dist/src/auth/index.js +22 -0
- package/dist/src/auth/index.js.map +1 -0
- package/dist/src/auth/password.d.ts +3 -0
- package/dist/src/auth/password.d.ts.map +1 -0
- package/dist/src/auth/password.js +25 -0
- package/dist/src/auth/password.js.map +1 -0
- package/dist/src/auth/pkce.d.ts +13 -0
- package/dist/src/auth/pkce.d.ts.map +1 -0
- package/dist/src/auth/pkce.js +58 -0
- package/dist/src/auth/pkce.js.map +1 -0
- package/dist/src/auth/providers.d.ts +28 -0
- package/dist/src/auth/providers.d.ts.map +1 -0
- package/dist/src/auth/providers.js +198 -0
- package/dist/src/auth/providers.js.map +1 -0
- package/dist/src/auth/routes.d.ts +14 -0
- package/dist/src/auth/routes.d.ts.map +1 -0
- package/dist/src/auth/routes.js +431 -0
- package/dist/src/auth/routes.js.map +1 -0
- package/dist/src/auth/session.d.ts +24 -0
- package/dist/src/auth/session.d.ts.map +1 -0
- package/dist/src/auth/session.js +105 -0
- package/dist/src/auth/session.js.map +1 -0
- package/dist/src/billing/index.d.ts +7 -0
- package/dist/src/billing/index.d.ts.map +1 -0
- package/dist/src/billing/index.js +14 -0
- package/dist/src/billing/index.js.map +1 -0
- package/dist/src/billing/plan-enforcer.d.ts +44 -0
- package/dist/src/billing/plan-enforcer.d.ts.map +1 -0
- package/dist/src/billing/plan-enforcer.js +110 -0
- package/dist/src/billing/plan-enforcer.js.map +1 -0
- package/dist/src/billing/routes.d.ts +15 -0
- package/dist/src/billing/routes.d.ts.map +1 -0
- package/dist/src/billing/routes.js +193 -0
- package/dist/src/billing/routes.js.map +1 -0
- package/dist/src/billing/stripe-client.d.ts +14 -0
- package/dist/src/billing/stripe-client.d.ts.map +1 -0
- package/dist/src/billing/stripe-client.js +51 -0
- package/dist/src/billing/stripe-client.js.map +1 -0
- package/dist/src/billing/webhook-handler.d.ts +19 -0
- package/dist/src/billing/webhook-handler.d.ts.map +1 -0
- package/dist/src/billing/webhook-handler.js +169 -0
- package/dist/src/billing/webhook-handler.js.map +1 -0
- package/dist/src/billing/webhook-routes.d.ts +5 -0
- package/dist/src/billing/webhook-routes.d.ts.map +1 -0
- package/dist/src/billing/webhook-routes.js +30 -0
- package/dist/src/billing/webhook-routes.js.map +1 -0
- package/dist/src/budget/manager.d.ts +95 -0
- package/dist/src/budget/manager.d.ts.map +1 -0
- package/dist/src/budget/manager.js +547 -0
- package/dist/src/budget/manager.js.map +1 -0
- package/dist/src/budget/usage-extractor.d.ts +38 -0
- package/dist/src/budget/usage-extractor.d.ts.map +1 -0
- package/dist/src/budget/usage-extractor.js +165 -0
- package/dist/src/budget/usage-extractor.js.map +1 -0
- package/dist/src/cli.d.ts +3 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/cli.js +115 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/config/defaults.d.ts +3 -0
- package/dist/src/config/defaults.d.ts.map +1 -0
- package/dist/src/config/defaults.js +243 -0
- package/dist/src/config/defaults.js.map +1 -0
- package/dist/src/config/validate.d.ts +15 -0
- package/dist/src/config/validate.d.ts.map +1 -0
- package/dist/src/config/validate.js +105 -0
- package/dist/src/config/validate.js.map +1 -0
- package/dist/src/dlp/composite-scanner.d.ts +47 -0
- package/dist/src/dlp/composite-scanner.d.ts.map +1 -0
- package/dist/src/dlp/composite-scanner.js +186 -0
- package/dist/src/dlp/composite-scanner.js.map +1 -0
- package/dist/src/dlp/index.d.ts +10 -0
- package/dist/src/dlp/index.d.ts.map +1 -0
- package/dist/src/dlp/index.js +26 -0
- package/dist/src/dlp/index.js.map +1 -0
- package/dist/src/dlp/interfaces.d.ts +33 -0
- package/dist/src/dlp/interfaces.d.ts.map +1 -0
- package/dist/src/dlp/interfaces.js +3 -0
- package/dist/src/dlp/interfaces.js.map +1 -0
- package/dist/src/dlp/patterns.d.ts +9 -0
- package/dist/src/dlp/patterns.d.ts.map +1 -0
- package/dist/src/dlp/patterns.js +25 -0
- package/dist/src/dlp/patterns.js.map +1 -0
- package/dist/src/dlp/prompt-injection-backend.d.ts +68 -0
- package/dist/src/dlp/prompt-injection-backend.d.ts.map +1 -0
- package/dist/src/dlp/prompt-injection-backend.js +148 -0
- package/dist/src/dlp/prompt-injection-backend.js.map +1 -0
- package/dist/src/dlp/prompt-injection-patterns.d.ts +32 -0
- package/dist/src/dlp/prompt-injection-patterns.d.ts.map +1 -0
- package/dist/src/dlp/prompt-injection-patterns.js +290 -0
- package/dist/src/dlp/prompt-injection-patterns.js.map +1 -0
- package/dist/src/dlp/regex-backend.d.ts +32 -0
- package/dist/src/dlp/regex-backend.d.ts.map +1 -0
- package/dist/src/dlp/regex-backend.js +153 -0
- package/dist/src/dlp/regex-backend.js.map +1 -0
- package/dist/src/dlp/scanner.d.ts +122 -0
- package/dist/src/dlp/scanner.d.ts.map +1 -0
- package/dist/src/dlp/scanner.js +444 -0
- package/dist/src/dlp/scanner.js.map +1 -0
- package/dist/src/dlp/text-normalizer.d.ts +41 -0
- package/dist/src/dlp/text-normalizer.d.ts.map +1 -0
- package/dist/src/dlp/text-normalizer.js +203 -0
- package/dist/src/dlp/text-normalizer.js.map +1 -0
- package/dist/src/dlp/trufflehog-backend.d.ts +64 -0
- package/dist/src/dlp/trufflehog-backend.d.ts.map +1 -0
- package/dist/src/dlp/trufflehog-backend.js +151 -0
- package/dist/src/dlp/trufflehog-backend.js.map +1 -0
- package/dist/src/executor/http-executor.d.ts +25 -0
- package/dist/src/executor/http-executor.d.ts.map +1 -0
- package/dist/src/executor/http-executor.js +333 -0
- package/dist/src/executor/http-executor.js.map +1 -0
- package/dist/src/executor/index.d.ts +6 -0
- package/dist/src/executor/index.d.ts.map +1 -0
- package/dist/src/executor/index.js +12 -0
- package/dist/src/executor/index.js.map +1 -0
- package/dist/src/executor/interfaces.d.ts +11 -0
- package/dist/src/executor/interfaces.d.ts.map +1 -0
- package/dist/src/executor/interfaces.js +3 -0
- package/dist/src/executor/interfaces.js.map +1 -0
- package/dist/src/executor/noop-executor.d.ts +13 -0
- package/dist/src/executor/noop-executor.d.ts.map +1 -0
- package/dist/src/executor/noop-executor.js +21 -0
- package/dist/src/executor/noop-executor.js.map +1 -0
- package/dist/src/executor/registry.d.ts +30 -0
- package/dist/src/executor/registry.d.ts.map +1 -0
- package/dist/src/executor/registry.js +62 -0
- package/dist/src/executor/registry.js.map +1 -0
- package/dist/src/executor/slack-executor.d.ts +24 -0
- package/dist/src/executor/slack-executor.d.ts.map +1 -0
- package/dist/src/executor/slack-executor.js +147 -0
- package/dist/src/executor/slack-executor.js.map +1 -0
- package/dist/src/index.d.ts +25 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +74 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/mcp/auth-verifier.d.ts +23 -0
- package/dist/src/mcp/auth-verifier.d.ts.map +1 -0
- package/dist/src/mcp/auth-verifier.js +162 -0
- package/dist/src/mcp/auth-verifier.js.map +1 -0
- package/dist/src/mcp/bridge.d.ts +132 -0
- package/dist/src/mcp/bridge.d.ts.map +1 -0
- package/dist/src/mcp/bridge.js +734 -0
- package/dist/src/mcp/bridge.js.map +1 -0
- package/dist/src/mcp/http-transport.d.ts +32 -0
- package/dist/src/mcp/http-transport.d.ts.map +1 -0
- package/dist/src/mcp/http-transport.js +538 -0
- package/dist/src/mcp/http-transport.js.map +1 -0
- package/dist/src/mcp/index.d.ts +10 -0
- package/dist/src/mcp/index.d.ts.map +1 -0
- package/dist/src/mcp/index.js +17 -0
- package/dist/src/mcp/index.js.map +1 -0
- package/dist/src/mcp/oauth-pages.d.ts +23 -0
- package/dist/src/mcp/oauth-pages.d.ts.map +1 -0
- package/dist/src/mcp/oauth-pages.js +121 -0
- package/dist/src/mcp/oauth-pages.js.map +1 -0
- package/dist/src/mcp/oauth-postgres-stores.d.ts +55 -0
- package/dist/src/mcp/oauth-postgres-stores.d.ts.map +1 -0
- package/dist/src/mcp/oauth-postgres-stores.js +226 -0
- package/dist/src/mcp/oauth-postgres-stores.js.map +1 -0
- package/dist/src/mcp/oauth-provider.d.ts +95 -0
- package/dist/src/mcp/oauth-provider.d.ts.map +1 -0
- package/dist/src/mcp/oauth-provider.js +360 -0
- package/dist/src/mcp/oauth-provider.js.map +1 -0
- package/dist/src/mcp/oauth-stores.d.ts +62 -0
- package/dist/src/mcp/oauth-stores.d.ts.map +1 -0
- package/dist/src/mcp/oauth-stores.js +154 -0
- package/dist/src/mcp/oauth-stores.js.map +1 -0
- package/dist/src/mcp/server.d.ts +18 -0
- package/dist/src/mcp/server.d.ts.map +1 -0
- package/dist/src/mcp/server.js +51 -0
- package/dist/src/mcp/server.js.map +1 -0
- package/dist/src/metrics/collector.d.ts +106 -0
- package/dist/src/metrics/collector.d.ts.map +1 -0
- package/dist/src/metrics/collector.js +311 -0
- package/dist/src/metrics/collector.js.map +1 -0
- package/dist/src/metrics/index.d.ts +2 -0
- package/dist/src/metrics/index.d.ts.map +1 -0
- package/dist/src/metrics/index.js +6 -0
- package/dist/src/metrics/index.js.map +1 -0
- package/dist/src/middleware/auth.d.ts +77 -0
- package/dist/src/middleware/auth.d.ts.map +1 -0
- package/dist/src/middleware/auth.js +720 -0
- package/dist/src/middleware/auth.js.map +1 -0
- package/dist/src/middleware/session.d.ts +18 -0
- package/dist/src/middleware/session.d.ts.map +1 -0
- package/dist/src/middleware/session.js +67 -0
- package/dist/src/middleware/session.js.map +1 -0
- package/dist/src/middleware/validate.d.ts +3 -0
- package/dist/src/middleware/validate.d.ts.map +1 -0
- package/dist/src/middleware/validate.js +85 -0
- package/dist/src/middleware/validate.js.map +1 -0
- package/dist/src/policy/engine.d.ts +107 -0
- package/dist/src/policy/engine.d.ts.map +1 -0
- package/dist/src/policy/engine.js +646 -0
- package/dist/src/policy/engine.js.map +1 -0
- package/dist/src/policy/index.d.ts +3 -0
- package/dist/src/policy/index.d.ts.map +1 -0
- package/dist/src/policy/index.js +8 -0
- package/dist/src/policy/index.js.map +1 -0
- package/dist/src/policy/opa-engine.d.ts +176 -0
- package/dist/src/policy/opa-engine.d.ts.map +1 -0
- package/dist/src/policy/opa-engine.js +790 -0
- package/dist/src/policy/opa-engine.js.map +1 -0
- package/dist/src/proxy/forward-proxy.d.ts +30 -0
- package/dist/src/proxy/forward-proxy.d.ts.map +1 -0
- package/dist/src/proxy/forward-proxy.js +580 -0
- package/dist/src/proxy/forward-proxy.js.map +1 -0
- package/dist/src/proxy/index.d.ts +2 -0
- package/dist/src/proxy/index.d.ts.map +1 -0
- package/dist/src/proxy/index.js +8 -0
- package/dist/src/proxy/index.js.map +1 -0
- package/dist/src/ratelimit/limiter.d.ts +45 -0
- package/dist/src/ratelimit/limiter.d.ts.map +1 -0
- package/dist/src/ratelimit/limiter.js +158 -0
- package/dist/src/ratelimit/limiter.js.map +1 -0
- package/dist/src/replay/engine.d.ts +40 -0
- package/dist/src/replay/engine.d.ts.map +1 -0
- package/dist/src/replay/engine.js +106 -0
- package/dist/src/replay/engine.js.map +1 -0
- package/dist/src/replay/index.d.ts +2 -0
- package/dist/src/replay/index.d.ts.map +1 -0
- package/dist/src/replay/index.js +6 -0
- package/dist/src/replay/index.js.map +1 -0
- package/dist/src/saas/index.d.ts +2 -0
- package/dist/src/saas/index.d.ts.map +1 -0
- package/dist/src/saas/index.js +18 -0
- package/dist/src/saas/index.js.map +1 -0
- package/dist/src/saas/routes.d.ts +18 -0
- package/dist/src/saas/routes.d.ts.map +1 -0
- package/dist/src/saas/routes.js +1566 -0
- package/dist/src/saas/routes.js.map +1 -0
- package/dist/src/server/app.d.ts +44 -0
- package/dist/src/server/app.d.ts.map +1 -0
- package/dist/src/server/app.js +854 -0
- package/dist/src/server/app.js.map +1 -0
- package/dist/src/server/errors.d.ts +32 -0
- package/dist/src/server/errors.d.ts.map +1 -0
- package/dist/src/server/errors.js +39 -0
- package/dist/src/server/errors.js.map +1 -0
- package/dist/src/server/gateway.d.ts +165 -0
- package/dist/src/server/gateway.d.ts.map +1 -0
- package/dist/src/server/gateway.js +964 -0
- package/dist/src/server/gateway.js.map +1 -0
- package/dist/src/server/index.d.ts +2 -0
- package/dist/src/server/index.d.ts.map +1 -0
- package/dist/src/server/index.js +295 -0
- package/dist/src/server/index.js.map +1 -0
- package/dist/src/server/logger.d.ts +33 -0
- package/dist/src/server/logger.d.ts.map +1 -0
- package/dist/src/server/logger.js +230 -0
- package/dist/src/server/logger.js.map +1 -0
- package/dist/src/server/stream-proxy.d.ts +32 -0
- package/dist/src/server/stream-proxy.d.ts.map +1 -0
- package/dist/src/server/stream-proxy.js +184 -0
- package/dist/src/server/stream-proxy.js.map +1 -0
- package/dist/src/storage/file-persistence.d.ts +48 -0
- package/dist/src/storage/file-persistence.d.ts.map +1 -0
- package/dist/src/storage/file-persistence.js +280 -0
- package/dist/src/storage/file-persistence.js.map +1 -0
- package/dist/src/storage/index.d.ts +5 -0
- package/dist/src/storage/index.d.ts.map +1 -0
- package/dist/src/storage/index.js +21 -0
- package/dist/src/storage/index.js.map +1 -0
- package/dist/src/storage/interfaces.d.ts +237 -0
- package/dist/src/storage/interfaces.d.ts.map +1 -0
- package/dist/src/storage/interfaces.js +3 -0
- package/dist/src/storage/interfaces.js.map +1 -0
- package/dist/src/storage/memory.d.ts +162 -0
- package/dist/src/storage/memory.d.ts.map +1 -0
- package/dist/src/storage/memory.js +603 -0
- package/dist/src/storage/memory.js.map +1 -0
- package/dist/src/storage/postgres.d.ts +267 -0
- package/dist/src/storage/postgres.d.ts.map +1 -0
- package/dist/src/storage/postgres.js +1555 -0
- package/dist/src/storage/postgres.js.map +1 -0
- package/dist/src/storage/redis.d.ts +202 -0
- package/dist/src/storage/redis.d.ts.map +1 -0
- package/dist/src/storage/redis.js +629 -0
- package/dist/src/storage/redis.js.map +1 -0
- package/dist/src/tracing/index.d.ts +2 -0
- package/dist/src/tracing/index.d.ts.map +1 -0
- package/dist/src/tracing/index.js +6 -0
- package/dist/src/tracing/index.js.map +1 -0
- package/dist/src/tracing/provider.d.ts +43 -0
- package/dist/src/tracing/provider.d.ts.map +1 -0
- package/dist/src/tracing/provider.js +74 -0
- package/dist/src/tracing/provider.js.map +1 -0
- package/dist/src/trust/calculator.d.ts +54 -0
- package/dist/src/trust/calculator.d.ts.map +1 -0
- package/dist/src/trust/calculator.js +102 -0
- package/dist/src/trust/calculator.js.map +1 -0
- package/dist/src/trust/index.d.ts +2 -0
- package/dist/src/trust/index.d.ts.map +1 -0
- package/dist/src/trust/index.js +7 -0
- package/dist/src/trust/index.js.map +1 -0
- package/dist/src/types/budget.d.ts +30 -0
- package/dist/src/types/budget.d.ts.map +1 -0
- package/dist/src/types/budget.js +3 -0
- package/dist/src/types/budget.js.map +1 -0
- package/dist/src/types/config.d.ts +176 -0
- package/dist/src/types/config.d.ts.map +1 -0
- package/dist/src/types/config.js +3 -0
- package/dist/src/types/config.js.map +1 -0
- package/dist/src/types/events.d.ts +24 -0
- package/dist/src/types/events.d.ts.map +1 -0
- package/dist/src/types/events.js +3 -0
- package/dist/src/types/events.js.map +1 -0
- package/dist/src/types/index.d.ts +8 -0
- package/dist/src/types/index.d.ts.map +1 -0
- package/dist/src/types/index.js +24 -0
- package/dist/src/types/index.js.map +1 -0
- package/dist/src/types/policy.d.ts +60 -0
- package/dist/src/types/policy.d.ts.map +1 -0
- package/dist/src/types/policy.js +3 -0
- package/dist/src/types/policy.js.map +1 -0
- package/dist/src/types/stripe-config.d.ts +12 -0
- package/dist/src/types/stripe-config.d.ts.map +1 -0
- package/dist/src/types/stripe-config.js +3 -0
- package/dist/src/types/stripe-config.js.map +1 -0
- package/dist/src/types/subscription.d.ts +24 -0
- package/dist/src/types/subscription.d.ts.map +1 -0
- package/dist/src/types/subscription.js +38 -0
- package/dist/src/types/subscription.js.map +1 -0
- package/dist/src/types/tool-call.d.ts +42 -0
- package/dist/src/types/tool-call.d.ts.map +1 -0
- package/dist/src/types/tool-call.js +3 -0
- package/dist/src/types/tool-call.js.map +1 -0
- package/dist/src/types/tool-result.d.ts +58 -0
- package/dist/src/types/tool-result.d.ts.map +1 -0
- package/dist/src/types/tool-result.js +3 -0
- package/dist/src/types/tool-result.js.map +1 -0
- package/dist/src/types/user.d.ts +101 -0
- package/dist/src/types/user.d.ts.map +1 -0
- package/dist/src/types/user.js +6 -0
- package/dist/src/types/user.js.map +1 -0
- package/dist/tests/integration/api.test.d.ts +2 -0
- package/dist/tests/integration/api.test.d.ts.map +1 -0
- package/dist/tests/integration/api.test.js +1199 -0
- package/dist/tests/integration/api.test.js.map +1 -0
- package/dist/tests/integration/proxy.test.d.ts +2 -0
- package/dist/tests/integration/proxy.test.d.ts.map +1 -0
- package/dist/tests/integration/proxy.test.js +251 -0
- package/dist/tests/integration/proxy.test.js.map +1 -0
- package/dist/tests/integration/storage.test.d.ts +16 -0
- package/dist/tests/integration/storage.test.d.ts.map +1 -0
- package/dist/tests/integration/storage.test.js +826 -0
- package/dist/tests/integration/storage.test.js.map +1 -0
- package/dist/tests/unit/admin.test.d.ts +2 -0
- package/dist/tests/unit/admin.test.d.ts.map +1 -0
- package/dist/tests/unit/admin.test.js +698 -0
- package/dist/tests/unit/admin.test.js.map +1 -0
- package/dist/tests/unit/anomaly-detector.test.d.ts +2 -0
- package/dist/tests/unit/anomaly-detector.test.d.ts.map +1 -0
- package/dist/tests/unit/anomaly-detector.test.js +903 -0
- package/dist/tests/unit/anomaly-detector.test.js.map +1 -0
- package/dist/tests/unit/approval-manager.test.d.ts +2 -0
- package/dist/tests/unit/approval-manager.test.d.ts.map +1 -0
- package/dist/tests/unit/approval-manager.test.js +528 -0
- package/dist/tests/unit/approval-manager.test.js.map +1 -0
- package/dist/tests/unit/approval-webhook.test.d.ts +2 -0
- package/dist/tests/unit/approval-webhook.test.d.ts.map +1 -0
- package/dist/tests/unit/approval-webhook.test.js +355 -0
- package/dist/tests/unit/approval-webhook.test.js.map +1 -0
- package/dist/tests/unit/audit-logger.test.d.ts +2 -0
- package/dist/tests/unit/audit-logger.test.d.ts.map +1 -0
- package/dist/tests/unit/audit-logger.test.js +635 -0
- package/dist/tests/unit/audit-logger.test.js.map +1 -0
- package/dist/tests/unit/auth-routes.test.d.ts +2 -0
- package/dist/tests/unit/auth-routes.test.d.ts.map +1 -0
- package/dist/tests/unit/auth-routes.test.js +281 -0
- package/dist/tests/unit/auth-routes.test.js.map +1 -0
- package/dist/tests/unit/auth.test.d.ts +2 -0
- package/dist/tests/unit/auth.test.d.ts.map +1 -0
- package/dist/tests/unit/auth.test.js +1382 -0
- package/dist/tests/unit/auth.test.js.map +1 -0
- package/dist/tests/unit/billing.test.d.ts +2 -0
- package/dist/tests/unit/billing.test.d.ts.map +1 -0
- package/dist/tests/unit/billing.test.js +579 -0
- package/dist/tests/unit/billing.test.js.map +1 -0
- package/dist/tests/unit/budget-manager.test.d.ts +2 -0
- package/dist/tests/unit/budget-manager.test.d.ts.map +1 -0
- package/dist/tests/unit/budget-manager.test.js +778 -0
- package/dist/tests/unit/budget-manager.test.js.map +1 -0
- package/dist/tests/unit/budget-race.test.d.ts +2 -0
- package/dist/tests/unit/budget-race.test.d.ts.map +1 -0
- package/dist/tests/unit/budget-race.test.js +58 -0
- package/dist/tests/unit/budget-race.test.js.map +1 -0
- package/dist/tests/unit/cli.test.d.ts +2 -0
- package/dist/tests/unit/cli.test.d.ts.map +1 -0
- package/dist/tests/unit/cli.test.js +93 -0
- package/dist/tests/unit/cli.test.js.map +1 -0
- package/dist/tests/unit/concurrency.test.d.ts +2 -0
- package/dist/tests/unit/concurrency.test.d.ts.map +1 -0
- package/dist/tests/unit/concurrency.test.js +1270 -0
- package/dist/tests/unit/concurrency.test.js.map +1 -0
- package/dist/tests/unit/config-validate.test.d.ts +2 -0
- package/dist/tests/unit/config-validate.test.d.ts.map +1 -0
- package/dist/tests/unit/config-validate.test.js +230 -0
- package/dist/tests/unit/config-validate.test.js.map +1 -0
- package/dist/tests/unit/defaults.test.d.ts +2 -0
- package/dist/tests/unit/defaults.test.d.ts.map +1 -0
- package/dist/tests/unit/defaults.test.js +364 -0
- package/dist/tests/unit/defaults.test.js.map +1 -0
- package/dist/tests/unit/dlp-backends.test.d.ts +2 -0
- package/dist/tests/unit/dlp-backends.test.d.ts.map +1 -0
- package/dist/tests/unit/dlp-backends.test.js +563 -0
- package/dist/tests/unit/dlp-backends.test.js.map +1 -0
- package/dist/tests/unit/dlp-scanner.test.d.ts +2 -0
- package/dist/tests/unit/dlp-scanner.test.d.ts.map +1 -0
- package/dist/tests/unit/dlp-scanner.test.js +739 -0
- package/dist/tests/unit/dlp-scanner.test.js.map +1 -0
- package/dist/tests/unit/error-responses.test.d.ts +2 -0
- package/dist/tests/unit/error-responses.test.d.ts.map +1 -0
- package/dist/tests/unit/error-responses.test.js +101 -0
- package/dist/tests/unit/error-responses.test.js.map +1 -0
- package/dist/tests/unit/executor-registry.test.d.ts +2 -0
- package/dist/tests/unit/executor-registry.test.d.ts.map +1 -0
- package/dist/tests/unit/executor-registry.test.js +390 -0
- package/dist/tests/unit/executor-registry.test.js.map +1 -0
- package/dist/tests/unit/forward-proxy.test.d.ts +2 -0
- package/dist/tests/unit/forward-proxy.test.d.ts.map +1 -0
- package/dist/tests/unit/forward-proxy.test.js +621 -0
- package/dist/tests/unit/forward-proxy.test.js.map +1 -0
- package/dist/tests/unit/gateway-features.test.d.ts +2 -0
- package/dist/tests/unit/gateway-features.test.d.ts.map +1 -0
- package/dist/tests/unit/gateway-features.test.js +753 -0
- package/dist/tests/unit/gateway-features.test.js.map +1 -0
- package/dist/tests/unit/http-executor.test.d.ts +2 -0
- package/dist/tests/unit/http-executor.test.d.ts.map +1 -0
- package/dist/tests/unit/http-executor.test.js +310 -0
- package/dist/tests/unit/http-executor.test.js.map +1 -0
- package/dist/tests/unit/mcp-bridge.test.d.ts +2 -0
- package/dist/tests/unit/mcp-bridge.test.d.ts.map +1 -0
- package/dist/tests/unit/mcp-bridge.test.js +1136 -0
- package/dist/tests/unit/mcp-bridge.test.js.map +1 -0
- package/dist/tests/unit/mcp-http-transport.test.d.ts +2 -0
- package/dist/tests/unit/mcp-http-transport.test.d.ts.map +1 -0
- package/dist/tests/unit/mcp-http-transport.test.js +899 -0
- package/dist/tests/unit/mcp-http-transport.test.js.map +1 -0
- package/dist/tests/unit/mcp-oauth.test.d.ts +2 -0
- package/dist/tests/unit/mcp-oauth.test.d.ts.map +1 -0
- package/dist/tests/unit/mcp-oauth.test.js +759 -0
- package/dist/tests/unit/mcp-oauth.test.js.map +1 -0
- package/dist/tests/unit/mcp-server.test.d.ts +15 -0
- package/dist/tests/unit/mcp-server.test.d.ts.map +1 -0
- package/dist/tests/unit/mcp-server.test.js +158 -0
- package/dist/tests/unit/mcp-server.test.js.map +1 -0
- package/dist/tests/unit/metrics.test.d.ts +2 -0
- package/dist/tests/unit/metrics.test.d.ts.map +1 -0
- package/dist/tests/unit/metrics.test.js +208 -0
- package/dist/tests/unit/metrics.test.js.map +1 -0
- package/dist/tests/unit/oauth.test.d.ts +2 -0
- package/dist/tests/unit/oauth.test.d.ts.map +1 -0
- package/dist/tests/unit/oauth.test.js +281 -0
- package/dist/tests/unit/oauth.test.js.map +1 -0
- package/dist/tests/unit/opa-circuit-breaker.test.d.ts +2 -0
- package/dist/tests/unit/opa-circuit-breaker.test.d.ts.map +1 -0
- package/dist/tests/unit/opa-circuit-breaker.test.js +297 -0
- package/dist/tests/unit/opa-circuit-breaker.test.js.map +1 -0
- package/dist/tests/unit/opa-engine.test.d.ts +2 -0
- package/dist/tests/unit/opa-engine.test.d.ts.map +1 -0
- package/dist/tests/unit/opa-engine.test.js +1813 -0
- package/dist/tests/unit/opa-engine.test.js.map +1 -0
- package/dist/tests/unit/pipeline-timing.test.d.ts +2 -0
- package/dist/tests/unit/pipeline-timing.test.d.ts.map +1 -0
- package/dist/tests/unit/pipeline-timing.test.js +528 -0
- package/dist/tests/unit/pipeline-timing.test.js.map +1 -0
- package/dist/tests/unit/policy-engine.test.d.ts +2 -0
- package/dist/tests/unit/policy-engine.test.d.ts.map +1 -0
- package/dist/tests/unit/policy-engine.test.js +1345 -0
- package/dist/tests/unit/policy-engine.test.js.map +1 -0
- package/dist/tests/unit/policy-store.test.d.ts +2 -0
- package/dist/tests/unit/policy-store.test.d.ts.map +1 -0
- package/dist/tests/unit/policy-store.test.js +60 -0
- package/dist/tests/unit/policy-store.test.js.map +1 -0
- package/dist/tests/unit/postgres-storage.test.d.ts +2 -0
- package/dist/tests/unit/postgres-storage.test.d.ts.map +1 -0
- package/dist/tests/unit/postgres-storage.test.js +614 -0
- package/dist/tests/unit/postgres-storage.test.js.map +1 -0
- package/dist/tests/unit/prompt-injection-backend.test.d.ts +2 -0
- package/dist/tests/unit/prompt-injection-backend.test.d.ts.map +1 -0
- package/dist/tests/unit/prompt-injection-backend.test.js +621 -0
- package/dist/tests/unit/prompt-injection-backend.test.js.map +1 -0
- package/dist/tests/unit/proxy-hardening.test.d.ts +2 -0
- package/dist/tests/unit/proxy-hardening.test.d.ts.map +1 -0
- package/dist/tests/unit/proxy-hardening.test.js +166 -0
- package/dist/tests/unit/proxy-hardening.test.js.map +1 -0
- package/dist/tests/unit/rate-limiter.test.d.ts +2 -0
- package/dist/tests/unit/rate-limiter.test.d.ts.map +1 -0
- package/dist/tests/unit/rate-limiter.test.js +443 -0
- package/dist/tests/unit/rate-limiter.test.js.map +1 -0
- package/dist/tests/unit/redis-storage.test.d.ts +2 -0
- package/dist/tests/unit/redis-storage.test.d.ts.map +1 -0
- package/dist/tests/unit/redis-storage.test.js +766 -0
- package/dist/tests/unit/redis-storage.test.js.map +1 -0
- package/dist/tests/unit/replay-engine.test.d.ts +2 -0
- package/dist/tests/unit/replay-engine.test.d.ts.map +1 -0
- package/dist/tests/unit/replay-engine.test.js +371 -0
- package/dist/tests/unit/replay-engine.test.js.map +1 -0
- package/dist/tests/unit/saas-routes.test.d.ts +2 -0
- package/dist/tests/unit/saas-routes.test.d.ts.map +1 -0
- package/dist/tests/unit/saas-routes.test.js +1399 -0
- package/dist/tests/unit/saas-routes.test.js.map +1 -0
- package/dist/tests/unit/session.test.d.ts +2 -0
- package/dist/tests/unit/session.test.d.ts.map +1 -0
- package/dist/tests/unit/session.test.js +532 -0
- package/dist/tests/unit/session.test.js.map +1 -0
- package/dist/tests/unit/slack-executor.test.d.ts +2 -0
- package/dist/tests/unit/slack-executor.test.d.ts.map +1 -0
- package/dist/tests/unit/slack-executor.test.js +209 -0
- package/dist/tests/unit/slack-executor.test.js.map +1 -0
- package/dist/tests/unit/storage-hardening.test.d.ts +2 -0
- package/dist/tests/unit/storage-hardening.test.d.ts.map +1 -0
- package/dist/tests/unit/storage-hardening.test.js +165 -0
- package/dist/tests/unit/storage-hardening.test.js.map +1 -0
- package/dist/tests/unit/storage.test.d.ts +2 -0
- package/dist/tests/unit/storage.test.d.ts.map +1 -0
- package/dist/tests/unit/storage.test.js +698 -0
- package/dist/tests/unit/storage.test.js.map +1 -0
- package/dist/tests/unit/text-normalizer.test.d.ts +2 -0
- package/dist/tests/unit/text-normalizer.test.d.ts.map +1 -0
- package/dist/tests/unit/text-normalizer.test.js +229 -0
- package/dist/tests/unit/text-normalizer.test.js.map +1 -0
- package/dist/tests/unit/tracing.test.d.ts +2 -0
- package/dist/tests/unit/tracing.test.d.ts.map +1 -0
- package/dist/tests/unit/tracing.test.js +611 -0
- package/dist/tests/unit/tracing.test.js.map +1 -0
- package/dist/tests/unit/trust-calculator.test.d.ts +2 -0
- package/dist/tests/unit/trust-calculator.test.d.ts.map +1 -0
- package/dist/tests/unit/trust-calculator.test.js +497 -0
- package/dist/tests/unit/trust-calculator.test.js.map +1 -0
- package/dist/tests/unit/ts-sdk.test.d.ts +2 -0
- package/dist/tests/unit/ts-sdk.test.d.ts.map +1 -0
- package/dist/tests/unit/ts-sdk.test.js +421 -0
- package/dist/tests/unit/ts-sdk.test.js.map +1 -0
- package/dist/tests/unit/usage-extractor-llm.test.d.ts +2 -0
- package/dist/tests/unit/usage-extractor-llm.test.d.ts.map +1 -0
- package/dist/tests/unit/usage-extractor-llm.test.js +139 -0
- package/dist/tests/unit/usage-extractor-llm.test.js.map +1 -0
- package/dist/tests/unit/usage-extractor.test.d.ts +2 -0
- package/dist/tests/unit/usage-extractor.test.d.ts.map +1 -0
- package/dist/tests/unit/usage-extractor.test.js +271 -0
- package/dist/tests/unit/usage-extractor.test.js.map +1 -0
- package/dist/tests/unit/user-stores.test.d.ts +2 -0
- package/dist/tests/unit/user-stores.test.d.ts.map +1 -0
- package/dist/tests/unit/user-stores.test.js +687 -0
- package/dist/tests/unit/user-stores.test.js.map +1 -0
- package/dist/tests/unit/validate.test.d.ts +2 -0
- package/dist/tests/unit/validate.test.d.ts.map +1 -0
- package/dist/tests/unit/validate.test.js +545 -0
- package/dist/tests/unit/validate.test.js.map +1 -0
- package/package.json +86 -0
- package/policy-packs/README.md +42 -0
- package/policy-packs/default.yaml +46 -0
- package/policy-packs/dev_fast.yaml +54 -0
- package/policy-packs/prod_strict.yaml +83 -0
|
@@ -0,0 +1,903 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const anomaly_1 = require("../../src/anomaly");
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Helpers
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
/** Build a minimal valid ToolCall with configurable fields. */
|
|
8
|
+
function buildToolCall(overrides = {}) {
|
|
9
|
+
return {
|
|
10
|
+
tool_call_id: `tc-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
11
|
+
task_id: 'task-001',
|
|
12
|
+
workspace_id: overrides.workspaceId ?? 'ws-default',
|
|
13
|
+
actor: {
|
|
14
|
+
type: 'agent',
|
|
15
|
+
id: overrides.actorId ?? 'agent-1',
|
|
16
|
+
},
|
|
17
|
+
source: {
|
|
18
|
+
platform: 'test',
|
|
19
|
+
},
|
|
20
|
+
tool: {
|
|
21
|
+
name: overrides.toolName ?? 'http.request',
|
|
22
|
+
capability: overrides.capability ?? 'read',
|
|
23
|
+
},
|
|
24
|
+
args: {
|
|
25
|
+
method: 'GET',
|
|
26
|
+
url: 'https://api.example.com/data',
|
|
27
|
+
},
|
|
28
|
+
timestamp: new Date().toISOString(),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Build a default AnomalyConfig with optional overrides. */
|
|
32
|
+
function defaultConfig(overrides) {
|
|
33
|
+
return {
|
|
34
|
+
enabled: true,
|
|
35
|
+
window_ms: 3600000,
|
|
36
|
+
z_score_threshold: 3,
|
|
37
|
+
min_samples: 10,
|
|
38
|
+
action: 'flag',
|
|
39
|
+
track_actors: true,
|
|
40
|
+
track_tools: true,
|
|
41
|
+
track_workspaces: true,
|
|
42
|
+
...overrides,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
// ===========================================================================
|
|
46
|
+
// Tests
|
|
47
|
+
// ===========================================================================
|
|
48
|
+
describe('AnomalyDetector', () => {
|
|
49
|
+
// -----------------------------------------------------------------------
|
|
50
|
+
// Disabled
|
|
51
|
+
// -----------------------------------------------------------------------
|
|
52
|
+
describe('when anomaly detection is disabled', () => {
|
|
53
|
+
it('should return no alerts from analyze()', () => {
|
|
54
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig({ enabled: false }));
|
|
55
|
+
const tc = buildToolCall();
|
|
56
|
+
const alerts = detector.analyze(tc);
|
|
57
|
+
expect(alerts).toEqual([]);
|
|
58
|
+
});
|
|
59
|
+
it('should return no alerts from analyzeResult()', () => {
|
|
60
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig({ enabled: false }));
|
|
61
|
+
const tc = buildToolCall();
|
|
62
|
+
const alerts = detector.analyzeResult(tc, 100, false);
|
|
63
|
+
expect(alerts).toEqual([]);
|
|
64
|
+
});
|
|
65
|
+
it('should not record results when disabled', () => {
|
|
66
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig({ enabled: false }));
|
|
67
|
+
const tc = buildToolCall();
|
|
68
|
+
// Should not throw
|
|
69
|
+
detector.recordResult(tc, 100, false);
|
|
70
|
+
detector.recordResult(tc, 200, true);
|
|
71
|
+
// No baselines should exist
|
|
72
|
+
expect(detector.getBaseline('tool', 'http.request', 'latency')).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
it('should return no alerts even with many calls', () => {
|
|
75
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig({ enabled: false }));
|
|
76
|
+
for (let i = 0; i < 50; i++) {
|
|
77
|
+
const alerts = detector.analyze(buildToolCall());
|
|
78
|
+
expect(alerts).toEqual([]);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
// -----------------------------------------------------------------------
|
|
83
|
+
// New tool usage detection
|
|
84
|
+
// -----------------------------------------------------------------------
|
|
85
|
+
describe('new tool usage detection', () => {
|
|
86
|
+
it('should not alert on the first tool an actor uses', () => {
|
|
87
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
88
|
+
const tc = buildToolCall({ actorId: 'actor-new', toolName: 'http.get' });
|
|
89
|
+
const alerts = detector.analyze(tc);
|
|
90
|
+
const newToolAlerts = alerts.filter(a => a.anomaly_type === 'new_tool_usage');
|
|
91
|
+
expect(newToolAlerts).toHaveLength(0);
|
|
92
|
+
});
|
|
93
|
+
it('should alert when an actor uses a new tool after their first', () => {
|
|
94
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
95
|
+
// First tool -- no alert
|
|
96
|
+
detector.analyze(buildToolCall({ actorId: 'actor-a', toolName: 'http.get' }));
|
|
97
|
+
// Second tool -- should alert
|
|
98
|
+
const alerts = detector.analyze(buildToolCall({ actorId: 'actor-a', toolName: 'slack.post' }));
|
|
99
|
+
const newToolAlerts = alerts.filter(a => a.anomaly_type === 'new_tool_usage');
|
|
100
|
+
expect(newToolAlerts).toHaveLength(1);
|
|
101
|
+
expect(newToolAlerts[0].entity_type).toBe('actor');
|
|
102
|
+
expect(newToolAlerts[0].entity_id).toBe('actor-a');
|
|
103
|
+
expect(newToolAlerts[0].metric).toBe('new_tool');
|
|
104
|
+
expect(newToolAlerts[0].severity).toBe('low');
|
|
105
|
+
});
|
|
106
|
+
it('should not alert on repeated use of the same tool', () => {
|
|
107
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
108
|
+
// Use tool twice
|
|
109
|
+
detector.analyze(buildToolCall({ actorId: 'actor-b', toolName: 'http.get' }));
|
|
110
|
+
detector.analyze(buildToolCall({ actorId: 'actor-b', toolName: 'slack.post' })); // triggers new tool alert
|
|
111
|
+
// Use the same tool again -- should not trigger new_tool_usage
|
|
112
|
+
const alerts = detector.analyze(buildToolCall({ actorId: 'actor-b', toolName: 'slack.post' }));
|
|
113
|
+
const newToolAlerts = alerts.filter(a => a.anomaly_type === 'new_tool_usage');
|
|
114
|
+
expect(newToolAlerts).toHaveLength(0);
|
|
115
|
+
});
|
|
116
|
+
it('should track different actors independently', () => {
|
|
117
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
118
|
+
// Actor A uses two tools
|
|
119
|
+
detector.analyze(buildToolCall({ actorId: 'actor-x', toolName: 'http.get' }));
|
|
120
|
+
detector.analyze(buildToolCall({ actorId: 'actor-x', toolName: 'slack.post' }));
|
|
121
|
+
// Actor B using http.get should not alert (it's their first tool)
|
|
122
|
+
const alerts = detector.analyze(buildToolCall({ actorId: 'actor-y', toolName: 'http.get' }));
|
|
123
|
+
const newToolAlerts = alerts.filter(a => a.anomaly_type === 'new_tool_usage');
|
|
124
|
+
expect(newToolAlerts).toHaveLength(0);
|
|
125
|
+
});
|
|
126
|
+
it('should not track new tools when track_actors is false', () => {
|
|
127
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig({ track_actors: false }));
|
|
128
|
+
detector.analyze(buildToolCall({ actorId: 'actor-c', toolName: 'http.get' }));
|
|
129
|
+
const alerts = detector.analyze(buildToolCall({ actorId: 'actor-c', toolName: 'slack.post' }));
|
|
130
|
+
const newToolAlerts = alerts.filter(a => a.anomaly_type === 'new_tool_usage');
|
|
131
|
+
expect(newToolAlerts).toHaveLength(0);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
// -----------------------------------------------------------------------
|
|
135
|
+
// Capability escalation detection
|
|
136
|
+
// -----------------------------------------------------------------------
|
|
137
|
+
describe('capability escalation detection', () => {
|
|
138
|
+
it('should not alert on the first capability an actor uses', () => {
|
|
139
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
140
|
+
const tc = buildToolCall({ actorId: 'cap-actor', capability: 'read' });
|
|
141
|
+
const alerts = detector.analyze(tc);
|
|
142
|
+
const capAlerts = alerts.filter(a => a.anomaly_type === 'capability_escalation');
|
|
143
|
+
expect(capAlerts).toHaveLength(0);
|
|
144
|
+
});
|
|
145
|
+
it('should alert when actor escalates from read to write', () => {
|
|
146
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
147
|
+
// Start with read
|
|
148
|
+
detector.analyze(buildToolCall({ actorId: 'cap-1', capability: 'read' }));
|
|
149
|
+
// Escalate to write
|
|
150
|
+
const alerts = detector.analyze(buildToolCall({ actorId: 'cap-1', capability: 'write' }));
|
|
151
|
+
const capAlerts = alerts.filter(a => a.anomaly_type === 'capability_escalation');
|
|
152
|
+
expect(capAlerts).toHaveLength(1);
|
|
153
|
+
expect(capAlerts[0].entity_type).toBe('actor');
|
|
154
|
+
expect(capAlerts[0].entity_id).toBe('cap-1');
|
|
155
|
+
expect(capAlerts[0].severity).toBe('low');
|
|
156
|
+
});
|
|
157
|
+
it('should alert with medium severity when escalating to delete', () => {
|
|
158
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
159
|
+
detector.analyze(buildToolCall({ actorId: 'cap-2', capability: 'read' }));
|
|
160
|
+
const alerts = detector.analyze(buildToolCall({ actorId: 'cap-2', capability: 'delete' }));
|
|
161
|
+
const capAlerts = alerts.filter(a => a.anomaly_type === 'capability_escalation');
|
|
162
|
+
expect(capAlerts).toHaveLength(1);
|
|
163
|
+
expect(capAlerts[0].severity).toBe('medium');
|
|
164
|
+
});
|
|
165
|
+
it('should alert with high severity when escalating to admin', () => {
|
|
166
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
167
|
+
detector.analyze(buildToolCall({ actorId: 'cap-3', capability: 'read' }));
|
|
168
|
+
const alerts = detector.analyze(buildToolCall({ actorId: 'cap-3', capability: 'admin' }));
|
|
169
|
+
const capAlerts = alerts.filter(a => a.anomaly_type === 'capability_escalation');
|
|
170
|
+
expect(capAlerts).toHaveLength(1);
|
|
171
|
+
expect(capAlerts[0].severity).toBe('high');
|
|
172
|
+
});
|
|
173
|
+
it('should not alert when actor already used this capability before', () => {
|
|
174
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
175
|
+
detector.analyze(buildToolCall({ actorId: 'cap-4', capability: 'read' }));
|
|
176
|
+
detector.analyze(buildToolCall({ actorId: 'cap-4', capability: 'write' })); // alerts once
|
|
177
|
+
// Using write again -- should not alert
|
|
178
|
+
const alerts = detector.analyze(buildToolCall({ actorId: 'cap-4', capability: 'write' }));
|
|
179
|
+
const capAlerts = alerts.filter(a => a.anomaly_type === 'capability_escalation');
|
|
180
|
+
expect(capAlerts).toHaveLength(0);
|
|
181
|
+
});
|
|
182
|
+
it('should not alert when going from write down to read (not an escalation)', () => {
|
|
183
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
184
|
+
detector.analyze(buildToolCall({ actorId: 'cap-5', capability: 'write' }));
|
|
185
|
+
// Going to read is a de-escalation, not escalation
|
|
186
|
+
const alerts = detector.analyze(buildToolCall({ actorId: 'cap-5', capability: 'read' }));
|
|
187
|
+
const capAlerts = alerts.filter(a => a.anomaly_type === 'capability_escalation');
|
|
188
|
+
expect(capAlerts).toHaveLength(0);
|
|
189
|
+
});
|
|
190
|
+
it('should not track capability escalation when track_actors is false', () => {
|
|
191
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig({ track_actors: false }));
|
|
192
|
+
detector.analyze(buildToolCall({ actorId: 'cap-6', capability: 'read' }));
|
|
193
|
+
const alerts = detector.analyze(buildToolCall({ actorId: 'cap-6', capability: 'admin' }));
|
|
194
|
+
const capAlerts = alerts.filter(a => a.anomaly_type === 'capability_escalation');
|
|
195
|
+
expect(capAlerts).toHaveLength(0);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
// -----------------------------------------------------------------------
|
|
199
|
+
// Off-hours activity detection
|
|
200
|
+
// -----------------------------------------------------------------------
|
|
201
|
+
describe('off-hours activity detection', () => {
|
|
202
|
+
beforeEach(() => {
|
|
203
|
+
jest.useFakeTimers();
|
|
204
|
+
});
|
|
205
|
+
afterEach(() => {
|
|
206
|
+
jest.useRealTimers();
|
|
207
|
+
});
|
|
208
|
+
it('should not alert during normal business hours (weekday)', () => {
|
|
209
|
+
// Set time to Wednesday 2pm UTC
|
|
210
|
+
const wednesday2pm = new Date('2025-01-15T14:00:00Z'); // Wednesday
|
|
211
|
+
jest.setSystemTime(wednesday2pm);
|
|
212
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
213
|
+
const alerts = detector.analyze(buildToolCall({ actorId: 'office-worker' }));
|
|
214
|
+
const offHoursAlerts = alerts.filter(a => a.anomaly_type === 'off_hours_activity');
|
|
215
|
+
expect(offHoursAlerts).toHaveLength(0);
|
|
216
|
+
});
|
|
217
|
+
it('should alert for activity at 3am UTC', () => {
|
|
218
|
+
// Set time to Wednesday 3am UTC
|
|
219
|
+
const wednesday3am = new Date('2025-01-15T03:00:00Z');
|
|
220
|
+
jest.setSystemTime(wednesday3am);
|
|
221
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
222
|
+
const alerts = detector.analyze(buildToolCall({ actorId: 'night-owl' }));
|
|
223
|
+
const offHoursAlerts = alerts.filter(a => a.anomaly_type === 'off_hours_activity');
|
|
224
|
+
expect(offHoursAlerts).toHaveLength(1);
|
|
225
|
+
expect(offHoursAlerts[0].entity_id).toBe('night-owl');
|
|
226
|
+
expect(offHoursAlerts[0].metric).toBe('hour_utc');
|
|
227
|
+
});
|
|
228
|
+
it('should alert for activity at 11pm UTC (hour 23)', () => {
|
|
229
|
+
const wednesday11pm = new Date('2025-01-15T23:00:00Z');
|
|
230
|
+
jest.setSystemTime(wednesday11pm);
|
|
231
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
232
|
+
const alerts = detector.analyze(buildToolCall({ actorId: 'late-worker' }));
|
|
233
|
+
const offHoursAlerts = alerts.filter(a => a.anomaly_type === 'off_hours_activity');
|
|
234
|
+
expect(offHoursAlerts).toHaveLength(1);
|
|
235
|
+
});
|
|
236
|
+
it('should alert for weekend activity', () => {
|
|
237
|
+
// Set time to Saturday 10am UTC
|
|
238
|
+
const saturday10am = new Date('2025-01-18T10:00:00Z'); // Saturday
|
|
239
|
+
jest.setSystemTime(saturday10am);
|
|
240
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
241
|
+
const alerts = detector.analyze(buildToolCall({ actorId: 'weekend-worker' }));
|
|
242
|
+
const offHoursAlerts = alerts.filter(a => a.anomaly_type === 'off_hours_activity');
|
|
243
|
+
expect(offHoursAlerts).toHaveLength(1);
|
|
244
|
+
expect(offHoursAlerts[0].severity).toBe('low');
|
|
245
|
+
});
|
|
246
|
+
it('should have medium severity for weekend + off-hours combined', () => {
|
|
247
|
+
// Set time to Saturday 3am UTC
|
|
248
|
+
const saturday3am = new Date('2025-01-18T03:00:00Z'); // Saturday
|
|
249
|
+
jest.setSystemTime(saturday3am);
|
|
250
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
251
|
+
const alerts = detector.analyze(buildToolCall({ actorId: 'deep-night-weekend' }));
|
|
252
|
+
const offHoursAlerts = alerts.filter(a => a.anomaly_type === 'off_hours_activity');
|
|
253
|
+
expect(offHoursAlerts).toHaveLength(1);
|
|
254
|
+
expect(offHoursAlerts[0].severity).toBe('medium');
|
|
255
|
+
});
|
|
256
|
+
it('should throttle off-hours alerts to once per entity per hour', () => {
|
|
257
|
+
const wednesday3am = new Date('2025-01-15T03:00:00Z');
|
|
258
|
+
jest.setSystemTime(wednesday3am);
|
|
259
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
260
|
+
// First call should alert
|
|
261
|
+
const alerts1 = detector.analyze(buildToolCall({ actorId: 'throttle-test' }));
|
|
262
|
+
const offHours1 = alerts1.filter(a => a.anomaly_type === 'off_hours_activity');
|
|
263
|
+
expect(offHours1).toHaveLength(1);
|
|
264
|
+
// Second call within same hour should NOT alert
|
|
265
|
+
const alerts2 = detector.analyze(buildToolCall({ actorId: 'throttle-test' }));
|
|
266
|
+
const offHours2 = alerts2.filter(a => a.anomaly_type === 'off_hours_activity');
|
|
267
|
+
expect(offHours2).toHaveLength(0);
|
|
268
|
+
});
|
|
269
|
+
it('should alert again in the next hour', () => {
|
|
270
|
+
const wednesday3am = new Date('2025-01-15T03:00:00Z');
|
|
271
|
+
jest.setSystemTime(wednesday3am);
|
|
272
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
273
|
+
// First alert at 3am
|
|
274
|
+
const alerts1 = detector.analyze(buildToolCall({ actorId: 'next-hour' }));
|
|
275
|
+
expect(alerts1.filter(a => a.anomaly_type === 'off_hours_activity')).toHaveLength(1);
|
|
276
|
+
// Advance to 4am (still off-hours)
|
|
277
|
+
jest.setSystemTime(new Date('2025-01-15T04:00:00Z'));
|
|
278
|
+
// Should alert again since it's a new hour
|
|
279
|
+
const alerts2 = detector.analyze(buildToolCall({ actorId: 'next-hour' }));
|
|
280
|
+
expect(alerts2.filter(a => a.anomaly_type === 'off_hours_activity')).toHaveLength(1);
|
|
281
|
+
});
|
|
282
|
+
it('should not alert at 6am UTC (boundary - start of business)', () => {
|
|
283
|
+
const wednesday6am = new Date('2025-01-15T06:00:00Z');
|
|
284
|
+
jest.setSystemTime(wednesday6am);
|
|
285
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
286
|
+
const alerts = detector.analyze(buildToolCall({ actorId: 'boundary-test' }));
|
|
287
|
+
const offHoursAlerts = alerts.filter(a => a.anomaly_type === 'off_hours_activity');
|
|
288
|
+
expect(offHoursAlerts).toHaveLength(0);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
// -----------------------------------------------------------------------
|
|
292
|
+
// Latency spike detection
|
|
293
|
+
// -----------------------------------------------------------------------
|
|
294
|
+
describe('latency spike detection', () => {
|
|
295
|
+
it('should not alert when there are fewer than min_samples', () => {
|
|
296
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig({ min_samples: 10 }));
|
|
297
|
+
const tc = buildToolCall({ toolName: 'http.fast' });
|
|
298
|
+
// Record 5 normal latencies
|
|
299
|
+
for (let i = 0; i < 5; i++) {
|
|
300
|
+
detector.recordResult(tc, 100, false);
|
|
301
|
+
}
|
|
302
|
+
// Even a very high latency should not alert (not enough samples)
|
|
303
|
+
const alerts = detector.analyzeResult(tc, 10000, false);
|
|
304
|
+
expect(alerts.filter(a => a.anomaly_type === 'latency_spike')).toHaveLength(0);
|
|
305
|
+
});
|
|
306
|
+
it('should not alert for normal latency within baseline', () => {
|
|
307
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig({ min_samples: 10 }));
|
|
308
|
+
const tc = buildToolCall({ toolName: 'http.normal' });
|
|
309
|
+
// Build baseline with significant variance (50-150ms range)
|
|
310
|
+
const values = [50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 100, 90, 110, 105];
|
|
311
|
+
for (const v of values) {
|
|
312
|
+
detector.recordResult(tc, v, false);
|
|
313
|
+
}
|
|
314
|
+
// A value within the normal range should not alert
|
|
315
|
+
const alerts = detector.analyzeResult(tc, 115, false);
|
|
316
|
+
expect(alerts.filter(a => a.anomaly_type === 'latency_spike')).toHaveLength(0);
|
|
317
|
+
});
|
|
318
|
+
it('should alert for a significant latency spike', () => {
|
|
319
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig({ min_samples: 10, z_score_threshold: 3 }));
|
|
320
|
+
const tc = buildToolCall({ toolName: 'http.spike' });
|
|
321
|
+
// Build baseline of ~100ms latency with low variance
|
|
322
|
+
for (let i = 0; i < 20; i++) {
|
|
323
|
+
detector.recordResult(tc, 100, false); // constant 100ms
|
|
324
|
+
}
|
|
325
|
+
// Add a tiny bit of variance so stddev > 0
|
|
326
|
+
detector.recordResult(tc, 101, false);
|
|
327
|
+
detector.recordResult(tc, 99, false);
|
|
328
|
+
// Now a 10x spike
|
|
329
|
+
const alerts = detector.analyzeResult(tc, 1000, false);
|
|
330
|
+
const latencyAlerts = alerts.filter(a => a.anomaly_type === 'latency_spike');
|
|
331
|
+
expect(latencyAlerts).toHaveLength(1);
|
|
332
|
+
expect(latencyAlerts[0].entity_type).toBe('tool');
|
|
333
|
+
expect(latencyAlerts[0].entity_id).toBe('http.spike');
|
|
334
|
+
expect(latencyAlerts[0].metric).toBe('latency_ms');
|
|
335
|
+
expect(latencyAlerts[0].current_value).toBe(1000);
|
|
336
|
+
expect(latencyAlerts[0].z_score).toBeGreaterThan(3);
|
|
337
|
+
});
|
|
338
|
+
it('should classify latency spike severity based on z-score', () => {
|
|
339
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig({ min_samples: 5, z_score_threshold: 2 }));
|
|
340
|
+
const tc = buildToolCall({ toolName: 'http.severity' });
|
|
341
|
+
// Build tight baseline
|
|
342
|
+
for (let i = 0; i < 10; i++) {
|
|
343
|
+
detector.recordResult(tc, 100, false);
|
|
344
|
+
}
|
|
345
|
+
detector.recordResult(tc, 101, false);
|
|
346
|
+
detector.recordResult(tc, 99, false);
|
|
347
|
+
// Extreme spike
|
|
348
|
+
const alerts = detector.analyzeResult(tc, 5000, false);
|
|
349
|
+
const latencyAlerts = alerts.filter(a => a.anomaly_type === 'latency_spike');
|
|
350
|
+
if (latencyAlerts.length > 0) {
|
|
351
|
+
// z-score for 5000 with mean ~100 should be very high
|
|
352
|
+
expect(latencyAlerts[0].z_score).toBeGreaterThan(6);
|
|
353
|
+
expect(latencyAlerts[0].severity).toBe('high');
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
// -----------------------------------------------------------------------
|
|
358
|
+
// Error rate spike detection
|
|
359
|
+
// -----------------------------------------------------------------------
|
|
360
|
+
describe('error rate spike detection', () => {
|
|
361
|
+
it('should not alert for errors when there are fewer than min_samples', () => {
|
|
362
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig({ min_samples: 10 }));
|
|
363
|
+
const tc = buildToolCall({ actorId: 'err-actor-1' });
|
|
364
|
+
// Record 5 successful calls
|
|
365
|
+
for (let i = 0; i < 5; i++) {
|
|
366
|
+
detector.recordResult(tc, 100, false);
|
|
367
|
+
}
|
|
368
|
+
// An error should not trigger an alert (not enough samples)
|
|
369
|
+
const alerts = detector.analyzeResult(tc, 100, true);
|
|
370
|
+
expect(alerts.filter(a => a.anomaly_type === 'error_rate_spike')).toHaveLength(0);
|
|
371
|
+
});
|
|
372
|
+
it('should alert when error rate spikes from a low baseline', () => {
|
|
373
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig({ min_samples: 10 }));
|
|
374
|
+
const tc = buildToolCall({ actorId: 'err-actor-2' });
|
|
375
|
+
// Build baseline with all successes (error_rate near 0)
|
|
376
|
+
for (let i = 0; i < 20; i++) {
|
|
377
|
+
detector.recordResult(tc, 100, false);
|
|
378
|
+
}
|
|
379
|
+
// Record the error and analyze
|
|
380
|
+
detector.recordResult(tc, 100, true);
|
|
381
|
+
const alerts = detector.analyzeResult(tc, 100, true);
|
|
382
|
+
const errorAlerts = alerts.filter(a => a.anomaly_type === 'error_rate_spike');
|
|
383
|
+
expect(errorAlerts).toHaveLength(1);
|
|
384
|
+
expect(errorAlerts[0].entity_type).toBe('actor');
|
|
385
|
+
expect(errorAlerts[0].entity_id).toBe('err-actor-2');
|
|
386
|
+
expect(errorAlerts[0].metric).toBe('error_rate');
|
|
387
|
+
});
|
|
388
|
+
it('should not alert for errors when error rate was already high', () => {
|
|
389
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig({ min_samples: 10 }));
|
|
390
|
+
const tc = buildToolCall({ actorId: 'err-actor-3' });
|
|
391
|
+
// Build baseline with high error rate (50% errors)
|
|
392
|
+
for (let i = 0; i < 20; i++) {
|
|
393
|
+
detector.recordResult(tc, 100, i % 2 === 0); // alternating
|
|
394
|
+
}
|
|
395
|
+
// Another error should not spike (error rate already ~0.5)
|
|
396
|
+
const alerts = detector.analyzeResult(tc, 100, true);
|
|
397
|
+
const errorAlerts = alerts.filter(a => a.anomaly_type === 'error_rate_spike');
|
|
398
|
+
expect(errorAlerts).toHaveLength(0);
|
|
399
|
+
});
|
|
400
|
+
it('should not alert for successful requests', () => {
|
|
401
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig({ min_samples: 10 }));
|
|
402
|
+
const tc = buildToolCall({ actorId: 'err-actor-4' });
|
|
403
|
+
// Build baseline
|
|
404
|
+
for (let i = 0; i < 15; i++) {
|
|
405
|
+
detector.recordResult(tc, 100, false);
|
|
406
|
+
}
|
|
407
|
+
// Analyze a success -- should not trigger error alert
|
|
408
|
+
const alerts = detector.analyzeResult(tc, 100, false);
|
|
409
|
+
const errorAlerts = alerts.filter(a => a.anomaly_type === 'error_rate_spike');
|
|
410
|
+
expect(errorAlerts).toHaveLength(0);
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
// -----------------------------------------------------------------------
|
|
414
|
+
// Request rate spike detection
|
|
415
|
+
// -----------------------------------------------------------------------
|
|
416
|
+
describe('request rate spike detection', () => {
|
|
417
|
+
it('should not alert with fewer than min_samples requests', () => {
|
|
418
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig({ min_samples: 10 }));
|
|
419
|
+
// Send a few requests
|
|
420
|
+
for (let i = 0; i < 5; i++) {
|
|
421
|
+
const alerts = detector.analyze(buildToolCall({ actorId: 'rate-1' }));
|
|
422
|
+
const rateAlerts = alerts.filter(a => a.anomaly_type === 'request_rate_spike');
|
|
423
|
+
expect(rateAlerts).toHaveLength(0);
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
it('should build baselines without alerting during normal traffic', () => {
|
|
427
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig({ min_samples: 5 }));
|
|
428
|
+
// Send steady traffic -- should not alert during baseline building
|
|
429
|
+
let rateAlertCount = 0;
|
|
430
|
+
for (let i = 0; i < 20; i++) {
|
|
431
|
+
const alerts = detector.analyze(buildToolCall({ actorId: 'rate-2' }));
|
|
432
|
+
rateAlertCount += alerts.filter(a => a.anomaly_type === 'request_rate_spike').length;
|
|
433
|
+
}
|
|
434
|
+
// Some alerts might fire during the initial window as baselines form,
|
|
435
|
+
// but after steady traffic the system should stabilize
|
|
436
|
+
// We just verify the detector runs without error
|
|
437
|
+
expect(typeof rateAlertCount).toBe('number');
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
// -----------------------------------------------------------------------
|
|
441
|
+
// Config options
|
|
442
|
+
// -----------------------------------------------------------------------
|
|
443
|
+
describe('configuration options', () => {
|
|
444
|
+
it('should use custom z_score_threshold', () => {
|
|
445
|
+
// With a very high threshold, latency spikes should not alert
|
|
446
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig({ min_samples: 5, z_score_threshold: 100 }));
|
|
447
|
+
const tc = buildToolCall({ toolName: 'http.threshold' });
|
|
448
|
+
// Build baseline with significant variance so z-scores stay manageable
|
|
449
|
+
const values = [50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 100, 90, 110, 105];
|
|
450
|
+
for (const v of values) {
|
|
451
|
+
detector.recordResult(tc, v, false);
|
|
452
|
+
}
|
|
453
|
+
// A spike that would normally alert with z_score_threshold=3
|
|
454
|
+
// but shouldn't alert with z_score_threshold=100
|
|
455
|
+
const alerts = detector.analyzeResult(tc, 500, false);
|
|
456
|
+
const latencyAlerts = alerts.filter(a => a.anomaly_type === 'latency_spike');
|
|
457
|
+
expect(latencyAlerts).toHaveLength(0);
|
|
458
|
+
});
|
|
459
|
+
it('should use custom min_samples', () => {
|
|
460
|
+
// With min_samples = 3, we should start detecting earlier
|
|
461
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig({ min_samples: 3, z_score_threshold: 2 }));
|
|
462
|
+
const tc = buildToolCall({ toolName: 'http.min' });
|
|
463
|
+
// Build baseline with just 3 samples
|
|
464
|
+
detector.recordResult(tc, 100, false);
|
|
465
|
+
detector.recordResult(tc, 101, false);
|
|
466
|
+
detector.recordResult(tc, 99, false);
|
|
467
|
+
// Should have enough samples now
|
|
468
|
+
const baseline = detector.getBaseline('tool', 'http.min', 'latency');
|
|
469
|
+
expect(baseline).not.toBeNull();
|
|
470
|
+
expect(baseline.count).toBe(3);
|
|
471
|
+
});
|
|
472
|
+
it('should respect track_actors: false', () => {
|
|
473
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig({ track_actors: false }));
|
|
474
|
+
// New tool usage and capability escalation should not be tracked
|
|
475
|
+
detector.analyze(buildToolCall({ actorId: 'no-track', toolName: 'http.get', capability: 'read' }));
|
|
476
|
+
const alerts = detector.analyze(buildToolCall({ actorId: 'no-track', toolName: 'slack.post', capability: 'admin' }));
|
|
477
|
+
const actorAlerts = alerts.filter(a => a.anomaly_type === 'new_tool_usage' || a.anomaly_type === 'capability_escalation');
|
|
478
|
+
expect(actorAlerts).toHaveLength(0);
|
|
479
|
+
});
|
|
480
|
+
it('should respect track_tools: false', () => {
|
|
481
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig({ track_tools: false }));
|
|
482
|
+
for (let i = 0; i < 15; i++) {
|
|
483
|
+
const alerts = detector.analyze(buildToolCall({ toolName: 'http.get' }));
|
|
484
|
+
const toolRateAlerts = alerts.filter(a => a.anomaly_type === 'request_rate_spike' && a.entity_type === 'tool');
|
|
485
|
+
// No tool-level rate tracking
|
|
486
|
+
expect(toolRateAlerts).toHaveLength(0);
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
it('should respect track_workspaces: false', () => {
|
|
490
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig({ track_workspaces: false }));
|
|
491
|
+
for (let i = 0; i < 15; i++) {
|
|
492
|
+
const alerts = detector.analyze(buildToolCall({ workspaceId: 'ws-1' }));
|
|
493
|
+
const wsRateAlerts = alerts.filter(a => a.anomaly_type === 'request_rate_spike' && a.entity_type === 'workspace');
|
|
494
|
+
expect(wsRateAlerts).toHaveLength(0);
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
it('should use custom window_ms', () => {
|
|
498
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig({ window_ms: 5000 }));
|
|
499
|
+
const tc = buildToolCall({ toolName: 'http.window' });
|
|
500
|
+
detector.recordResult(tc, 100, false);
|
|
501
|
+
const baseline = detector.getBaseline('tool', 'http.window', 'latency');
|
|
502
|
+
expect(baseline).not.toBeNull();
|
|
503
|
+
expect(baseline.count).toBe(1);
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
// -----------------------------------------------------------------------
|
|
507
|
+
// Alert management
|
|
508
|
+
// -----------------------------------------------------------------------
|
|
509
|
+
describe('alert management', () => {
|
|
510
|
+
it('should return recent alerts via getAlerts()', () => {
|
|
511
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
512
|
+
// Generate some alerts by using new tools
|
|
513
|
+
detector.analyze(buildToolCall({ actorId: 'alert-mgmt', toolName: 'tool-a' }));
|
|
514
|
+
detector.analyze(buildToolCall({ actorId: 'alert-mgmt', toolName: 'tool-b' }));
|
|
515
|
+
detector.analyze(buildToolCall({ actorId: 'alert-mgmt', toolName: 'tool-c' }));
|
|
516
|
+
const alerts = detector.getAlerts();
|
|
517
|
+
// Should have new_tool_usage alerts for tool-b and tool-c
|
|
518
|
+
const newToolAlerts = alerts.filter(a => a.anomaly_type === 'new_tool_usage');
|
|
519
|
+
expect(newToolAlerts.length).toBeGreaterThanOrEqual(2);
|
|
520
|
+
});
|
|
521
|
+
it('should limit alerts with getAlerts(limit)', () => {
|
|
522
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
523
|
+
// Generate multiple alerts
|
|
524
|
+
detector.analyze(buildToolCall({ actorId: 'limit-test', toolName: 'tool-1' }));
|
|
525
|
+
detector.analyze(buildToolCall({ actorId: 'limit-test', toolName: 'tool-2' }));
|
|
526
|
+
detector.analyze(buildToolCall({ actorId: 'limit-test', toolName: 'tool-3' }));
|
|
527
|
+
detector.analyze(buildToolCall({ actorId: 'limit-test', toolName: 'tool-4' }));
|
|
528
|
+
const alerts = detector.getAlerts(2);
|
|
529
|
+
expect(alerts.length).toBeLessThanOrEqual(2);
|
|
530
|
+
});
|
|
531
|
+
it('should filter alerts by entity via getAlertsForEntity()', () => {
|
|
532
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
533
|
+
// Generate alerts for different actors
|
|
534
|
+
detector.analyze(buildToolCall({ actorId: 'entity-a', toolName: 'tool-1' }));
|
|
535
|
+
detector.analyze(buildToolCall({ actorId: 'entity-a', toolName: 'tool-2' })); // new_tool alert
|
|
536
|
+
detector.analyze(buildToolCall({ actorId: 'entity-b', toolName: 'tool-1' }));
|
|
537
|
+
detector.analyze(buildToolCall({ actorId: 'entity-b', toolName: 'tool-3' })); // new_tool alert
|
|
538
|
+
const alertsA = detector.getAlertsForEntity('actor', 'entity-a');
|
|
539
|
+
const alertsB = detector.getAlertsForEntity('actor', 'entity-b');
|
|
540
|
+
// Each should have their own alerts
|
|
541
|
+
for (const alert of alertsA) {
|
|
542
|
+
expect(alert.entity_id).toBe('entity-a');
|
|
543
|
+
}
|
|
544
|
+
for (const alert of alertsB) {
|
|
545
|
+
expect(alert.entity_id).toBe('entity-b');
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
it('should return empty array for non-existent entity', () => {
|
|
549
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
550
|
+
const alerts = detector.getAlertsForEntity('actor', 'does-not-exist');
|
|
551
|
+
expect(alerts).toEqual([]);
|
|
552
|
+
});
|
|
553
|
+
it('should cap stored alerts at maxAlerts', () => {
|
|
554
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
555
|
+
// Generate more than 1000 alerts by creating many new tool usages
|
|
556
|
+
// Each new actor gets one bootstrap tool, then every subsequent tool triggers an alert
|
|
557
|
+
for (let i = 0; i < 600; i++) {
|
|
558
|
+
const actorId = `bulk-actor-${i}`;
|
|
559
|
+
detector.analyze(buildToolCall({ actorId, toolName: 'tool-base' }));
|
|
560
|
+
detector.analyze(buildToolCall({ actorId, toolName: 'tool-new' }));
|
|
561
|
+
}
|
|
562
|
+
const allAlerts = detector.getAlerts(2000);
|
|
563
|
+
expect(allAlerts.length).toBeLessThanOrEqual(1000);
|
|
564
|
+
});
|
|
565
|
+
it('should have valid alert fields', () => {
|
|
566
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
567
|
+
detector.analyze(buildToolCall({ actorId: 'field-test', toolName: 'tool-a' }));
|
|
568
|
+
const alerts = detector.analyze(buildToolCall({ actorId: 'field-test', toolName: 'tool-b' }));
|
|
569
|
+
const newToolAlerts = alerts.filter(a => a.anomaly_type === 'new_tool_usage');
|
|
570
|
+
expect(newToolAlerts).toHaveLength(1);
|
|
571
|
+
const alert = newToolAlerts[0];
|
|
572
|
+
// Verify all required fields are present
|
|
573
|
+
expect(alert.alert_id).toBeTruthy();
|
|
574
|
+
expect(typeof alert.alert_id).toBe('string');
|
|
575
|
+
expect(alert.timestamp).toBeTruthy();
|
|
576
|
+
expect(new Date(alert.timestamp).getTime()).not.toBeNaN();
|
|
577
|
+
expect(alert.anomaly_type).toBe('new_tool_usage');
|
|
578
|
+
expect(alert.entity_type).toBe('actor');
|
|
579
|
+
expect(alert.entity_id).toBe('field-test');
|
|
580
|
+
expect(alert.metric).toBe('new_tool');
|
|
581
|
+
expect(typeof alert.current_value).toBe('number');
|
|
582
|
+
expect(typeof alert.baseline_mean).toBe('number');
|
|
583
|
+
expect(typeof alert.baseline_stddev).toBe('number');
|
|
584
|
+
expect(typeof alert.z_score).toBe('number');
|
|
585
|
+
expect(['low', 'medium', 'high']).toContain(alert.severity);
|
|
586
|
+
});
|
|
587
|
+
it('should generate unique alert_ids', () => {
|
|
588
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
589
|
+
detector.analyze(buildToolCall({ actorId: 'unique-id', toolName: 'tool-a' }));
|
|
590
|
+
detector.analyze(buildToolCall({ actorId: 'unique-id', toolName: 'tool-b' }));
|
|
591
|
+
detector.analyze(buildToolCall({ actorId: 'unique-id', toolName: 'tool-c' }));
|
|
592
|
+
const alerts = detector.getAlerts();
|
|
593
|
+
const ids = alerts.map(a => a.alert_id);
|
|
594
|
+
const uniqueIds = new Set(ids);
|
|
595
|
+
expect(uniqueIds.size).toBe(ids.length);
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
// -----------------------------------------------------------------------
|
|
599
|
+
// Baseline queries
|
|
600
|
+
// -----------------------------------------------------------------------
|
|
601
|
+
describe('getBaseline()', () => {
|
|
602
|
+
it('should return null for unknown metrics', () => {
|
|
603
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
604
|
+
const baseline = detector.getBaseline('tool', 'unknown-tool', 'latency');
|
|
605
|
+
expect(baseline).toBeNull();
|
|
606
|
+
});
|
|
607
|
+
it('should return correct stats after recording data', () => {
|
|
608
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
609
|
+
const tc = buildToolCall({ toolName: 'http.stats' });
|
|
610
|
+
// Record 5 latencies: 100, 200, 300, 400, 500
|
|
611
|
+
detector.recordResult(tc, 100, false);
|
|
612
|
+
detector.recordResult(tc, 200, false);
|
|
613
|
+
detector.recordResult(tc, 300, false);
|
|
614
|
+
detector.recordResult(tc, 400, false);
|
|
615
|
+
detector.recordResult(tc, 500, false);
|
|
616
|
+
const baseline = detector.getBaseline('tool', 'http.stats', 'latency');
|
|
617
|
+
expect(baseline).not.toBeNull();
|
|
618
|
+
expect(baseline.count).toBe(5);
|
|
619
|
+
expect(baseline.mean).toBe(300); // (100+200+300+400+500)/5
|
|
620
|
+
expect(baseline.stddev).toBeCloseTo(Math.sqrt(25000), 1); // sample stddev ~ 158.11
|
|
621
|
+
});
|
|
622
|
+
it('should return correct error rate baseline', () => {
|
|
623
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
624
|
+
const tc = buildToolCall({ actorId: 'err-baseline' });
|
|
625
|
+
// Record 8 successes and 2 errors
|
|
626
|
+
for (let i = 0; i < 8; i++) {
|
|
627
|
+
detector.recordResult(tc, 100, false);
|
|
628
|
+
}
|
|
629
|
+
for (let i = 0; i < 2; i++) {
|
|
630
|
+
detector.recordResult(tc, 100, true);
|
|
631
|
+
}
|
|
632
|
+
const baseline = detector.getBaseline('actor', 'err-baseline', 'error_rate');
|
|
633
|
+
expect(baseline).not.toBeNull();
|
|
634
|
+
expect(baseline.count).toBe(10);
|
|
635
|
+
expect(baseline.mean).toBeCloseTo(0.2, 2); // 2/10
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
// -----------------------------------------------------------------------
|
|
639
|
+
// reset()
|
|
640
|
+
// -----------------------------------------------------------------------
|
|
641
|
+
describe('reset()', () => {
|
|
642
|
+
it('should clear all tracking data', () => {
|
|
643
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
644
|
+
// Generate some state
|
|
645
|
+
detector.analyze(buildToolCall({ actorId: 'reset-test', toolName: 'tool-a' }));
|
|
646
|
+
detector.analyze(buildToolCall({ actorId: 'reset-test', toolName: 'tool-b' }));
|
|
647
|
+
detector.recordResult(buildToolCall({ toolName: 'http.reset' }), 100, false);
|
|
648
|
+
expect(detector.getAlerts().length).toBeGreaterThan(0);
|
|
649
|
+
expect(detector.getBaseline('tool', 'http.reset', 'latency')).not.toBeNull();
|
|
650
|
+
// Reset
|
|
651
|
+
detector.reset();
|
|
652
|
+
// Everything should be cleared
|
|
653
|
+
expect(detector.getAlerts()).toEqual([]);
|
|
654
|
+
expect(detector.getBaseline('tool', 'http.reset', 'latency')).toBeNull();
|
|
655
|
+
});
|
|
656
|
+
it('should allow new tool usage alerts again after reset', () => {
|
|
657
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
658
|
+
// Build tool history
|
|
659
|
+
detector.analyze(buildToolCall({ actorId: 'reset-actor', toolName: 'tool-a' }));
|
|
660
|
+
detector.analyze(buildToolCall({ actorId: 'reset-actor', toolName: 'tool-b' }));
|
|
661
|
+
detector.reset();
|
|
662
|
+
// After reset, tool-a should not trigger new_tool_usage (first tool again)
|
|
663
|
+
const alerts1 = detector.analyze(buildToolCall({ actorId: 'reset-actor', toolName: 'tool-a' }));
|
|
664
|
+
expect(alerts1.filter(a => a.anomaly_type === 'new_tool_usage')).toHaveLength(0);
|
|
665
|
+
// tool-b should now trigger new_tool_usage (it's the second tool after reset)
|
|
666
|
+
const alerts2 = detector.analyze(buildToolCall({ actorId: 'reset-actor', toolName: 'tool-b' }));
|
|
667
|
+
expect(alerts2.filter(a => a.anomaly_type === 'new_tool_usage')).toHaveLength(1);
|
|
668
|
+
});
|
|
669
|
+
it('should allow capability escalation alerts again after reset', () => {
|
|
670
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
671
|
+
// Build capability history
|
|
672
|
+
detector.analyze(buildToolCall({ actorId: 'reset-cap', capability: 'read' }));
|
|
673
|
+
detector.analyze(buildToolCall({ actorId: 'reset-cap', capability: 'write' }));
|
|
674
|
+
detector.reset();
|
|
675
|
+
// After reset, read should not alert (first cap)
|
|
676
|
+
const alerts1 = detector.analyze(buildToolCall({ actorId: 'reset-cap', capability: 'read' }));
|
|
677
|
+
expect(alerts1.filter(a => a.anomaly_type === 'capability_escalation')).toHaveLength(0);
|
|
678
|
+
// write should alert again (escalation from read)
|
|
679
|
+
const alerts2 = detector.analyze(buildToolCall({ actorId: 'reset-cap', capability: 'write' }));
|
|
680
|
+
expect(alerts2.filter(a => a.anomaly_type === 'capability_escalation')).toHaveLength(1);
|
|
681
|
+
});
|
|
682
|
+
});
|
|
683
|
+
// -----------------------------------------------------------------------
|
|
684
|
+
// Window pruning (data expiry)
|
|
685
|
+
// -----------------------------------------------------------------------
|
|
686
|
+
describe('window pruning', () => {
|
|
687
|
+
beforeEach(() => {
|
|
688
|
+
jest.useFakeTimers();
|
|
689
|
+
});
|
|
690
|
+
afterEach(() => {
|
|
691
|
+
jest.useRealTimers();
|
|
692
|
+
});
|
|
693
|
+
it('should prune old data points outside the window', () => {
|
|
694
|
+
const windowMs = 10000; // 10 seconds
|
|
695
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig({ window_ms: windowMs }));
|
|
696
|
+
const tc = buildToolCall({ toolName: 'http.prune' });
|
|
697
|
+
// Record at t=0
|
|
698
|
+
detector.recordResult(tc, 100, false);
|
|
699
|
+
expect(detector.getBaseline('tool', 'http.prune', 'latency').count).toBe(1);
|
|
700
|
+
// Advance past window
|
|
701
|
+
jest.advanceTimersByTime(windowMs + 1);
|
|
702
|
+
// Record a new point -- the old one should be pruned
|
|
703
|
+
detector.recordResult(tc, 200, false);
|
|
704
|
+
const baseline = detector.getBaseline('tool', 'http.prune', 'latency');
|
|
705
|
+
expect(baseline.count).toBe(1);
|
|
706
|
+
expect(baseline.mean).toBe(200); // Only the new data point
|
|
707
|
+
});
|
|
708
|
+
it('should keep data points within the window', () => {
|
|
709
|
+
const windowMs = 10000;
|
|
710
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig({ window_ms: windowMs }));
|
|
711
|
+
const tc = buildToolCall({ toolName: 'http.keep' });
|
|
712
|
+
// Record at t=0
|
|
713
|
+
detector.recordResult(tc, 100, false);
|
|
714
|
+
// Advance by half the window
|
|
715
|
+
jest.advanceTimersByTime(windowMs / 2);
|
|
716
|
+
// Record at t=5s
|
|
717
|
+
detector.recordResult(tc, 200, false);
|
|
718
|
+
// Both should still be in the window
|
|
719
|
+
const baseline = detector.getBaseline('tool', 'http.keep', 'latency');
|
|
720
|
+
expect(baseline.count).toBe(2);
|
|
721
|
+
expect(baseline.mean).toBe(150);
|
|
722
|
+
});
|
|
723
|
+
});
|
|
724
|
+
// -----------------------------------------------------------------------
|
|
725
|
+
// Rolling window statistics (mean, stddev, z-score)
|
|
726
|
+
// -----------------------------------------------------------------------
|
|
727
|
+
describe('rolling window statistics', () => {
|
|
728
|
+
it('should compute correct mean', () => {
|
|
729
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
730
|
+
const tc = buildToolCall({ toolName: 'http.mean' });
|
|
731
|
+
detector.recordResult(tc, 10, false);
|
|
732
|
+
detector.recordResult(tc, 20, false);
|
|
733
|
+
detector.recordResult(tc, 30, false);
|
|
734
|
+
const baseline = detector.getBaseline('tool', 'http.mean', 'latency');
|
|
735
|
+
expect(baseline.mean).toBe(20);
|
|
736
|
+
});
|
|
737
|
+
it('should compute correct stddev', () => {
|
|
738
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
739
|
+
const tc = buildToolCall({ toolName: 'http.stddev' });
|
|
740
|
+
// Values: 2, 4, 4, 4, 5, 5, 7, 9
|
|
741
|
+
// Mean = 5, Sample Variance = 4.571..., Sample Stddev = ~2.138
|
|
742
|
+
[2, 4, 4, 4, 5, 5, 7, 9].forEach(v => {
|
|
743
|
+
detector.recordResult(tc, v, false);
|
|
744
|
+
});
|
|
745
|
+
const baseline = detector.getBaseline('tool', 'http.stddev', 'latency');
|
|
746
|
+
expect(baseline.mean).toBe(5);
|
|
747
|
+
expect(baseline.stddev).toBeCloseTo(2.138, 2);
|
|
748
|
+
});
|
|
749
|
+
it('should return stddev of 0 with a single data point', () => {
|
|
750
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
751
|
+
const tc = buildToolCall({ toolName: 'http.single' });
|
|
752
|
+
detector.recordResult(tc, 100, false);
|
|
753
|
+
const baseline = detector.getBaseline('tool', 'http.single', 'latency');
|
|
754
|
+
expect(baseline.stddev).toBe(0);
|
|
755
|
+
});
|
|
756
|
+
it('should return mean of 0 with no data points', () => {
|
|
757
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
758
|
+
const baseline = detector.getBaseline('tool', 'http.empty', 'latency');
|
|
759
|
+
expect(baseline).toBeNull();
|
|
760
|
+
});
|
|
761
|
+
});
|
|
762
|
+
// -----------------------------------------------------------------------
|
|
763
|
+
// Severity classification
|
|
764
|
+
// -----------------------------------------------------------------------
|
|
765
|
+
describe('severity classification', () => {
|
|
766
|
+
it('should classify z-score 3-4 as low', () => {
|
|
767
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig({ min_samples: 5, z_score_threshold: 2 }));
|
|
768
|
+
const tc = buildToolCall({ toolName: 'http.sev-low' });
|
|
769
|
+
// Build baseline with values where we can control z-score
|
|
770
|
+
// Values: 10 x 100, then record 101, 99 for tiny variance
|
|
771
|
+
for (let i = 0; i < 10; i++) {
|
|
772
|
+
detector.recordResult(tc, 100, false);
|
|
773
|
+
}
|
|
774
|
+
detector.recordResult(tc, 102, false);
|
|
775
|
+
detector.recordResult(tc, 98, false);
|
|
776
|
+
// The baseline has mean ~100, tiny stddev
|
|
777
|
+
// We need a spike that gives z ~3-4
|
|
778
|
+
const baseline = detector.getBaseline('tool', 'http.sev-low', 'latency');
|
|
779
|
+
if (baseline && baseline.stddev > 0) {
|
|
780
|
+
// Compute a value that would give z ~ 3.5
|
|
781
|
+
const targetValue = baseline.mean + 3.5 * baseline.stddev;
|
|
782
|
+
const alerts = detector.analyzeResult(tc, targetValue, false);
|
|
783
|
+
const latencyAlerts = alerts.filter(a => a.anomaly_type === 'latency_spike');
|
|
784
|
+
if (latencyAlerts.length > 0) {
|
|
785
|
+
expect(latencyAlerts[0].severity).toBe('low');
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
// -----------------------------------------------------------------------
|
|
791
|
+
// Gateway integration (action modes)
|
|
792
|
+
// -----------------------------------------------------------------------
|
|
793
|
+
describe('action modes', () => {
|
|
794
|
+
it('should not block on action: "log"', () => {
|
|
795
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig({ action: 'log' }));
|
|
796
|
+
// Even if alerts are generated, action=log means we just record them
|
|
797
|
+
detector.analyze(buildToolCall({ actorId: 'log-test', toolName: 'tool-a' }));
|
|
798
|
+
const alerts = detector.analyze(buildToolCall({ actorId: 'log-test', toolName: 'tool-b' }));
|
|
799
|
+
// Alerts are generated but action is just 'log'
|
|
800
|
+
const newToolAlerts = alerts.filter(a => a.anomaly_type === 'new_tool_usage');
|
|
801
|
+
expect(newToolAlerts).toHaveLength(1);
|
|
802
|
+
// The detector itself doesn't enforce actions -- that's the gateway's job
|
|
803
|
+
// We just verify alerts are still produced
|
|
804
|
+
});
|
|
805
|
+
it('should not block on action: "flag"', () => {
|
|
806
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig({ action: 'flag' }));
|
|
807
|
+
detector.analyze(buildToolCall({ actorId: 'flag-test', toolName: 'tool-a' }));
|
|
808
|
+
const alerts = detector.analyze(buildToolCall({ actorId: 'flag-test', toolName: 'tool-b' }));
|
|
809
|
+
const newToolAlerts = alerts.filter(a => a.anomaly_type === 'new_tool_usage');
|
|
810
|
+
expect(newToolAlerts).toHaveLength(1);
|
|
811
|
+
});
|
|
812
|
+
it('action: "block" with high severity should produce alerts for gateway to act on', () => {
|
|
813
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig({ action: 'block' }));
|
|
814
|
+
// Generate a high severity alert (capability escalation to admin)
|
|
815
|
+
detector.analyze(buildToolCall({ actorId: 'block-test', capability: 'read' }));
|
|
816
|
+
const alerts = detector.analyze(buildToolCall({ actorId: 'block-test', capability: 'admin' }));
|
|
817
|
+
const highAlerts = alerts.filter(a => a.severity === 'high');
|
|
818
|
+
expect(highAlerts.length).toBeGreaterThanOrEqual(1);
|
|
819
|
+
});
|
|
820
|
+
});
|
|
821
|
+
// -----------------------------------------------------------------------
|
|
822
|
+
// Multiple anomaly types in single call
|
|
823
|
+
// -----------------------------------------------------------------------
|
|
824
|
+
describe('multiple anomaly types in single call', () => {
|
|
825
|
+
beforeEach(() => {
|
|
826
|
+
jest.useFakeTimers();
|
|
827
|
+
});
|
|
828
|
+
afterEach(() => {
|
|
829
|
+
jest.useRealTimers();
|
|
830
|
+
});
|
|
831
|
+
it('should detect multiple anomaly types simultaneously', () => {
|
|
832
|
+
// Establish history during business hours on a weekday
|
|
833
|
+
jest.setSystemTime(new Date('2025-01-15T14:00:00Z')); // Wednesday 2pm
|
|
834
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
835
|
+
// First, establish some history during business hours (no off-hours alert)
|
|
836
|
+
detector.analyze(buildToolCall({ actorId: 'multi', toolName: 'http.get', capability: 'read' }));
|
|
837
|
+
// Now set time to Saturday 3am UTC (off hours + weekend)
|
|
838
|
+
jest.setSystemTime(new Date('2025-01-18T03:00:00Z'));
|
|
839
|
+
// Trigger multiple anomalies at once:
|
|
840
|
+
// - new_tool_usage (slack.post is new)
|
|
841
|
+
// - capability_escalation (admin is new)
|
|
842
|
+
// - off_hours_activity (3am Saturday -- first time at this hour)
|
|
843
|
+
const alerts = detector.analyze(buildToolCall({
|
|
844
|
+
actorId: 'multi',
|
|
845
|
+
toolName: 'slack.post',
|
|
846
|
+
capability: 'admin',
|
|
847
|
+
}));
|
|
848
|
+
const types = new Set(alerts.map(a => a.anomaly_type));
|
|
849
|
+
expect(types.has('new_tool_usage')).toBe(true);
|
|
850
|
+
expect(types.has('capability_escalation')).toBe(true);
|
|
851
|
+
expect(types.has('off_hours_activity')).toBe(true);
|
|
852
|
+
});
|
|
853
|
+
});
|
|
854
|
+
// -----------------------------------------------------------------------
|
|
855
|
+
// Edge cases
|
|
856
|
+
// -----------------------------------------------------------------------
|
|
857
|
+
describe('edge cases', () => {
|
|
858
|
+
it('should handle empty actor id', () => {
|
|
859
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
860
|
+
const tc = buildToolCall({ actorId: '' });
|
|
861
|
+
// Should not throw
|
|
862
|
+
const alerts = detector.analyze(tc);
|
|
863
|
+
expect(Array.isArray(alerts)).toBe(true);
|
|
864
|
+
});
|
|
865
|
+
it('should handle empty tool name', () => {
|
|
866
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
867
|
+
const tc = buildToolCall({ toolName: '' });
|
|
868
|
+
const alerts = detector.analyze(tc);
|
|
869
|
+
expect(Array.isArray(alerts)).toBe(true);
|
|
870
|
+
});
|
|
871
|
+
it('should handle zero latency', () => {
|
|
872
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
873
|
+
const tc = buildToolCall();
|
|
874
|
+
detector.recordResult(tc, 0, false);
|
|
875
|
+
const baseline = detector.getBaseline('tool', tc.tool.name, 'latency');
|
|
876
|
+
expect(baseline.mean).toBe(0);
|
|
877
|
+
});
|
|
878
|
+
it('should handle negative latency gracefully', () => {
|
|
879
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
880
|
+
const tc = buildToolCall();
|
|
881
|
+
// Shouldn't throw
|
|
882
|
+
detector.recordResult(tc, -1, false);
|
|
883
|
+
const baseline = detector.getBaseline('tool', tc.tool.name, 'latency');
|
|
884
|
+
expect(baseline).not.toBeNull();
|
|
885
|
+
});
|
|
886
|
+
it('should handle very large latency values', () => {
|
|
887
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
888
|
+
const tc = buildToolCall({ toolName: 'http.large' });
|
|
889
|
+
detector.recordResult(tc, Number.MAX_SAFE_INTEGER, false);
|
|
890
|
+
const baseline = detector.getBaseline('tool', 'http.large', 'latency');
|
|
891
|
+
expect(baseline.mean).toBe(Number.MAX_SAFE_INTEGER);
|
|
892
|
+
});
|
|
893
|
+
it('should handle concurrent analysis of many actors', () => {
|
|
894
|
+
const detector = new anomaly_1.AnomalyDetector(defaultConfig());
|
|
895
|
+
// Simulate many different actors
|
|
896
|
+
for (let i = 0; i < 100; i++) {
|
|
897
|
+
const alerts = detector.analyze(buildToolCall({ actorId: `actor-${i}` }));
|
|
898
|
+
expect(Array.isArray(alerts)).toBe(true);
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
});
|
|
902
|
+
});
|
|
903
|
+
//# sourceMappingURL=anomaly-detector.test.js.map
|