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,1813 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const http = __importStar(require("http"));
|
|
37
|
+
const opa_engine_1 = require("../../src/policy/opa-engine");
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Helpers
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
/** Build a minimal valid ToolCall for testing. */
|
|
42
|
+
function buildToolCall(overrides = {}) {
|
|
43
|
+
return {
|
|
44
|
+
tool_call_id: 'tc-opa-001',
|
|
45
|
+
task_id: 'task-opa-001',
|
|
46
|
+
workspace_id: 'ws-default',
|
|
47
|
+
actor: {
|
|
48
|
+
type: 'agent',
|
|
49
|
+
id: 'agent-1',
|
|
50
|
+
...(overrides.actor || {}),
|
|
51
|
+
},
|
|
52
|
+
source: {
|
|
53
|
+
platform: 'langgraph',
|
|
54
|
+
...(overrides.source || {}),
|
|
55
|
+
},
|
|
56
|
+
tool: {
|
|
57
|
+
name: 'http.request',
|
|
58
|
+
capability: 'read',
|
|
59
|
+
...(overrides.tool || {}),
|
|
60
|
+
},
|
|
61
|
+
args: {
|
|
62
|
+
method: 'GET',
|
|
63
|
+
url: 'https://api.github.com/repos',
|
|
64
|
+
...(overrides.args || {}),
|
|
65
|
+
},
|
|
66
|
+
context: overrides.context,
|
|
67
|
+
constraints: overrides.constraints,
|
|
68
|
+
timestamp: overrides.timestamp,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Remote OPA evaluation (mock HTTP server)
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
describe('OPAEngine - Remote evaluation', () => {
|
|
75
|
+
let opaServer;
|
|
76
|
+
let opaPort;
|
|
77
|
+
let serverResponse;
|
|
78
|
+
let lastRequestBody;
|
|
79
|
+
let serverShouldTimeout;
|
|
80
|
+
let serverShouldError;
|
|
81
|
+
let serverShouldReturnInvalidJson;
|
|
82
|
+
beforeAll((done) => {
|
|
83
|
+
opaServer = http.createServer((req, res) => {
|
|
84
|
+
if (serverShouldTimeout) {
|
|
85
|
+
// Do nothing; let the client timeout
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (serverShouldError) {
|
|
89
|
+
res.destroy();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (serverShouldReturnInvalidJson) {
|
|
93
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
94
|
+
res.end('not valid json{{{');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
let body = '';
|
|
98
|
+
req.on('data', (chunk) => body += chunk);
|
|
99
|
+
req.on('end', () => {
|
|
100
|
+
lastRequestBody = JSON.parse(body);
|
|
101
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
102
|
+
res.end(JSON.stringify(serverResponse));
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
opaServer.listen(0, '127.0.0.1', () => {
|
|
106
|
+
opaPort = opaServer.address().port;
|
|
107
|
+
done();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
afterAll((done) => {
|
|
111
|
+
opaServer.close(done);
|
|
112
|
+
});
|
|
113
|
+
beforeEach(() => {
|
|
114
|
+
serverResponse = { result: { decision: 'allow' } };
|
|
115
|
+
lastRequestBody = null;
|
|
116
|
+
serverShouldTimeout = false;
|
|
117
|
+
serverShouldError = false;
|
|
118
|
+
serverShouldReturnInvalidJson = false;
|
|
119
|
+
});
|
|
120
|
+
test('should evaluate allow decision from OPA server', async () => {
|
|
121
|
+
serverResponse = {
|
|
122
|
+
result: {
|
|
123
|
+
decision: 'allow',
|
|
124
|
+
rule_id: 'opa_allow_reads',
|
|
125
|
+
rule_name: 'Allow read operations',
|
|
126
|
+
reasons: ['Read operations are permitted'],
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
130
|
+
enabled: true,
|
|
131
|
+
server_url: `http://127.0.0.1:${opaPort}`,
|
|
132
|
+
});
|
|
133
|
+
const result = await engine.evaluate(buildToolCall());
|
|
134
|
+
expect(result.decision).toBe('allow');
|
|
135
|
+
expect(result.rule_id).toBe('opa_allow_reads');
|
|
136
|
+
expect(result.rule_name).toBe('Allow read operations');
|
|
137
|
+
expect(result.reasons).toEqual(['Read operations are permitted']);
|
|
138
|
+
});
|
|
139
|
+
test('should evaluate deny decision from OPA server', async () => {
|
|
140
|
+
serverResponse = {
|
|
141
|
+
result: {
|
|
142
|
+
decision: 'deny',
|
|
143
|
+
rule_id: 'opa_deny_writes',
|
|
144
|
+
rule_name: 'Deny write operations',
|
|
145
|
+
reasons: ['Write operations are blocked in this environment'],
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
149
|
+
enabled: true,
|
|
150
|
+
server_url: `http://127.0.0.1:${opaPort}`,
|
|
151
|
+
});
|
|
152
|
+
const tc = buildToolCall({ tool: { name: 'http.request', capability: 'write' } });
|
|
153
|
+
const result = await engine.evaluate(tc);
|
|
154
|
+
expect(result.decision).toBe('deny');
|
|
155
|
+
expect(result.rule_id).toBe('opa_deny_writes');
|
|
156
|
+
expect(result.reasons).toContain('Write operations are blocked in this environment');
|
|
157
|
+
});
|
|
158
|
+
test('should evaluate transform decision with transformations', async () => {
|
|
159
|
+
serverResponse = {
|
|
160
|
+
result: {
|
|
161
|
+
decision: 'transform',
|
|
162
|
+
rule_id: 'opa_strip_auth',
|
|
163
|
+
rule_name: 'Strip auth headers',
|
|
164
|
+
reasons: ['Auth headers must be stripped for external calls'],
|
|
165
|
+
transformations: [
|
|
166
|
+
{ type: 'strip_header', target: 'Authorization' },
|
|
167
|
+
],
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
171
|
+
enabled: true,
|
|
172
|
+
server_url: `http://127.0.0.1:${opaPort}`,
|
|
173
|
+
});
|
|
174
|
+
const result = await engine.evaluate(buildToolCall());
|
|
175
|
+
expect(result.decision).toBe('transform');
|
|
176
|
+
expect(result.transformations).toHaveLength(1);
|
|
177
|
+
expect(result.transformations[0]).toEqual({ type: 'strip_header', target: 'Authorization' });
|
|
178
|
+
});
|
|
179
|
+
test('should evaluate require_approval decision with approval info', async () => {
|
|
180
|
+
serverResponse = {
|
|
181
|
+
result: {
|
|
182
|
+
decision: 'require_approval',
|
|
183
|
+
rule_id: 'opa_admin_approval',
|
|
184
|
+
rule_name: 'Admin operations need approval',
|
|
185
|
+
reasons: ['Admin capability requires human approval'],
|
|
186
|
+
approval: { scope: 'security', ttl_seconds: 7200, reason: 'Admin operation detected' },
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
190
|
+
enabled: true,
|
|
191
|
+
server_url: `http://127.0.0.1:${opaPort}`,
|
|
192
|
+
});
|
|
193
|
+
const tc = buildToolCall({ tool: { name: 'http.request', capability: 'admin' } });
|
|
194
|
+
const result = await engine.evaluate(tc);
|
|
195
|
+
expect(result.decision).toBe('require_approval');
|
|
196
|
+
expect(result.approval).toEqual({ scope: 'security', ttl_seconds: 7200, reason: 'Admin operation detected' });
|
|
197
|
+
});
|
|
198
|
+
test('should send correct input format to OPA server', async () => {
|
|
199
|
+
serverResponse = { result: { decision: 'allow' } };
|
|
200
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
201
|
+
enabled: true,
|
|
202
|
+
server_url: `http://127.0.0.1:${opaPort}`,
|
|
203
|
+
});
|
|
204
|
+
const tc = buildToolCall({
|
|
205
|
+
tool: { name: 'http.get', capability: 'read', version: '2.0' },
|
|
206
|
+
context: { purpose: 'test', labels: ['ci'] },
|
|
207
|
+
});
|
|
208
|
+
await engine.evaluate(tc);
|
|
209
|
+
expect(lastRequestBody).toBeDefined();
|
|
210
|
+
expect(lastRequestBody.input).toBeDefined();
|
|
211
|
+
expect(lastRequestBody.input.tool_call_id).toBe('tc-opa-001');
|
|
212
|
+
expect(lastRequestBody.input.task_id).toBe('task-opa-001');
|
|
213
|
+
expect(lastRequestBody.input.tool.name).toBe('http.get');
|
|
214
|
+
expect(lastRequestBody.input.tool.capability).toBe('read');
|
|
215
|
+
expect(lastRequestBody.input.context.labels).toEqual(['ci']);
|
|
216
|
+
});
|
|
217
|
+
test('should use custom policy_path', async () => {
|
|
218
|
+
serverResponse = { result: { decision: 'allow' } };
|
|
219
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
220
|
+
enabled: true,
|
|
221
|
+
server_url: `http://127.0.0.1:${opaPort}`,
|
|
222
|
+
policy_path: 'v1/data/custom/path',
|
|
223
|
+
});
|
|
224
|
+
await engine.evaluate(buildToolCall());
|
|
225
|
+
// If the server received the request successfully, it means the correct path was used
|
|
226
|
+
expect(lastRequestBody).toBeDefined();
|
|
227
|
+
});
|
|
228
|
+
test('should fall back when OPA server times out', async () => {
|
|
229
|
+
serverShouldTimeout = true;
|
|
230
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
231
|
+
enabled: true,
|
|
232
|
+
server_url: `http://127.0.0.1:${opaPort}`,
|
|
233
|
+
timeout_ms: 200,
|
|
234
|
+
fallback_decision: 'deny',
|
|
235
|
+
});
|
|
236
|
+
const result = await engine.evaluate(buildToolCall());
|
|
237
|
+
expect(result.decision).toBe('deny');
|
|
238
|
+
expect(result.rule_id).toBe('opa_fallback');
|
|
239
|
+
expect(result.reasons[0]).toContain('OPA server error');
|
|
240
|
+
});
|
|
241
|
+
test('should fall back when OPA server returns error', async () => {
|
|
242
|
+
serverShouldError = true;
|
|
243
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
244
|
+
enabled: true,
|
|
245
|
+
server_url: `http://127.0.0.1:${opaPort}`,
|
|
246
|
+
fallback_decision: 'deny',
|
|
247
|
+
});
|
|
248
|
+
const result = await engine.evaluate(buildToolCall());
|
|
249
|
+
expect(result.decision).toBe('deny');
|
|
250
|
+
expect(result.rule_id).toBe('opa_fallback');
|
|
251
|
+
expect(result.reasons[0]).toContain('OPA server error');
|
|
252
|
+
});
|
|
253
|
+
test('should fall back when OPA returns invalid JSON', async () => {
|
|
254
|
+
serverShouldReturnInvalidJson = true;
|
|
255
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
256
|
+
enabled: true,
|
|
257
|
+
server_url: `http://127.0.0.1:${opaPort}`,
|
|
258
|
+
fallback_decision: 'deny',
|
|
259
|
+
});
|
|
260
|
+
const result = await engine.evaluate(buildToolCall());
|
|
261
|
+
expect(result.decision).toBe('deny');
|
|
262
|
+
expect(result.rule_id).toBe('opa_fallback');
|
|
263
|
+
expect(result.reasons[0]).toContain('OPA server error');
|
|
264
|
+
});
|
|
265
|
+
test('should fall back when OPA returns empty result', async () => {
|
|
266
|
+
serverResponse = {};
|
|
267
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
268
|
+
enabled: true,
|
|
269
|
+
server_url: `http://127.0.0.1:${opaPort}`,
|
|
270
|
+
fallback_decision: 'deny',
|
|
271
|
+
});
|
|
272
|
+
const result = await engine.evaluate(buildToolCall());
|
|
273
|
+
expect(result.decision).toBe('deny');
|
|
274
|
+
expect(result.rule_id).toBe('opa_fallback');
|
|
275
|
+
expect(result.reasons[0]).toBe('OPA returned empty result');
|
|
276
|
+
});
|
|
277
|
+
test('should fall back when OPA returns result without decision', async () => {
|
|
278
|
+
serverResponse = { result: { rule_id: 'some_rule' } };
|
|
279
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
280
|
+
enabled: true,
|
|
281
|
+
server_url: `http://127.0.0.1:${opaPort}`,
|
|
282
|
+
fallback_decision: 'deny',
|
|
283
|
+
});
|
|
284
|
+
const result = await engine.evaluate(buildToolCall());
|
|
285
|
+
// normalizeDecision handles undefined → fallback
|
|
286
|
+
expect(result.decision).toBe('deny');
|
|
287
|
+
});
|
|
288
|
+
test('should use allow as fallback decision when configured', async () => {
|
|
289
|
+
serverShouldError = true;
|
|
290
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
291
|
+
enabled: true,
|
|
292
|
+
server_url: `http://127.0.0.1:${opaPort}`,
|
|
293
|
+
fallback_decision: 'allow',
|
|
294
|
+
});
|
|
295
|
+
const result = await engine.evaluate(buildToolCall());
|
|
296
|
+
expect(result.decision).toBe('allow');
|
|
297
|
+
expect(result.rule_id).toBe('opa_fallback');
|
|
298
|
+
});
|
|
299
|
+
test('should return OPA server unreachable fallback for invalid URL', async () => {
|
|
300
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
301
|
+
enabled: true,
|
|
302
|
+
server_url: 'http://127.0.0.1:1', // Port 1 is unlikely to have anything listening
|
|
303
|
+
timeout_ms: 500,
|
|
304
|
+
fallback_decision: 'deny',
|
|
305
|
+
});
|
|
306
|
+
const result = await engine.evaluate(buildToolCall());
|
|
307
|
+
expect(result.decision).toBe('deny');
|
|
308
|
+
expect(result.rule_id).toBe('opa_fallback');
|
|
309
|
+
});
|
|
310
|
+
test('should use default values for reasons when OPA provides a string', async () => {
|
|
311
|
+
serverResponse = {
|
|
312
|
+
result: {
|
|
313
|
+
decision: 'deny',
|
|
314
|
+
reasons: 'single reason string',
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
318
|
+
enabled: true,
|
|
319
|
+
server_url: `http://127.0.0.1:${opaPort}`,
|
|
320
|
+
});
|
|
321
|
+
const result = await engine.evaluate(buildToolCall());
|
|
322
|
+
expect(result.reasons).toEqual(['single reason string']);
|
|
323
|
+
});
|
|
324
|
+
test('should provide default reasons when OPA returns none', async () => {
|
|
325
|
+
serverResponse = {
|
|
326
|
+
result: {
|
|
327
|
+
decision: 'allow',
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
331
|
+
enabled: true,
|
|
332
|
+
server_url: `http://127.0.0.1:${opaPort}`,
|
|
333
|
+
});
|
|
334
|
+
const result = await engine.evaluate(buildToolCall());
|
|
335
|
+
expect(result.reasons).toEqual(['OPA decision: allow']);
|
|
336
|
+
});
|
|
337
|
+
test('should provide default rule_id and rule_name when OPA returns none', async () => {
|
|
338
|
+
serverResponse = {
|
|
339
|
+
result: {
|
|
340
|
+
decision: 'allow',
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
344
|
+
enabled: true,
|
|
345
|
+
server_url: `http://127.0.0.1:${opaPort}`,
|
|
346
|
+
});
|
|
347
|
+
const result = await engine.evaluate(buildToolCall());
|
|
348
|
+
expect(result.rule_id).toBe('opa_policy');
|
|
349
|
+
expect(result.rule_name).toBe('OPA policy evaluation');
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
// ---------------------------------------------------------------------------
|
|
353
|
+
// Local Rego evaluation
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
describe('OPAEngine - Local Rego evaluation', () => {
|
|
356
|
+
test('should deny by default with basic deny policy', async () => {
|
|
357
|
+
const policy = `
|
|
358
|
+
package palaryn.policy
|
|
359
|
+
default decision = "deny"
|
|
360
|
+
`;
|
|
361
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
362
|
+
enabled: true,
|
|
363
|
+
rego_policy: policy,
|
|
364
|
+
});
|
|
365
|
+
const result = await engine.evaluate(buildToolCall());
|
|
366
|
+
expect(result.decision).toBe('deny');
|
|
367
|
+
expect(result.rule_id).toBe('opa_local');
|
|
368
|
+
});
|
|
369
|
+
test('should allow by default with basic allow policy', async () => {
|
|
370
|
+
const policy = `
|
|
371
|
+
package palaryn.policy
|
|
372
|
+
default decision = "allow"
|
|
373
|
+
`;
|
|
374
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
375
|
+
enabled: true,
|
|
376
|
+
rego_policy: policy,
|
|
377
|
+
});
|
|
378
|
+
const result = await engine.evaluate(buildToolCall());
|
|
379
|
+
expect(result.decision).toBe('allow');
|
|
380
|
+
});
|
|
381
|
+
test('should evaluate allow rule with capability condition', async () => {
|
|
382
|
+
const policy = `
|
|
383
|
+
package palaryn.policy
|
|
384
|
+
default decision = "deny"
|
|
385
|
+
decision = "allow" {
|
|
386
|
+
input.tool.capability == "read"
|
|
387
|
+
}
|
|
388
|
+
`;
|
|
389
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
390
|
+
enabled: true,
|
|
391
|
+
rego_policy: policy,
|
|
392
|
+
});
|
|
393
|
+
// Read capability should be allowed
|
|
394
|
+
const readResult = await engine.evaluate(buildToolCall({ tool: { name: 'http.request', capability: 'read' } }));
|
|
395
|
+
expect(readResult.decision).toBe('allow');
|
|
396
|
+
// Write capability should be denied
|
|
397
|
+
const writeResult = await engine.evaluate(buildToolCall({ tool: { name: 'http.request', capability: 'write' } }));
|
|
398
|
+
expect(writeResult.decision).toBe('deny');
|
|
399
|
+
});
|
|
400
|
+
test('should evaluate deny rule for specific tool name', async () => {
|
|
401
|
+
const policy = `
|
|
402
|
+
package palaryn.policy
|
|
403
|
+
default decision = "allow"
|
|
404
|
+
decision = "deny" {
|
|
405
|
+
input.tool.name == "dangerous"
|
|
406
|
+
}
|
|
407
|
+
`;
|
|
408
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
409
|
+
enabled: true,
|
|
410
|
+
rego_policy: policy,
|
|
411
|
+
});
|
|
412
|
+
// Normal tool should be allowed
|
|
413
|
+
const normalResult = await engine.evaluate(buildToolCall());
|
|
414
|
+
expect(normalResult.decision).toBe('allow');
|
|
415
|
+
// Dangerous tool should be denied
|
|
416
|
+
const dangerousResult = await engine.evaluate(buildToolCall({ tool: { name: 'dangerous', capability: 'read' } }));
|
|
417
|
+
expect(dangerousResult.decision).toBe('deny');
|
|
418
|
+
});
|
|
419
|
+
test('should handle multiple rules (last match wins)', async () => {
|
|
420
|
+
const policy = `
|
|
421
|
+
package palaryn.policy
|
|
422
|
+
default decision = "deny"
|
|
423
|
+
decision = "allow" {
|
|
424
|
+
input.tool.capability == "read"
|
|
425
|
+
}
|
|
426
|
+
decision = "deny" {
|
|
427
|
+
input.tool.name == "http.dangerous"
|
|
428
|
+
}
|
|
429
|
+
`;
|
|
430
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
431
|
+
enabled: true,
|
|
432
|
+
rego_policy: policy,
|
|
433
|
+
});
|
|
434
|
+
// A read tool that isn't dangerous should be allowed
|
|
435
|
+
const readResult = await engine.evaluate(buildToolCall({ tool: { name: 'http.request', capability: 'read' } }));
|
|
436
|
+
expect(readResult.decision).toBe('allow');
|
|
437
|
+
// A dangerous tool with read capability -- both rules match, last wins (deny)
|
|
438
|
+
const dangerousRead = await engine.evaluate(buildToolCall({ tool: { name: 'http.dangerous', capability: 'read' } }));
|
|
439
|
+
expect(dangerousRead.decision).toBe('deny');
|
|
440
|
+
});
|
|
441
|
+
test('should collect reasons from reasons rules', async () => {
|
|
442
|
+
const policy = `
|
|
443
|
+
package palaryn.policy
|
|
444
|
+
default decision = "deny"
|
|
445
|
+
decision = "deny" {
|
|
446
|
+
input.tool.capability == "admin"
|
|
447
|
+
}
|
|
448
|
+
reasons[reason] {
|
|
449
|
+
input.tool.capability == "admin"
|
|
450
|
+
reason := "Admin capability requires approval"
|
|
451
|
+
}
|
|
452
|
+
`;
|
|
453
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
454
|
+
enabled: true,
|
|
455
|
+
rego_policy: policy,
|
|
456
|
+
});
|
|
457
|
+
const result = await engine.evaluate(buildToolCall({ tool: { name: 'http.request', capability: 'admin' } }));
|
|
458
|
+
expect(result.decision).toBe('deny');
|
|
459
|
+
expect(result.reasons).toContain('Admin capability requires approval');
|
|
460
|
+
});
|
|
461
|
+
test('should handle inequality conditions', async () => {
|
|
462
|
+
const policy = `
|
|
463
|
+
package palaryn.policy
|
|
464
|
+
default decision = "deny"
|
|
465
|
+
decision = "allow" {
|
|
466
|
+
input.tool.capability != "admin"
|
|
467
|
+
}
|
|
468
|
+
`;
|
|
469
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
470
|
+
enabled: true,
|
|
471
|
+
rego_policy: policy,
|
|
472
|
+
});
|
|
473
|
+
const readResult = await engine.evaluate(buildToolCall({ tool: { name: 'http.request', capability: 'read' } }));
|
|
474
|
+
expect(readResult.decision).toBe('allow');
|
|
475
|
+
const adminResult = await engine.evaluate(buildToolCall({ tool: { name: 'http.request', capability: 'admin' } }));
|
|
476
|
+
expect(adminResult.decision).toBe('deny');
|
|
477
|
+
});
|
|
478
|
+
test('should handle AND logic (multiple conditions in a rule)', async () => {
|
|
479
|
+
const policy = `
|
|
480
|
+
package palaryn.policy
|
|
481
|
+
default decision = "deny"
|
|
482
|
+
decision = "allow" {
|
|
483
|
+
input.tool.capability == "read"
|
|
484
|
+
input.source.platform == "langgraph"
|
|
485
|
+
}
|
|
486
|
+
`;
|
|
487
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
488
|
+
enabled: true,
|
|
489
|
+
rego_policy: policy,
|
|
490
|
+
});
|
|
491
|
+
// Both conditions met
|
|
492
|
+
const result1 = await engine.evaluate(buildToolCall({
|
|
493
|
+
tool: { name: 'http.request', capability: 'read' },
|
|
494
|
+
source: { platform: 'langgraph' },
|
|
495
|
+
}));
|
|
496
|
+
expect(result1.decision).toBe('allow');
|
|
497
|
+
// Only one condition met
|
|
498
|
+
const result2 = await engine.evaluate(buildToolCall({
|
|
499
|
+
tool: { name: 'http.request', capability: 'read' },
|
|
500
|
+
source: { platform: 'n8n' },
|
|
501
|
+
}));
|
|
502
|
+
expect(result2.decision).toBe('deny');
|
|
503
|
+
});
|
|
504
|
+
test('should handle actor type conditions', async () => {
|
|
505
|
+
const policy = `
|
|
506
|
+
package palaryn.policy
|
|
507
|
+
default decision = "deny"
|
|
508
|
+
decision = "allow" {
|
|
509
|
+
input.actor.type == "user"
|
|
510
|
+
}
|
|
511
|
+
`;
|
|
512
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
513
|
+
enabled: true,
|
|
514
|
+
rego_policy: policy,
|
|
515
|
+
});
|
|
516
|
+
const agentResult = await engine.evaluate(buildToolCall({ actor: { type: 'agent', id: 'a1' } }));
|
|
517
|
+
expect(agentResult.decision).toBe('deny');
|
|
518
|
+
const userResult = await engine.evaluate(buildToolCall({ actor: { type: 'user', id: 'u1' } }));
|
|
519
|
+
expect(userResult.decision).toBe('allow');
|
|
520
|
+
});
|
|
521
|
+
test('should handle workspace_id conditions', async () => {
|
|
522
|
+
const policy = `
|
|
523
|
+
package palaryn.policy
|
|
524
|
+
default decision = "deny"
|
|
525
|
+
decision = "allow" {
|
|
526
|
+
input.workspace_id == "ws-allowed"
|
|
527
|
+
}
|
|
528
|
+
`;
|
|
529
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
530
|
+
enabled: true,
|
|
531
|
+
rego_policy: policy,
|
|
532
|
+
});
|
|
533
|
+
const tc1 = {
|
|
534
|
+
...buildToolCall(),
|
|
535
|
+
workspace_id: 'ws-allowed',
|
|
536
|
+
};
|
|
537
|
+
const result1 = await engine.evaluate(tc1);
|
|
538
|
+
expect(result1.decision).toBe('allow');
|
|
539
|
+
const tc2 = {
|
|
540
|
+
...buildToolCall(),
|
|
541
|
+
workspace_id: 'ws-blocked',
|
|
542
|
+
};
|
|
543
|
+
const result2 = await engine.evaluate(tc2);
|
|
544
|
+
expect(result2.decision).toBe('deny');
|
|
545
|
+
});
|
|
546
|
+
test('should handle startswith function', async () => {
|
|
547
|
+
const policy = `
|
|
548
|
+
package palaryn.policy
|
|
549
|
+
default decision = "deny"
|
|
550
|
+
decision = "allow" {
|
|
551
|
+
startswith(input.tool.name, "http.")
|
|
552
|
+
}
|
|
553
|
+
`;
|
|
554
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
555
|
+
enabled: true,
|
|
556
|
+
rego_policy: policy,
|
|
557
|
+
});
|
|
558
|
+
const httpResult = await engine.evaluate(buildToolCall({ tool: { name: 'http.request', capability: 'read' } }));
|
|
559
|
+
expect(httpResult.decision).toBe('allow');
|
|
560
|
+
const slackResult = await engine.evaluate(buildToolCall({ tool: { name: 'slack.post', capability: 'write' } }));
|
|
561
|
+
expect(slackResult.decision).toBe('deny');
|
|
562
|
+
});
|
|
563
|
+
test('should handle endswith function', async () => {
|
|
564
|
+
const policy = `
|
|
565
|
+
package palaryn.policy
|
|
566
|
+
default decision = "deny"
|
|
567
|
+
decision = "allow" {
|
|
568
|
+
endswith(input.args.url, "/repos")
|
|
569
|
+
}
|
|
570
|
+
`;
|
|
571
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
572
|
+
enabled: true,
|
|
573
|
+
rego_policy: policy,
|
|
574
|
+
});
|
|
575
|
+
const matchResult = await engine.evaluate(buildToolCall({ args: { url: 'https://api.github.com/repos' } }));
|
|
576
|
+
expect(matchResult.decision).toBe('allow');
|
|
577
|
+
const noMatchResult = await engine.evaluate(buildToolCall({ args: { url: 'https://api.github.com/users' } }));
|
|
578
|
+
expect(noMatchResult.decision).toBe('deny');
|
|
579
|
+
});
|
|
580
|
+
test('should handle contains function', async () => {
|
|
581
|
+
const policy = `
|
|
582
|
+
package palaryn.policy
|
|
583
|
+
default decision = "deny"
|
|
584
|
+
decision = "allow" {
|
|
585
|
+
contains(input.args.url, "github.com")
|
|
586
|
+
}
|
|
587
|
+
`;
|
|
588
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
589
|
+
enabled: true,
|
|
590
|
+
rego_policy: policy,
|
|
591
|
+
});
|
|
592
|
+
const githubResult = await engine.evaluate(buildToolCall({ args: { url: 'https://api.github.com/repos' } }));
|
|
593
|
+
expect(githubResult.decision).toBe('allow');
|
|
594
|
+
const otherResult = await engine.evaluate(buildToolCall({ args: { url: 'https://api.gitlab.com/repos' } }));
|
|
595
|
+
expect(otherResult.decision).toBe('deny');
|
|
596
|
+
});
|
|
597
|
+
test('should handle truthy check condition', async () => {
|
|
598
|
+
const policy = `
|
|
599
|
+
package palaryn.policy
|
|
600
|
+
default decision = "deny"
|
|
601
|
+
decision = "allow" {
|
|
602
|
+
input.context.purpose
|
|
603
|
+
}
|
|
604
|
+
`;
|
|
605
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
606
|
+
enabled: true,
|
|
607
|
+
rego_policy: policy,
|
|
608
|
+
});
|
|
609
|
+
const withPurpose = await engine.evaluate(buildToolCall({ context: { purpose: 'testing' } }));
|
|
610
|
+
expect(withPurpose.decision).toBe('allow');
|
|
611
|
+
const withoutPurpose = await engine.evaluate(buildToolCall());
|
|
612
|
+
expect(withoutPurpose.decision).toBe('deny');
|
|
613
|
+
});
|
|
614
|
+
test('should handle not condition', async () => {
|
|
615
|
+
const policy = `
|
|
616
|
+
package palaryn.policy
|
|
617
|
+
default decision = "deny"
|
|
618
|
+
decision = "allow" {
|
|
619
|
+
not input.constraints.max_cost_usd
|
|
620
|
+
}
|
|
621
|
+
`;
|
|
622
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
623
|
+
enabled: true,
|
|
624
|
+
rego_policy: policy,
|
|
625
|
+
});
|
|
626
|
+
const noConstraints = await engine.evaluate(buildToolCall());
|
|
627
|
+
expect(noConstraints.decision).toBe('allow');
|
|
628
|
+
const withConstraints = await engine.evaluate(buildToolCall({ constraints: { max_cost_usd: 5 } }));
|
|
629
|
+
expect(withConstraints.decision).toBe('deny');
|
|
630
|
+
});
|
|
631
|
+
test('should handle custom default rule_id and rule_name', async () => {
|
|
632
|
+
const policy = `
|
|
633
|
+
package palaryn.policy
|
|
634
|
+
default decision = "allow"
|
|
635
|
+
default rule_id = "custom_opa"
|
|
636
|
+
default rule_name = "Custom OPA Policy"
|
|
637
|
+
`;
|
|
638
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
639
|
+
enabled: true,
|
|
640
|
+
rego_policy: policy,
|
|
641
|
+
});
|
|
642
|
+
const result = await engine.evaluate(buildToolCall());
|
|
643
|
+
expect(result.decision).toBe('allow');
|
|
644
|
+
expect(result.rule_id).toBe('custom_opa');
|
|
645
|
+
expect(result.rule_name).toBe('Custom OPA Policy');
|
|
646
|
+
});
|
|
647
|
+
test('should provide default reason when no reasons rules match', async () => {
|
|
648
|
+
const policy = `
|
|
649
|
+
package palaryn.policy
|
|
650
|
+
default decision = "allow"
|
|
651
|
+
`;
|
|
652
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
653
|
+
enabled: true,
|
|
654
|
+
rego_policy: policy,
|
|
655
|
+
});
|
|
656
|
+
const result = await engine.evaluate(buildToolCall());
|
|
657
|
+
expect(result.reasons).toEqual(['OPA local decision: allow']);
|
|
658
|
+
});
|
|
659
|
+
test('should handle require_approval decision locally', async () => {
|
|
660
|
+
const policy = `
|
|
661
|
+
package palaryn.policy
|
|
662
|
+
default decision = "deny"
|
|
663
|
+
decision = "require_approval" {
|
|
664
|
+
input.tool.capability == "delete"
|
|
665
|
+
}
|
|
666
|
+
`;
|
|
667
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
668
|
+
enabled: true,
|
|
669
|
+
rego_policy: policy,
|
|
670
|
+
});
|
|
671
|
+
const result = await engine.evaluate(buildToolCall({ tool: { name: 'http.request', capability: 'delete' } }));
|
|
672
|
+
expect(result.decision).toBe('require_approval');
|
|
673
|
+
});
|
|
674
|
+
test('should handle transform decision locally', async () => {
|
|
675
|
+
const policy = `
|
|
676
|
+
package palaryn.policy
|
|
677
|
+
default decision = "deny"
|
|
678
|
+
decision = "transform" {
|
|
679
|
+
input.tool.capability == "write"
|
|
680
|
+
}
|
|
681
|
+
`;
|
|
682
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
683
|
+
enabled: true,
|
|
684
|
+
rego_policy: policy,
|
|
685
|
+
});
|
|
686
|
+
const result = await engine.evaluate(buildToolCall({ tool: { name: 'http.request', capability: 'write' } }));
|
|
687
|
+
expect(result.decision).toBe('transform');
|
|
688
|
+
});
|
|
689
|
+
test('should skip comment lines in Rego policy', async () => {
|
|
690
|
+
const policy = `
|
|
691
|
+
package palaryn.policy
|
|
692
|
+
# This is a comment
|
|
693
|
+
default decision = "allow"
|
|
694
|
+
# Another comment
|
|
695
|
+
decision = "deny" {
|
|
696
|
+
input.tool.name == "blocked"
|
|
697
|
+
}
|
|
698
|
+
`;
|
|
699
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
700
|
+
enabled: true,
|
|
701
|
+
rego_policy: policy,
|
|
702
|
+
});
|
|
703
|
+
const result = await engine.evaluate(buildToolCall());
|
|
704
|
+
expect(result.decision).toBe('allow');
|
|
705
|
+
});
|
|
706
|
+
test('should fall back on Rego evaluation error', async () => {
|
|
707
|
+
// Test with empty rego_policy (edge case)
|
|
708
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
709
|
+
enabled: true,
|
|
710
|
+
rego_policy: '',
|
|
711
|
+
fallback_decision: 'deny',
|
|
712
|
+
});
|
|
713
|
+
const result = await engine.evaluate(buildToolCall());
|
|
714
|
+
// Empty policy = no defaults, so fallback to normalizeDecision(undefined) = 'deny'
|
|
715
|
+
expect(result.decision).toBe('deny');
|
|
716
|
+
});
|
|
717
|
+
test('should handle boolean comparison in conditions', async () => {
|
|
718
|
+
const policy = `
|
|
719
|
+
package palaryn.policy
|
|
720
|
+
default decision = "deny"
|
|
721
|
+
decision = "allow" {
|
|
722
|
+
input.tool.capability == "read"
|
|
723
|
+
}
|
|
724
|
+
`;
|
|
725
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
726
|
+
enabled: true,
|
|
727
|
+
rego_policy: policy,
|
|
728
|
+
});
|
|
729
|
+
const result = await engine.evaluate(buildToolCall({ tool: { name: 'test', capability: 'read' } }));
|
|
730
|
+
expect(result.decision).toBe('allow');
|
|
731
|
+
});
|
|
732
|
+
});
|
|
733
|
+
// ---------------------------------------------------------------------------
|
|
734
|
+
// Decision normalization
|
|
735
|
+
// ---------------------------------------------------------------------------
|
|
736
|
+
describe('OPAEngine - Decision normalization', () => {
|
|
737
|
+
let engine;
|
|
738
|
+
beforeEach(() => {
|
|
739
|
+
engine = new opa_engine_1.OPAEngine({
|
|
740
|
+
enabled: true,
|
|
741
|
+
fallback_decision: 'deny',
|
|
742
|
+
});
|
|
743
|
+
});
|
|
744
|
+
test('should pass through valid decisions', () => {
|
|
745
|
+
expect(engine.normalizeDecision('allow')).toBe('allow');
|
|
746
|
+
expect(engine.normalizeDecision('deny')).toBe('deny');
|
|
747
|
+
expect(engine.normalizeDecision('transform')).toBe('transform');
|
|
748
|
+
expect(engine.normalizeDecision('require_approval')).toBe('require_approval');
|
|
749
|
+
});
|
|
750
|
+
test('should handle case-insensitive decisions', () => {
|
|
751
|
+
expect(engine.normalizeDecision('ALLOW')).toBe('allow');
|
|
752
|
+
expect(engine.normalizeDecision('DENY')).toBe('deny');
|
|
753
|
+
expect(engine.normalizeDecision('Transform')).toBe('transform');
|
|
754
|
+
expect(engine.normalizeDecision('REQUIRE_APPROVAL')).toBe('require_approval');
|
|
755
|
+
});
|
|
756
|
+
test('should map common OPA boolean patterns', () => {
|
|
757
|
+
expect(engine.normalizeDecision('true')).toBe('allow');
|
|
758
|
+
expect(engine.normalizeDecision('allowed')).toBe('allow');
|
|
759
|
+
expect(engine.normalizeDecision('false')).toBe('deny');
|
|
760
|
+
expect(engine.normalizeDecision('denied')).toBe('deny');
|
|
761
|
+
});
|
|
762
|
+
test('should return fallback for undefined decision', () => {
|
|
763
|
+
expect(engine.normalizeDecision(undefined)).toBe('deny');
|
|
764
|
+
});
|
|
765
|
+
test('should return fallback for unknown decision strings', () => {
|
|
766
|
+
expect(engine.normalizeDecision('maybe')).toBe('deny');
|
|
767
|
+
expect(engine.normalizeDecision('pending')).toBe('deny');
|
|
768
|
+
expect(engine.normalizeDecision('')).toBe('deny');
|
|
769
|
+
});
|
|
770
|
+
test('should use custom fallback decision', () => {
|
|
771
|
+
const customEngine = new opa_engine_1.OPAEngine({
|
|
772
|
+
enabled: true,
|
|
773
|
+
fallback_decision: 'allow',
|
|
774
|
+
});
|
|
775
|
+
expect(customEngine.normalizeDecision(undefined)).toBe('allow');
|
|
776
|
+
expect(customEngine.normalizeDecision('unknown')).toBe('allow');
|
|
777
|
+
});
|
|
778
|
+
});
|
|
779
|
+
// ---------------------------------------------------------------------------
|
|
780
|
+
// parseOPAResponse
|
|
781
|
+
// ---------------------------------------------------------------------------
|
|
782
|
+
describe('OPAEngine - parseOPAResponse', () => {
|
|
783
|
+
let engine;
|
|
784
|
+
const tc = buildToolCall();
|
|
785
|
+
beforeEach(() => {
|
|
786
|
+
engine = new opa_engine_1.OPAEngine({ enabled: true, fallback_decision: 'deny' });
|
|
787
|
+
});
|
|
788
|
+
test('should parse complete OPA response', () => {
|
|
789
|
+
const response = {
|
|
790
|
+
result: {
|
|
791
|
+
decision: 'allow',
|
|
792
|
+
rule_id: 'rule_1',
|
|
793
|
+
rule_name: 'Rule One',
|
|
794
|
+
reasons: ['Allowed by policy'],
|
|
795
|
+
transformations: [{ type: 'strip_header', target: 'Authorization' }],
|
|
796
|
+
approval: { scope: 'admin', ttl_seconds: 3600 },
|
|
797
|
+
},
|
|
798
|
+
};
|
|
799
|
+
const result = engine.parseOPAResponse(response, tc);
|
|
800
|
+
expect(result.decision).toBe('allow');
|
|
801
|
+
expect(result.rule_id).toBe('rule_1');
|
|
802
|
+
expect(result.rule_name).toBe('Rule One');
|
|
803
|
+
expect(result.reasons).toEqual(['Allowed by policy']);
|
|
804
|
+
expect(result.transformations).toHaveLength(1);
|
|
805
|
+
expect(result.approval).toEqual({ scope: 'admin', ttl_seconds: 3600 });
|
|
806
|
+
});
|
|
807
|
+
test('should handle null response', () => {
|
|
808
|
+
const result = engine.parseOPAResponse(null, tc);
|
|
809
|
+
expect(result.decision).toBe('deny');
|
|
810
|
+
expect(result.rule_id).toBe('opa_fallback');
|
|
811
|
+
});
|
|
812
|
+
test('should handle response with no result', () => {
|
|
813
|
+
const result = engine.parseOPAResponse({}, tc);
|
|
814
|
+
expect(result.decision).toBe('deny');
|
|
815
|
+
expect(result.rule_id).toBe('opa_fallback');
|
|
816
|
+
});
|
|
817
|
+
test('should handle response with partial result', () => {
|
|
818
|
+
const response = {
|
|
819
|
+
result: {
|
|
820
|
+
decision: 'deny',
|
|
821
|
+
},
|
|
822
|
+
};
|
|
823
|
+
const result = engine.parseOPAResponse(response, tc);
|
|
824
|
+
expect(result.decision).toBe('deny');
|
|
825
|
+
expect(result.rule_id).toBe('opa_policy');
|
|
826
|
+
expect(result.rule_name).toBe('OPA policy evaluation');
|
|
827
|
+
expect(result.reasons).toEqual(['OPA decision: deny']);
|
|
828
|
+
expect(result.transformations).toBeUndefined();
|
|
829
|
+
});
|
|
830
|
+
});
|
|
831
|
+
// ---------------------------------------------------------------------------
|
|
832
|
+
// buildInput
|
|
833
|
+
// ---------------------------------------------------------------------------
|
|
834
|
+
describe('OPAEngine - buildInput', () => {
|
|
835
|
+
test('should convert ToolCall to OPA input format', () => {
|
|
836
|
+
const engine = new opa_engine_1.OPAEngine({ enabled: true });
|
|
837
|
+
const tc = buildToolCall({
|
|
838
|
+
tool: { name: 'http.get', capability: 'read', version: '1.0' },
|
|
839
|
+
context: { purpose: 'testing', labels: ['ci', 'automated'] },
|
|
840
|
+
constraints: { max_cost_usd: 1.0, timeout_ms: 5000 },
|
|
841
|
+
});
|
|
842
|
+
tc.timestamp = '2025-01-01T00:00:00Z';
|
|
843
|
+
const input = engine.buildInput(tc);
|
|
844
|
+
expect(input.tool_call_id).toBe('tc-opa-001');
|
|
845
|
+
expect(input.task_id).toBe('task-opa-001');
|
|
846
|
+
expect(input.workspace_id).toBe('ws-default');
|
|
847
|
+
expect(input.actor.type).toBe('agent');
|
|
848
|
+
expect(input.tool.name).toBe('http.get');
|
|
849
|
+
expect(input.tool.capability).toBe('read');
|
|
850
|
+
expect(input.context.purpose).toBe('testing');
|
|
851
|
+
expect(input.constraints.max_cost_usd).toBe(1.0);
|
|
852
|
+
expect(input.timestamp).toBe('2025-01-01T00:00:00Z');
|
|
853
|
+
});
|
|
854
|
+
test('should handle ToolCall with minimal fields', () => {
|
|
855
|
+
const engine = new opa_engine_1.OPAEngine({ enabled: true });
|
|
856
|
+
const tc = buildToolCall();
|
|
857
|
+
const input = engine.buildInput(tc);
|
|
858
|
+
expect(input.constraints).toBeUndefined();
|
|
859
|
+
expect(input.context).toBeUndefined();
|
|
860
|
+
expect(input.timestamp).toBeUndefined();
|
|
861
|
+
});
|
|
862
|
+
});
|
|
863
|
+
// ---------------------------------------------------------------------------
|
|
864
|
+
// No configuration (fallback)
|
|
865
|
+
// ---------------------------------------------------------------------------
|
|
866
|
+
describe('OPAEngine - No configuration', () => {
|
|
867
|
+
test('should return fallback when neither server_url nor rego_policy is configured', async () => {
|
|
868
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
869
|
+
enabled: true,
|
|
870
|
+
fallback_decision: 'deny',
|
|
871
|
+
});
|
|
872
|
+
const result = await engine.evaluate(buildToolCall());
|
|
873
|
+
expect(result.decision).toBe('deny');
|
|
874
|
+
expect(result.rule_id).toBe('opa_fallback');
|
|
875
|
+
expect(result.reasons[0]).toBe('OPA not configured: no server_url or rego_policy provided');
|
|
876
|
+
});
|
|
877
|
+
test('should return allow fallback when configured', async () => {
|
|
878
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
879
|
+
enabled: true,
|
|
880
|
+
fallback_decision: 'allow',
|
|
881
|
+
});
|
|
882
|
+
const result = await engine.evaluate(buildToolCall());
|
|
883
|
+
expect(result.decision).toBe('allow');
|
|
884
|
+
});
|
|
885
|
+
});
|
|
886
|
+
// ---------------------------------------------------------------------------
|
|
887
|
+
// isAvailable
|
|
888
|
+
// ---------------------------------------------------------------------------
|
|
889
|
+
describe('OPAEngine - isAvailable', () => {
|
|
890
|
+
test('should return true when rego_policy is configured', async () => {
|
|
891
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
892
|
+
enabled: true,
|
|
893
|
+
rego_policy: 'package palaryn.policy\ndefault decision = "allow"',
|
|
894
|
+
});
|
|
895
|
+
const available = await engine.isAvailable();
|
|
896
|
+
expect(available).toBe(true);
|
|
897
|
+
});
|
|
898
|
+
test('should return false when nothing is configured', async () => {
|
|
899
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
900
|
+
enabled: true,
|
|
901
|
+
});
|
|
902
|
+
const available = await engine.isAvailable();
|
|
903
|
+
expect(available).toBe(false);
|
|
904
|
+
});
|
|
905
|
+
test('should return false when server_url is unreachable', async () => {
|
|
906
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
907
|
+
enabled: true,
|
|
908
|
+
server_url: 'http://127.0.0.1:1', // unlikely to be listening
|
|
909
|
+
});
|
|
910
|
+
const available = await engine.isAvailable();
|
|
911
|
+
expect(available).toBe(false);
|
|
912
|
+
});
|
|
913
|
+
test('should return true when server_url is reachable', async () => {
|
|
914
|
+
// Create a quick mock OPA server
|
|
915
|
+
const server = http.createServer((req, res) => {
|
|
916
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
917
|
+
res.end(JSON.stringify({ result: {} }));
|
|
918
|
+
});
|
|
919
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
920
|
+
const port = server.address().port;
|
|
921
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
922
|
+
enabled: true,
|
|
923
|
+
server_url: `http://127.0.0.1:${port}`,
|
|
924
|
+
});
|
|
925
|
+
const available = await engine.isAvailable();
|
|
926
|
+
expect(available).toBe(true);
|
|
927
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
928
|
+
});
|
|
929
|
+
});
|
|
930
|
+
// ---------------------------------------------------------------------------
|
|
931
|
+
// Circuit breaker (S15)
|
|
932
|
+
// ---------------------------------------------------------------------------
|
|
933
|
+
describe('OPAEngine - Circuit breaker', () => {
|
|
934
|
+
let opaServer;
|
|
935
|
+
let opaPort;
|
|
936
|
+
let requestCount;
|
|
937
|
+
let serverShouldFail;
|
|
938
|
+
beforeAll((done) => {
|
|
939
|
+
opaServer = http.createServer((req, res) => {
|
|
940
|
+
requestCount++;
|
|
941
|
+
if (serverShouldFail) {
|
|
942
|
+
res.destroy();
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
let body = '';
|
|
946
|
+
req.on('data', (chunk) => body += chunk);
|
|
947
|
+
req.on('end', () => {
|
|
948
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
949
|
+
res.end(JSON.stringify({ result: { decision: 'allow' } }));
|
|
950
|
+
});
|
|
951
|
+
});
|
|
952
|
+
opaServer.listen(0, '127.0.0.1', () => {
|
|
953
|
+
opaPort = opaServer.address().port;
|
|
954
|
+
done();
|
|
955
|
+
});
|
|
956
|
+
});
|
|
957
|
+
afterAll((done) => {
|
|
958
|
+
opaServer.close(done);
|
|
959
|
+
});
|
|
960
|
+
beforeEach(() => {
|
|
961
|
+
requestCount = 0;
|
|
962
|
+
serverShouldFail = false;
|
|
963
|
+
});
|
|
964
|
+
test('should open circuit after consecutive failures', async () => {
|
|
965
|
+
serverShouldFail = true;
|
|
966
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
967
|
+
enabled: true,
|
|
968
|
+
server_url: `http://127.0.0.1:${opaPort}`,
|
|
969
|
+
fallback_decision: 'deny',
|
|
970
|
+
});
|
|
971
|
+
// 3 consecutive failures should open the circuit
|
|
972
|
+
for (let i = 0; i < 3; i++) {
|
|
973
|
+
await engine.evaluate(buildToolCall());
|
|
974
|
+
}
|
|
975
|
+
expect(engine.getCircuitState().state).toBe('open');
|
|
976
|
+
expect(engine.getCircuitState().failures).toBe(3);
|
|
977
|
+
});
|
|
978
|
+
test('should skip HTTP call when circuit is open', async () => {
|
|
979
|
+
serverShouldFail = true;
|
|
980
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
981
|
+
enabled: true,
|
|
982
|
+
server_url: `http://127.0.0.1:${opaPort}`,
|
|
983
|
+
fallback_decision: 'deny',
|
|
984
|
+
});
|
|
985
|
+
// Trip the circuit
|
|
986
|
+
for (let i = 0; i < 3; i++) {
|
|
987
|
+
await engine.evaluate(buildToolCall());
|
|
988
|
+
}
|
|
989
|
+
const beforeCount = requestCount;
|
|
990
|
+
// Next call should NOT make an HTTP request (circuit open)
|
|
991
|
+
const result = await engine.evaluate(buildToolCall());
|
|
992
|
+
expect(result.decision).toBe('deny');
|
|
993
|
+
expect(result.reasons[0]).toContain('circuit breaker open');
|
|
994
|
+
expect(requestCount).toBe(beforeCount); // no new HTTP calls
|
|
995
|
+
});
|
|
996
|
+
test('should return fallback with circuit breaker reason when open', async () => {
|
|
997
|
+
serverShouldFail = true;
|
|
998
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
999
|
+
enabled: true,
|
|
1000
|
+
server_url: `http://127.0.0.1:${opaPort}`,
|
|
1001
|
+
fallback_decision: 'deny',
|
|
1002
|
+
});
|
|
1003
|
+
// Trip the circuit
|
|
1004
|
+
for (let i = 0; i < 3; i++) {
|
|
1005
|
+
await engine.evaluate(buildToolCall());
|
|
1006
|
+
}
|
|
1007
|
+
const result = await engine.evaluate(buildToolCall());
|
|
1008
|
+
expect(result.decision).toBe('deny');
|
|
1009
|
+
expect(result.rule_id).toBe('opa_fallback');
|
|
1010
|
+
expect(result.reasons[0]).toContain('circuit breaker');
|
|
1011
|
+
});
|
|
1012
|
+
test('should reset circuit on successful call', async () => {
|
|
1013
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
1014
|
+
enabled: true,
|
|
1015
|
+
server_url: `http://127.0.0.1:${opaPort}`,
|
|
1016
|
+
fallback_decision: 'deny',
|
|
1017
|
+
});
|
|
1018
|
+
// Cause some failures (but not enough to trip)
|
|
1019
|
+
serverShouldFail = true;
|
|
1020
|
+
await engine.evaluate(buildToolCall());
|
|
1021
|
+
await engine.evaluate(buildToolCall());
|
|
1022
|
+
expect(engine.getCircuitState().failures).toBe(2);
|
|
1023
|
+
// Now succeed
|
|
1024
|
+
serverShouldFail = false;
|
|
1025
|
+
await engine.evaluate(buildToolCall());
|
|
1026
|
+
expect(engine.getCircuitState().state).toBe('closed');
|
|
1027
|
+
expect(engine.getCircuitState().failures).toBe(0);
|
|
1028
|
+
});
|
|
1029
|
+
test('should expose circuit state via getCircuitState()', () => {
|
|
1030
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
1031
|
+
enabled: true,
|
|
1032
|
+
server_url: `http://127.0.0.1:${opaPort}`,
|
|
1033
|
+
});
|
|
1034
|
+
const state = engine.getCircuitState();
|
|
1035
|
+
expect(state.state).toBe('closed');
|
|
1036
|
+
expect(state.failures).toBe(0);
|
|
1037
|
+
expect(state.lastFailure).toBe(0);
|
|
1038
|
+
});
|
|
1039
|
+
test('should allow resetCircuit() to force close the circuit', async () => {
|
|
1040
|
+
serverShouldFail = true;
|
|
1041
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
1042
|
+
enabled: true,
|
|
1043
|
+
server_url: `http://127.0.0.1:${opaPort}`,
|
|
1044
|
+
fallback_decision: 'deny',
|
|
1045
|
+
});
|
|
1046
|
+
// Trip the circuit
|
|
1047
|
+
for (let i = 0; i < 3; i++) {
|
|
1048
|
+
await engine.evaluate(buildToolCall());
|
|
1049
|
+
}
|
|
1050
|
+
expect(engine.getCircuitState().state).toBe('open');
|
|
1051
|
+
// Admin reset
|
|
1052
|
+
engine.resetCircuit();
|
|
1053
|
+
expect(engine.getCircuitState().state).toBe('closed');
|
|
1054
|
+
expect(engine.getCircuitState().failures).toBe(0);
|
|
1055
|
+
});
|
|
1056
|
+
});
|
|
1057
|
+
// ---------------------------------------------------------------------------
|
|
1058
|
+
// getConfig
|
|
1059
|
+
// ---------------------------------------------------------------------------
|
|
1060
|
+
describe('OPAEngine - getConfig', () => {
|
|
1061
|
+
test('should return the engine configuration with defaults applied', () => {
|
|
1062
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
1063
|
+
enabled: true,
|
|
1064
|
+
server_url: 'http://localhost:8181',
|
|
1065
|
+
});
|
|
1066
|
+
const config = engine.getConfig();
|
|
1067
|
+
expect(config.enabled).toBe(true);
|
|
1068
|
+
expect(config.server_url).toBe('http://localhost:8181');
|
|
1069
|
+
expect(config.policy_path).toBe('v1/data/palaryn/policy');
|
|
1070
|
+
expect(config.timeout_ms).toBe(5000);
|
|
1071
|
+
expect(config.fallback_decision).toBe('deny');
|
|
1072
|
+
expect(config.package_name).toBe('palaryn.policy');
|
|
1073
|
+
});
|
|
1074
|
+
test('should return custom configuration values', () => {
|
|
1075
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
1076
|
+
enabled: true,
|
|
1077
|
+
server_url: 'http://opa.internal:8181',
|
|
1078
|
+
policy_path: 'v1/data/custom/policy',
|
|
1079
|
+
timeout_ms: 10000,
|
|
1080
|
+
fallback_decision: 'allow',
|
|
1081
|
+
package_name: 'custom.policy',
|
|
1082
|
+
});
|
|
1083
|
+
const config = engine.getConfig();
|
|
1084
|
+
expect(config.policy_path).toBe('v1/data/custom/policy');
|
|
1085
|
+
expect(config.timeout_ms).toBe(10000);
|
|
1086
|
+
expect(config.fallback_decision).toBe('allow');
|
|
1087
|
+
expect(config.package_name).toBe('custom.policy');
|
|
1088
|
+
});
|
|
1089
|
+
});
|
|
1090
|
+
// ---------------------------------------------------------------------------
|
|
1091
|
+
// Gateway integration behavior
|
|
1092
|
+
// ---------------------------------------------------------------------------
|
|
1093
|
+
describe('OPAEngine - Gateway integration', () => {
|
|
1094
|
+
test('OPA disabled: existing YAML behavior unchanged', () => {
|
|
1095
|
+
// When OPA is not enabled, the config.policy.opa is undefined,
|
|
1096
|
+
// so the Gateway constructor won't create an OPAEngine.
|
|
1097
|
+
// This test verifies that building a config without OPA still works.
|
|
1098
|
+
const config = {
|
|
1099
|
+
enabled: false,
|
|
1100
|
+
};
|
|
1101
|
+
// The Gateway should not initialize OPA when enabled=false
|
|
1102
|
+
expect(config.enabled).toBe(false);
|
|
1103
|
+
});
|
|
1104
|
+
test('OPA fallback result has rule_id opa_fallback', async () => {
|
|
1105
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
1106
|
+
enabled: true,
|
|
1107
|
+
fallback_decision: 'deny',
|
|
1108
|
+
});
|
|
1109
|
+
const result = await engine.evaluate(buildToolCall());
|
|
1110
|
+
// When OPA is not configured and returns a fallback, gateway.ts checks
|
|
1111
|
+
// for rule_id === 'opa_fallback' to decide whether to fall back to YAML
|
|
1112
|
+
expect(result.rule_id).toBe('opa_fallback');
|
|
1113
|
+
});
|
|
1114
|
+
test('OPA successful result does not have opa_fallback rule_id', async () => {
|
|
1115
|
+
const policy = `
|
|
1116
|
+
package palaryn.policy
|
|
1117
|
+
default decision = "allow"
|
|
1118
|
+
`;
|
|
1119
|
+
const engine = new opa_engine_1.OPAEngine({
|
|
1120
|
+
enabled: true,
|
|
1121
|
+
rego_policy: policy,
|
|
1122
|
+
});
|
|
1123
|
+
const result = await engine.evaluate(buildToolCall());
|
|
1124
|
+
expect(result.rule_id).not.toBe('opa_fallback');
|
|
1125
|
+
expect(result.rule_id).toBe('opa_local');
|
|
1126
|
+
});
|
|
1127
|
+
});
|
|
1128
|
+
// ---------------------------------------------------------------------------
|
|
1129
|
+
// evaluateRegoSubset (direct method testing)
|
|
1130
|
+
// ---------------------------------------------------------------------------
|
|
1131
|
+
describe('OPAEngine - evaluateRegoSubset', () => {
|
|
1132
|
+
let engine;
|
|
1133
|
+
beforeEach(() => {
|
|
1134
|
+
engine = new opa_engine_1.OPAEngine({ enabled: true, rego_policy: '' });
|
|
1135
|
+
});
|
|
1136
|
+
test('should handle empty policy', () => {
|
|
1137
|
+
const result = engine.evaluateRegoSubset('', { tool: { name: 'test', capability: 'read' } });
|
|
1138
|
+
// No defaults, no rules -- normalizeDecision(undefined) = fallback = 'deny'
|
|
1139
|
+
expect(result.decision).toBe('deny');
|
|
1140
|
+
});
|
|
1141
|
+
test('should handle policy with only package declaration', () => {
|
|
1142
|
+
const result = engine.evaluateRegoSubset('package palaryn.policy', { tool: { name: 'test' } });
|
|
1143
|
+
expect(result.decision).toBe('deny');
|
|
1144
|
+
});
|
|
1145
|
+
test('should handle deeply nested input paths', () => {
|
|
1146
|
+
const policy = `
|
|
1147
|
+
package palaryn.policy
|
|
1148
|
+
default decision = "deny"
|
|
1149
|
+
decision = "allow" {
|
|
1150
|
+
input.args.headers.Authorization == "Bearer token"
|
|
1151
|
+
}
|
|
1152
|
+
`;
|
|
1153
|
+
const input = {
|
|
1154
|
+
args: {
|
|
1155
|
+
headers: { Authorization: 'Bearer token' },
|
|
1156
|
+
},
|
|
1157
|
+
};
|
|
1158
|
+
const result = engine.evaluateRegoSubset(policy, input);
|
|
1159
|
+
expect(result.decision).toBe('allow');
|
|
1160
|
+
});
|
|
1161
|
+
test('should handle missing nested paths gracefully', () => {
|
|
1162
|
+
const policy = `
|
|
1163
|
+
package palaryn.policy
|
|
1164
|
+
default decision = "deny"
|
|
1165
|
+
decision = "allow" {
|
|
1166
|
+
input.nonexistent.deep.path == "value"
|
|
1167
|
+
}
|
|
1168
|
+
`;
|
|
1169
|
+
const result = engine.evaluateRegoSubset(policy, { tool: { name: 'test' } });
|
|
1170
|
+
expect(result.decision).toBe('deny');
|
|
1171
|
+
});
|
|
1172
|
+
test('should handle multiple reasons rules', () => {
|
|
1173
|
+
const policy = `
|
|
1174
|
+
package palaryn.policy
|
|
1175
|
+
default decision = "deny"
|
|
1176
|
+
decision = "deny" {
|
|
1177
|
+
input.tool.capability == "admin"
|
|
1178
|
+
}
|
|
1179
|
+
reasons[reason] {
|
|
1180
|
+
input.tool.capability == "admin"
|
|
1181
|
+
reason := "Admin access is restricted"
|
|
1182
|
+
}
|
|
1183
|
+
reasons[reason] {
|
|
1184
|
+
input.tool.name == "dangerous"
|
|
1185
|
+
reason := "Dangerous tool detected"
|
|
1186
|
+
}
|
|
1187
|
+
`;
|
|
1188
|
+
const input = { tool: { capability: 'admin', name: 'dangerous' } };
|
|
1189
|
+
const result = engine.evaluateRegoSubset(policy, input);
|
|
1190
|
+
expect(result.decision).toBe('deny');
|
|
1191
|
+
expect(result.reasons).toContain('Admin access is restricted');
|
|
1192
|
+
expect(result.reasons).toContain('Dangerous tool detected');
|
|
1193
|
+
});
|
|
1194
|
+
test('should handle numeric comparison', () => {
|
|
1195
|
+
const policy = `
|
|
1196
|
+
package palaryn.policy
|
|
1197
|
+
default decision = "deny"
|
|
1198
|
+
decision = "allow" {
|
|
1199
|
+
input.constraints.max_cost_usd == 5
|
|
1200
|
+
}
|
|
1201
|
+
`;
|
|
1202
|
+
const input = { constraints: { max_cost_usd: 5 } };
|
|
1203
|
+
const result = engine.evaluateRegoSubset(policy, input);
|
|
1204
|
+
expect(result.decision).toBe('allow');
|
|
1205
|
+
});
|
|
1206
|
+
});
|
|
1207
|
+
// ---------------------------------------------------------------------------
|
|
1208
|
+
// Extended Rego patterns: some, count, numeric comparisons, regex.match,
|
|
1209
|
+
// type checks, in keyword
|
|
1210
|
+
// ---------------------------------------------------------------------------
|
|
1211
|
+
describe('OPAEngine - Extended Rego patterns', () => {
|
|
1212
|
+
let engine;
|
|
1213
|
+
beforeEach(() => {
|
|
1214
|
+
engine = new opa_engine_1.OPAEngine({ enabled: true, rego_policy: '' });
|
|
1215
|
+
});
|
|
1216
|
+
// -------------------------------------------------------------------------
|
|
1217
|
+
// `some` keyword for iteration
|
|
1218
|
+
// -------------------------------------------------------------------------
|
|
1219
|
+
describe('some keyword for iteration', () => {
|
|
1220
|
+
test('should match when array contains matching element (semicolon syntax)', () => {
|
|
1221
|
+
const policy = `
|
|
1222
|
+
package palaryn.policy
|
|
1223
|
+
default decision = "deny"
|
|
1224
|
+
decision = "allow" {
|
|
1225
|
+
some x in input.context.labels; x == "sensitive"
|
|
1226
|
+
}
|
|
1227
|
+
`;
|
|
1228
|
+
const input = { context: { labels: ['public', 'sensitive', 'data'] } };
|
|
1229
|
+
const result = engine.evaluateRegoSubset(policy, input);
|
|
1230
|
+
expect(result.decision).toBe('allow');
|
|
1231
|
+
});
|
|
1232
|
+
test('should not match when array does not contain matching element', () => {
|
|
1233
|
+
const policy = `
|
|
1234
|
+
package palaryn.policy
|
|
1235
|
+
default decision = "deny"
|
|
1236
|
+
decision = "allow" {
|
|
1237
|
+
some x in input.context.labels; x == "sensitive"
|
|
1238
|
+
}
|
|
1239
|
+
`;
|
|
1240
|
+
const input = { context: { labels: ['public', 'data'] } };
|
|
1241
|
+
const result = engine.evaluateRegoSubset(policy, input);
|
|
1242
|
+
expect(result.decision).toBe('deny');
|
|
1243
|
+
});
|
|
1244
|
+
test('should match with multi-line some block', () => {
|
|
1245
|
+
const policy = `
|
|
1246
|
+
package palaryn.policy
|
|
1247
|
+
default decision = "deny"
|
|
1248
|
+
decision = "allow" {
|
|
1249
|
+
some x in input.context.labels
|
|
1250
|
+
x == "admin"
|
|
1251
|
+
}
|
|
1252
|
+
`;
|
|
1253
|
+
const input = { context: { labels: ['user', 'admin', 'editor'] } };
|
|
1254
|
+
const result = engine.evaluateRegoSubset(policy, input);
|
|
1255
|
+
expect(result.decision).toBe('allow');
|
|
1256
|
+
});
|
|
1257
|
+
test('should not match when collection is not an array', () => {
|
|
1258
|
+
const policy = `
|
|
1259
|
+
package palaryn.policy
|
|
1260
|
+
default decision = "deny"
|
|
1261
|
+
decision = "allow" {
|
|
1262
|
+
some x in input.context.purpose
|
|
1263
|
+
x == "test"
|
|
1264
|
+
}
|
|
1265
|
+
`;
|
|
1266
|
+
const input = { context: { purpose: 'testing' } };
|
|
1267
|
+
const result = engine.evaluateRegoSubset(policy, input);
|
|
1268
|
+
expect(result.decision).toBe('deny');
|
|
1269
|
+
});
|
|
1270
|
+
test('should handle some with empty array', () => {
|
|
1271
|
+
const policy = `
|
|
1272
|
+
package palaryn.policy
|
|
1273
|
+
default decision = "deny"
|
|
1274
|
+
decision = "allow" {
|
|
1275
|
+
some x in input.context.labels
|
|
1276
|
+
x == "admin"
|
|
1277
|
+
}
|
|
1278
|
+
`;
|
|
1279
|
+
const input = { context: { labels: [] } };
|
|
1280
|
+
const result = engine.evaluateRegoSubset(policy, input);
|
|
1281
|
+
expect(result.decision).toBe('deny');
|
|
1282
|
+
});
|
|
1283
|
+
test('should handle some with additional normal conditions', () => {
|
|
1284
|
+
const policy = `
|
|
1285
|
+
package palaryn.policy
|
|
1286
|
+
default decision = "deny"
|
|
1287
|
+
decision = "allow" {
|
|
1288
|
+
input.tool.capability == "read"
|
|
1289
|
+
some x in input.context.labels
|
|
1290
|
+
x == "approved"
|
|
1291
|
+
}
|
|
1292
|
+
`;
|
|
1293
|
+
// Both conditions met
|
|
1294
|
+
const input1 = { tool: { capability: 'read' }, context: { labels: ['approved'] } };
|
|
1295
|
+
const result1 = engine.evaluateRegoSubset(policy, input1);
|
|
1296
|
+
expect(result1.decision).toBe('allow');
|
|
1297
|
+
// Normal condition fails
|
|
1298
|
+
const input2 = { tool: { capability: 'write' }, context: { labels: ['approved'] } };
|
|
1299
|
+
const result2 = engine.evaluateRegoSubset(policy, input2);
|
|
1300
|
+
expect(result2.decision).toBe('deny');
|
|
1301
|
+
// Some condition fails
|
|
1302
|
+
const input3 = { tool: { capability: 'read' }, context: { labels: ['pending'] } };
|
|
1303
|
+
const result3 = engine.evaluateRegoSubset(policy, input3);
|
|
1304
|
+
expect(result3.decision).toBe('deny');
|
|
1305
|
+
});
|
|
1306
|
+
test('should handle some with inequality sub-condition', () => {
|
|
1307
|
+
const policy = `
|
|
1308
|
+
package palaryn.policy
|
|
1309
|
+
default decision = "deny"
|
|
1310
|
+
decision = "allow" {
|
|
1311
|
+
some x in input.context.labels; x != "blocked"
|
|
1312
|
+
}
|
|
1313
|
+
`;
|
|
1314
|
+
// Has at least one element that is not "blocked"
|
|
1315
|
+
const input1 = { context: { labels: ['blocked', 'ok'] } };
|
|
1316
|
+
const result1 = engine.evaluateRegoSubset(policy, input1);
|
|
1317
|
+
expect(result1.decision).toBe('allow');
|
|
1318
|
+
// All elements are "blocked" -- no x satisfies x != "blocked"
|
|
1319
|
+
const input2 = { context: { labels: ['blocked'] } };
|
|
1320
|
+
const result2 = engine.evaluateRegoSubset(policy, input2);
|
|
1321
|
+
expect(result2.decision).toBe('deny');
|
|
1322
|
+
});
|
|
1323
|
+
});
|
|
1324
|
+
// -------------------------------------------------------------------------
|
|
1325
|
+
// count() built-in
|
|
1326
|
+
// -------------------------------------------------------------------------
|
|
1327
|
+
describe('count() built-in', () => {
|
|
1328
|
+
test('should count array elements with > comparison', () => {
|
|
1329
|
+
const policy = `
|
|
1330
|
+
package palaryn.policy
|
|
1331
|
+
default decision = "deny"
|
|
1332
|
+
decision = "allow" {
|
|
1333
|
+
count(input.context.labels) > 3
|
|
1334
|
+
}
|
|
1335
|
+
`;
|
|
1336
|
+
const input1 = { context: { labels: ['a', 'b', 'c', 'd'] } };
|
|
1337
|
+
const result1 = engine.evaluateRegoSubset(policy, input1);
|
|
1338
|
+
expect(result1.decision).toBe('allow');
|
|
1339
|
+
const input2 = { context: { labels: ['a', 'b', 'c'] } };
|
|
1340
|
+
const result2 = engine.evaluateRegoSubset(policy, input2);
|
|
1341
|
+
expect(result2.decision).toBe('deny');
|
|
1342
|
+
});
|
|
1343
|
+
test('should count array elements with == comparison', () => {
|
|
1344
|
+
const policy = `
|
|
1345
|
+
package palaryn.policy
|
|
1346
|
+
default decision = "deny"
|
|
1347
|
+
decision = "allow" {
|
|
1348
|
+
count(input.context.labels) == 2
|
|
1349
|
+
}
|
|
1350
|
+
`;
|
|
1351
|
+
const input = { context: { labels: ['a', 'b'] } };
|
|
1352
|
+
const result = engine.evaluateRegoSubset(policy, input);
|
|
1353
|
+
expect(result.decision).toBe('allow');
|
|
1354
|
+
});
|
|
1355
|
+
test('should count array elements with < comparison', () => {
|
|
1356
|
+
const policy = `
|
|
1357
|
+
package palaryn.policy
|
|
1358
|
+
default decision = "deny"
|
|
1359
|
+
decision = "allow" {
|
|
1360
|
+
count(input.context.labels) < 3
|
|
1361
|
+
}
|
|
1362
|
+
`;
|
|
1363
|
+
const input1 = { context: { labels: ['a', 'b'] } };
|
|
1364
|
+
const result1 = engine.evaluateRegoSubset(policy, input1);
|
|
1365
|
+
expect(result1.decision).toBe('allow');
|
|
1366
|
+
const input2 = { context: { labels: ['a', 'b', 'c'] } };
|
|
1367
|
+
const result2 = engine.evaluateRegoSubset(policy, input2);
|
|
1368
|
+
expect(result2.decision).toBe('deny');
|
|
1369
|
+
});
|
|
1370
|
+
test('should count array elements with >= comparison', () => {
|
|
1371
|
+
const policy = `
|
|
1372
|
+
package palaryn.policy
|
|
1373
|
+
default decision = "deny"
|
|
1374
|
+
decision = "allow" {
|
|
1375
|
+
count(input.context.labels) >= 2
|
|
1376
|
+
}
|
|
1377
|
+
`;
|
|
1378
|
+
const input1 = { context: { labels: ['a', 'b'] } };
|
|
1379
|
+
expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
|
|
1380
|
+
const input2 = { context: { labels: ['a'] } };
|
|
1381
|
+
expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
|
|
1382
|
+
});
|
|
1383
|
+
test('should count array elements with <= comparison', () => {
|
|
1384
|
+
const policy = `
|
|
1385
|
+
package palaryn.policy
|
|
1386
|
+
default decision = "deny"
|
|
1387
|
+
decision = "allow" {
|
|
1388
|
+
count(input.context.labels) <= 2
|
|
1389
|
+
}
|
|
1390
|
+
`;
|
|
1391
|
+
const input1 = { context: { labels: ['a', 'b'] } };
|
|
1392
|
+
expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
|
|
1393
|
+
const input2 = { context: { labels: ['a', 'b', 'c'] } };
|
|
1394
|
+
expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
|
|
1395
|
+
});
|
|
1396
|
+
test('should count array elements with != comparison', () => {
|
|
1397
|
+
const policy = `
|
|
1398
|
+
package palaryn.policy
|
|
1399
|
+
default decision = "deny"
|
|
1400
|
+
decision = "allow" {
|
|
1401
|
+
count(input.context.labels) != 0
|
|
1402
|
+
}
|
|
1403
|
+
`;
|
|
1404
|
+
const input1 = { context: { labels: ['a'] } };
|
|
1405
|
+
expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
|
|
1406
|
+
const input2 = { context: { labels: [] } };
|
|
1407
|
+
expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
|
|
1408
|
+
});
|
|
1409
|
+
test('should count object keys', () => {
|
|
1410
|
+
const policy = `
|
|
1411
|
+
package palaryn.policy
|
|
1412
|
+
default decision = "deny"
|
|
1413
|
+
decision = "allow" {
|
|
1414
|
+
count(input.args) > 1
|
|
1415
|
+
}
|
|
1416
|
+
`;
|
|
1417
|
+
const input = { args: { method: 'GET', url: 'https://example.com' } };
|
|
1418
|
+
const result = engine.evaluateRegoSubset(policy, input);
|
|
1419
|
+
expect(result.decision).toBe('allow');
|
|
1420
|
+
});
|
|
1421
|
+
test('should count string length', () => {
|
|
1422
|
+
const policy = `
|
|
1423
|
+
package palaryn.policy
|
|
1424
|
+
default decision = "deny"
|
|
1425
|
+
decision = "allow" {
|
|
1426
|
+
count(input.tool.name) > 5
|
|
1427
|
+
}
|
|
1428
|
+
`;
|
|
1429
|
+
const input = { tool: { name: 'http.request' } };
|
|
1430
|
+
const result = engine.evaluateRegoSubset(policy, input);
|
|
1431
|
+
expect(result.decision).toBe('allow');
|
|
1432
|
+
});
|
|
1433
|
+
test('should return 0 for undefined value', () => {
|
|
1434
|
+
const policy = `
|
|
1435
|
+
package palaryn.policy
|
|
1436
|
+
default decision = "deny"
|
|
1437
|
+
decision = "allow" {
|
|
1438
|
+
count(input.nonexistent) == 0
|
|
1439
|
+
}
|
|
1440
|
+
`;
|
|
1441
|
+
const result = engine.evaluateRegoSubset(policy, { tool: { name: 'test' } });
|
|
1442
|
+
expect(result.decision).toBe('allow');
|
|
1443
|
+
});
|
|
1444
|
+
});
|
|
1445
|
+
// -------------------------------------------------------------------------
|
|
1446
|
+
// Numeric comparisons (>, <, >=, <=)
|
|
1447
|
+
// -------------------------------------------------------------------------
|
|
1448
|
+
describe('numeric comparisons', () => {
|
|
1449
|
+
test('should evaluate greater than', () => {
|
|
1450
|
+
const policy = `
|
|
1451
|
+
package palaryn.policy
|
|
1452
|
+
default decision = "deny"
|
|
1453
|
+
decision = "allow" {
|
|
1454
|
+
input.constraints.max_cost_usd > 10
|
|
1455
|
+
}
|
|
1456
|
+
`;
|
|
1457
|
+
const input1 = { constraints: { max_cost_usd: 15 } };
|
|
1458
|
+
expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
|
|
1459
|
+
const input2 = { constraints: { max_cost_usd: 10 } };
|
|
1460
|
+
expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
|
|
1461
|
+
const input3 = { constraints: { max_cost_usd: 5 } };
|
|
1462
|
+
expect(engine.evaluateRegoSubset(policy, input3).decision).toBe('deny');
|
|
1463
|
+
});
|
|
1464
|
+
test('should evaluate less than', () => {
|
|
1465
|
+
const policy = `
|
|
1466
|
+
package palaryn.policy
|
|
1467
|
+
default decision = "deny"
|
|
1468
|
+
decision = "allow" {
|
|
1469
|
+
input.constraints.max_cost_usd < 10
|
|
1470
|
+
}
|
|
1471
|
+
`;
|
|
1472
|
+
const input1 = { constraints: { max_cost_usd: 5 } };
|
|
1473
|
+
expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
|
|
1474
|
+
const input2 = { constraints: { max_cost_usd: 10 } };
|
|
1475
|
+
expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
|
|
1476
|
+
});
|
|
1477
|
+
test('should evaluate greater than or equal', () => {
|
|
1478
|
+
const policy = `
|
|
1479
|
+
package palaryn.policy
|
|
1480
|
+
default decision = "deny"
|
|
1481
|
+
decision = "allow" {
|
|
1482
|
+
input.constraints.max_cost_usd >= 10
|
|
1483
|
+
}
|
|
1484
|
+
`;
|
|
1485
|
+
const input1 = { constraints: { max_cost_usd: 10 } };
|
|
1486
|
+
expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
|
|
1487
|
+
const input2 = { constraints: { max_cost_usd: 15 } };
|
|
1488
|
+
expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('allow');
|
|
1489
|
+
const input3 = { constraints: { max_cost_usd: 9 } };
|
|
1490
|
+
expect(engine.evaluateRegoSubset(policy, input3).decision).toBe('deny');
|
|
1491
|
+
});
|
|
1492
|
+
test('should evaluate less than or equal', () => {
|
|
1493
|
+
const policy = `
|
|
1494
|
+
package palaryn.policy
|
|
1495
|
+
default decision = "deny"
|
|
1496
|
+
decision = "allow" {
|
|
1497
|
+
input.constraints.max_cost_usd <= 10
|
|
1498
|
+
}
|
|
1499
|
+
`;
|
|
1500
|
+
const input1 = { constraints: { max_cost_usd: 10 } };
|
|
1501
|
+
expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
|
|
1502
|
+
const input2 = { constraints: { max_cost_usd: 5 } };
|
|
1503
|
+
expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('allow');
|
|
1504
|
+
const input3 = { constraints: { max_cost_usd: 11 } };
|
|
1505
|
+
expect(engine.evaluateRegoSubset(policy, input3).decision).toBe('deny');
|
|
1506
|
+
});
|
|
1507
|
+
test('should return false for non-numeric comparisons', () => {
|
|
1508
|
+
const policy = `
|
|
1509
|
+
package palaryn.policy
|
|
1510
|
+
default decision = "deny"
|
|
1511
|
+
decision = "allow" {
|
|
1512
|
+
input.tool.name > 5
|
|
1513
|
+
}
|
|
1514
|
+
`;
|
|
1515
|
+
const input = { tool: { name: 'http.request' } };
|
|
1516
|
+
expect(engine.evaluateRegoSubset(policy, input).decision).toBe('deny');
|
|
1517
|
+
});
|
|
1518
|
+
test('should handle decimal numbers', () => {
|
|
1519
|
+
const policy = `
|
|
1520
|
+
package palaryn.policy
|
|
1521
|
+
default decision = "deny"
|
|
1522
|
+
decision = "allow" {
|
|
1523
|
+
input.constraints.max_cost_usd <= 0.5
|
|
1524
|
+
}
|
|
1525
|
+
`;
|
|
1526
|
+
const input1 = { constraints: { max_cost_usd: 0.3 } };
|
|
1527
|
+
expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
|
|
1528
|
+
const input2 = { constraints: { max_cost_usd: 0.7 } };
|
|
1529
|
+
expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
|
|
1530
|
+
});
|
|
1531
|
+
});
|
|
1532
|
+
// -------------------------------------------------------------------------
|
|
1533
|
+
// regex.match() built-in
|
|
1534
|
+
// -------------------------------------------------------------------------
|
|
1535
|
+
describe('regex.match() built-in', () => {
|
|
1536
|
+
test('should match URL pattern', () => {
|
|
1537
|
+
const policy = `
|
|
1538
|
+
package palaryn.policy
|
|
1539
|
+
default decision = "deny"
|
|
1540
|
+
decision = "allow" {
|
|
1541
|
+
regex.match("^https://.*\\.example\\.com", input.args.url)
|
|
1542
|
+
}
|
|
1543
|
+
`;
|
|
1544
|
+
const input1 = { args: { url: 'https://api.example.com/data' } };
|
|
1545
|
+
expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
|
|
1546
|
+
const input2 = { args: { url: 'http://api.example.com/data' } };
|
|
1547
|
+
expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
|
|
1548
|
+
const input3 = { args: { url: 'https://malicious.com/data' } };
|
|
1549
|
+
expect(engine.evaluateRegoSubset(policy, input3).decision).toBe('deny');
|
|
1550
|
+
});
|
|
1551
|
+
test('should match simple patterns', () => {
|
|
1552
|
+
const policy = `
|
|
1553
|
+
package palaryn.policy
|
|
1554
|
+
default decision = "deny"
|
|
1555
|
+
decision = "allow" {
|
|
1556
|
+
regex.match("^http\\.", input.tool.name)
|
|
1557
|
+
}
|
|
1558
|
+
`;
|
|
1559
|
+
const input1 = { tool: { name: 'http.request' } };
|
|
1560
|
+
expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
|
|
1561
|
+
const input2 = { tool: { name: 'slack.post' } };
|
|
1562
|
+
expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
|
|
1563
|
+
});
|
|
1564
|
+
test('should return false for non-string values', () => {
|
|
1565
|
+
const policy = `
|
|
1566
|
+
package palaryn.policy
|
|
1567
|
+
default decision = "deny"
|
|
1568
|
+
decision = "allow" {
|
|
1569
|
+
regex.match("^test", input.constraints.max_cost_usd)
|
|
1570
|
+
}
|
|
1571
|
+
`;
|
|
1572
|
+
const input = { constraints: { max_cost_usd: 5 } };
|
|
1573
|
+
expect(engine.evaluateRegoSubset(policy, input).decision).toBe('deny');
|
|
1574
|
+
});
|
|
1575
|
+
test('should handle invalid regex gracefully', () => {
|
|
1576
|
+
const policy = `
|
|
1577
|
+
package palaryn.policy
|
|
1578
|
+
default decision = "deny"
|
|
1579
|
+
decision = "allow" {
|
|
1580
|
+
regex.match("[invalid(", input.tool.name)
|
|
1581
|
+
}
|
|
1582
|
+
`;
|
|
1583
|
+
const input = { tool: { name: 'test' } };
|
|
1584
|
+
expect(engine.evaluateRegoSubset(policy, input).decision).toBe('deny');
|
|
1585
|
+
});
|
|
1586
|
+
});
|
|
1587
|
+
// -------------------------------------------------------------------------
|
|
1588
|
+
// Type checks: is_string, is_number, is_boolean, is_array
|
|
1589
|
+
// -------------------------------------------------------------------------
|
|
1590
|
+
describe('type checks', () => {
|
|
1591
|
+
test('should check is_string', () => {
|
|
1592
|
+
const policy = `
|
|
1593
|
+
package palaryn.policy
|
|
1594
|
+
default decision = "deny"
|
|
1595
|
+
decision = "allow" {
|
|
1596
|
+
is_string(input.args.url)
|
|
1597
|
+
}
|
|
1598
|
+
`;
|
|
1599
|
+
const input1 = { args: { url: 'https://example.com' } };
|
|
1600
|
+
expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
|
|
1601
|
+
const input2 = { args: { url: 123 } };
|
|
1602
|
+
expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
|
|
1603
|
+
});
|
|
1604
|
+
test('should check is_number', () => {
|
|
1605
|
+
const policy = `
|
|
1606
|
+
package palaryn.policy
|
|
1607
|
+
default decision = "deny"
|
|
1608
|
+
decision = "allow" {
|
|
1609
|
+
is_number(input.constraints.max_cost_usd)
|
|
1610
|
+
}
|
|
1611
|
+
`;
|
|
1612
|
+
const input1 = { constraints: { max_cost_usd: 5.0 } };
|
|
1613
|
+
expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
|
|
1614
|
+
const input2 = { constraints: { max_cost_usd: 'five' } };
|
|
1615
|
+
expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
|
|
1616
|
+
});
|
|
1617
|
+
test('should check is_boolean', () => {
|
|
1618
|
+
const policy = `
|
|
1619
|
+
package palaryn.policy
|
|
1620
|
+
default decision = "deny"
|
|
1621
|
+
decision = "allow" {
|
|
1622
|
+
is_boolean(input.args.dry_run)
|
|
1623
|
+
}
|
|
1624
|
+
`;
|
|
1625
|
+
const input1 = { args: { dry_run: true } };
|
|
1626
|
+
expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
|
|
1627
|
+
const input2 = { args: { dry_run: 'true' } };
|
|
1628
|
+
expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
|
|
1629
|
+
});
|
|
1630
|
+
test('should check is_array', () => {
|
|
1631
|
+
const policy = `
|
|
1632
|
+
package palaryn.policy
|
|
1633
|
+
default decision = "deny"
|
|
1634
|
+
decision = "allow" {
|
|
1635
|
+
is_array(input.context.labels)
|
|
1636
|
+
}
|
|
1637
|
+
`;
|
|
1638
|
+
const input1 = { context: { labels: ['a', 'b'] } };
|
|
1639
|
+
expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
|
|
1640
|
+
const input2 = { context: { labels: 'not-an-array' } };
|
|
1641
|
+
expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
|
|
1642
|
+
});
|
|
1643
|
+
test('should return false for undefined values', () => {
|
|
1644
|
+
const policy = `
|
|
1645
|
+
package palaryn.policy
|
|
1646
|
+
default decision = "deny"
|
|
1647
|
+
decision = "allow" {
|
|
1648
|
+
is_string(input.nonexistent.path)
|
|
1649
|
+
}
|
|
1650
|
+
`;
|
|
1651
|
+
const result = engine.evaluateRegoSubset(policy, { tool: { name: 'test' } });
|
|
1652
|
+
expect(result.decision).toBe('deny');
|
|
1653
|
+
});
|
|
1654
|
+
test('should combine type checks with other conditions', () => {
|
|
1655
|
+
const policy = `
|
|
1656
|
+
package palaryn.policy
|
|
1657
|
+
default decision = "deny"
|
|
1658
|
+
decision = "allow" {
|
|
1659
|
+
is_string(input.args.url)
|
|
1660
|
+
startswith(input.args.url, "https://")
|
|
1661
|
+
}
|
|
1662
|
+
`;
|
|
1663
|
+
const input1 = { args: { url: 'https://example.com' } };
|
|
1664
|
+
expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
|
|
1665
|
+
const input2 = { args: { url: 'http://example.com' } };
|
|
1666
|
+
expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
|
|
1667
|
+
});
|
|
1668
|
+
});
|
|
1669
|
+
// -------------------------------------------------------------------------
|
|
1670
|
+
// Array membership with `in` keyword
|
|
1671
|
+
// -------------------------------------------------------------------------
|
|
1672
|
+
describe('in keyword for array membership', () => {
|
|
1673
|
+
test('should check string membership in array', () => {
|
|
1674
|
+
const policy = `
|
|
1675
|
+
package palaryn.policy
|
|
1676
|
+
default decision = "deny"
|
|
1677
|
+
decision = "allow" {
|
|
1678
|
+
"admin" in input.context.labels
|
|
1679
|
+
}
|
|
1680
|
+
`;
|
|
1681
|
+
const input1 = { context: { labels: ['user', 'admin', 'editor'] } };
|
|
1682
|
+
expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
|
|
1683
|
+
const input2 = { context: { labels: ['user', 'editor'] } };
|
|
1684
|
+
expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
|
|
1685
|
+
});
|
|
1686
|
+
test('should check numeric membership in array', () => {
|
|
1687
|
+
const policy = `
|
|
1688
|
+
package palaryn.policy
|
|
1689
|
+
default decision = "deny"
|
|
1690
|
+
decision = "allow" {
|
|
1691
|
+
42 in input.context.labels
|
|
1692
|
+
}
|
|
1693
|
+
`;
|
|
1694
|
+
const input1 = { context: { labels: [1, 42, 100] } };
|
|
1695
|
+
expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
|
|
1696
|
+
const input2 = { context: { labels: [1, 2, 3] } };
|
|
1697
|
+
expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
|
|
1698
|
+
});
|
|
1699
|
+
test('should return false when target is not an array', () => {
|
|
1700
|
+
const policy = `
|
|
1701
|
+
package palaryn.policy
|
|
1702
|
+
default decision = "deny"
|
|
1703
|
+
decision = "allow" {
|
|
1704
|
+
"admin" in input.tool.name
|
|
1705
|
+
}
|
|
1706
|
+
`;
|
|
1707
|
+
const input = { tool: { name: 'http.request' } };
|
|
1708
|
+
expect(engine.evaluateRegoSubset(policy, input).decision).toBe('deny');
|
|
1709
|
+
});
|
|
1710
|
+
test('should handle boolean membership', () => {
|
|
1711
|
+
const policy = `
|
|
1712
|
+
package palaryn.policy
|
|
1713
|
+
default decision = "deny"
|
|
1714
|
+
decision = "allow" {
|
|
1715
|
+
true in input.context.labels
|
|
1716
|
+
}
|
|
1717
|
+
`;
|
|
1718
|
+
const input1 = { context: { labels: [true, false] } };
|
|
1719
|
+
expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
|
|
1720
|
+
const input2 = { context: { labels: ['true', false] } };
|
|
1721
|
+
expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
|
|
1722
|
+
});
|
|
1723
|
+
test('should combine in keyword with other conditions', () => {
|
|
1724
|
+
const policy = `
|
|
1725
|
+
package palaryn.policy
|
|
1726
|
+
default decision = "deny"
|
|
1727
|
+
decision = "allow" {
|
|
1728
|
+
input.tool.capability == "write"
|
|
1729
|
+
"approved" in input.context.labels
|
|
1730
|
+
}
|
|
1731
|
+
`;
|
|
1732
|
+
const input1 = { tool: { capability: 'write' }, context: { labels: ['approved'] } };
|
|
1733
|
+
expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
|
|
1734
|
+
const input2 = { tool: { capability: 'write' }, context: { labels: ['pending'] } };
|
|
1735
|
+
expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
|
|
1736
|
+
const input3 = { tool: { capability: 'read' }, context: { labels: ['approved'] } };
|
|
1737
|
+
expect(engine.evaluateRegoSubset(policy, input3).decision).toBe('deny');
|
|
1738
|
+
});
|
|
1739
|
+
});
|
|
1740
|
+
// -------------------------------------------------------------------------
|
|
1741
|
+
// Integration: combining multiple new patterns
|
|
1742
|
+
// -------------------------------------------------------------------------
|
|
1743
|
+
describe('combined patterns', () => {
|
|
1744
|
+
test('should combine count, in, and numeric comparisons', () => {
|
|
1745
|
+
const policy = `
|
|
1746
|
+
package palaryn.policy
|
|
1747
|
+
default decision = "deny"
|
|
1748
|
+
decision = "allow" {
|
|
1749
|
+
count(input.context.labels) >= 2
|
|
1750
|
+
"approved" in input.context.labels
|
|
1751
|
+
input.constraints.max_cost_usd <= 100
|
|
1752
|
+
}
|
|
1753
|
+
`;
|
|
1754
|
+
const input1 = {
|
|
1755
|
+
context: { labels: ['approved', 'reviewed'] },
|
|
1756
|
+
constraints: { max_cost_usd: 50 },
|
|
1757
|
+
};
|
|
1758
|
+
expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
|
|
1759
|
+
// count fails
|
|
1760
|
+
const input2 = {
|
|
1761
|
+
context: { labels: ['approved'] },
|
|
1762
|
+
constraints: { max_cost_usd: 50 },
|
|
1763
|
+
};
|
|
1764
|
+
expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
|
|
1765
|
+
// in fails
|
|
1766
|
+
const input3 = {
|
|
1767
|
+
context: { labels: ['pending', 'reviewed'] },
|
|
1768
|
+
constraints: { max_cost_usd: 50 },
|
|
1769
|
+
};
|
|
1770
|
+
expect(engine.evaluateRegoSubset(policy, input3).decision).toBe('deny');
|
|
1771
|
+
// numeric comparison fails
|
|
1772
|
+
const input4 = {
|
|
1773
|
+
context: { labels: ['approved', 'reviewed'] },
|
|
1774
|
+
constraints: { max_cost_usd: 150 },
|
|
1775
|
+
};
|
|
1776
|
+
expect(engine.evaluateRegoSubset(policy, input4).decision).toBe('deny');
|
|
1777
|
+
});
|
|
1778
|
+
test('should combine regex.match with type checks', () => {
|
|
1779
|
+
const policy = `
|
|
1780
|
+
package palaryn.policy
|
|
1781
|
+
default decision = "deny"
|
|
1782
|
+
decision = "allow" {
|
|
1783
|
+
is_string(input.args.url)
|
|
1784
|
+
regex.match("^https://.*\\.example\\.com", input.args.url)
|
|
1785
|
+
}
|
|
1786
|
+
`;
|
|
1787
|
+
const input1 = { args: { url: 'https://api.example.com/v1' } };
|
|
1788
|
+
expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
|
|
1789
|
+
const input2 = { args: { url: 123 } };
|
|
1790
|
+
expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
|
|
1791
|
+
});
|
|
1792
|
+
test('should combine some with count', () => {
|
|
1793
|
+
const policy = `
|
|
1794
|
+
package palaryn.policy
|
|
1795
|
+
default decision = "deny"
|
|
1796
|
+
decision = "allow" {
|
|
1797
|
+
count(input.context.labels) > 1
|
|
1798
|
+
some x in input.context.labels
|
|
1799
|
+
x == "critical"
|
|
1800
|
+
}
|
|
1801
|
+
`;
|
|
1802
|
+
const input1 = { context: { labels: ['info', 'critical'] } };
|
|
1803
|
+
expect(engine.evaluateRegoSubset(policy, input1).decision).toBe('allow');
|
|
1804
|
+
// count satisfied but some fails
|
|
1805
|
+
const input2 = { context: { labels: ['info', 'warning'] } };
|
|
1806
|
+
expect(engine.evaluateRegoSubset(policy, input2).decision).toBe('deny');
|
|
1807
|
+
// some satisfied but count fails
|
|
1808
|
+
const input3 = { context: { labels: ['critical'] } };
|
|
1809
|
+
expect(engine.evaluateRegoSubset(policy, input3).decision).toBe('deny');
|
|
1810
|
+
});
|
|
1811
|
+
});
|
|
1812
|
+
});
|
|
1813
|
+
//# sourceMappingURL=opa-engine.test.js.map
|