shield360 1.1.0 → 1.2.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/README.md +330 -3
- package/dist/__tests__/build-info.test.js +8 -1
- package/dist/__tests__/build-info.test.js.map +1 -1
- package/dist/__tests__/code-location.test.js +1 -1
- package/dist/__tests__/masking.test.d.ts +1 -0
- package/dist/__tests__/masking.test.js +66 -0
- package/dist/__tests__/masking.test.js.map +1 -0
- package/dist/__tests__/register.e2e.test.d.ts +1 -0
- package/dist/__tests__/register.e2e.test.js +264 -0
- package/dist/__tests__/register.e2e.test.js.map +1 -0
- package/dist/__tests__/register.test.js +60 -0
- package/dist/__tests__/register.test.js.map +1 -1
- package/dist/agents/__tests__/agents.test.d.ts +1 -0
- package/dist/agents/__tests__/agents.test.js +529 -0
- package/dist/agents/__tests__/agents.test.js.map +1 -0
- package/dist/agents/index.d.ts +15 -0
- package/dist/agents/index.js +25 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/passport.d.ts +92 -0
- package/dist/agents/passport.js +301 -0
- package/dist/agents/passport.js.map +1 -0
- package/dist/agents/policy.d.ts +24 -0
- package/dist/agents/policy.js +93 -0
- package/dist/agents/policy.js.map +1 -0
- package/dist/agents/replay.d.ts +20 -0
- package/dist/agents/replay.js +47 -0
- package/dist/agents/replay.js.map +1 -0
- package/dist/agents/report.d.ts +30 -0
- package/dist/agents/report.js +93 -0
- package/dist/agents/report.js.map +1 -0
- package/dist/agents/types.d.ts +148 -0
- package/dist/agents/types.js +37 -0
- package/dist/agents/types.js.map +1 -0
- package/dist/agents/verifier.d.ts +33 -0
- package/dist/agents/verifier.js +276 -0
- package/dist/agents/verifier.js.map +1 -0
- package/dist/ai-security/__tests__/bench.test.d.ts +1 -0
- package/dist/ai-security/__tests__/bench.test.js +50 -0
- package/dist/ai-security/__tests__/bench.test.js.map +1 -0
- package/dist/ai-security/__tests__/checks.test.d.ts +1 -0
- package/dist/ai-security/__tests__/checks.test.js +248 -0
- package/dist/ai-security/__tests__/checks.test.js.map +1 -0
- package/dist/ai-security/__tests__/config.test.d.ts +1 -0
- package/dist/ai-security/__tests__/config.test.js +164 -0
- package/dist/ai-security/__tests__/config.test.js.map +1 -0
- package/dist/ai-security/__tests__/detection.test.d.ts +1 -0
- package/dist/ai-security/__tests__/detection.test.js +114 -0
- package/dist/ai-security/__tests__/detection.test.js.map +1 -0
- package/dist/ai-security/__tests__/fixtures.d.ts +8 -0
- package/dist/ai-security/__tests__/fixtures.js +30 -0
- package/dist/ai-security/__tests__/fixtures.js.map +1 -0
- package/dist/ai-security/__tests__/hooks.test.d.ts +1 -0
- package/dist/ai-security/__tests__/hooks.test.js +174 -0
- package/dist/ai-security/__tests__/hooks.test.js.map +1 -0
- package/dist/ai-security/__tests__/stream.test.d.ts +1 -0
- package/dist/ai-security/__tests__/stream.test.js +85 -0
- package/dist/ai-security/__tests__/stream.test.js.map +1 -0
- package/dist/ai-security/config.d.ts +94 -0
- package/dist/ai-security/config.js +241 -0
- package/dist/ai-security/config.js.map +1 -0
- package/dist/ai-security/context.d.ts +45 -0
- package/dist/ai-security/context.js +108 -0
- package/dist/ai-security/context.js.map +1 -0
- package/dist/ai-security/custom-kinds.d.ts +23 -0
- package/dist/ai-security/custom-kinds.js +97 -0
- package/dist/ai-security/custom-kinds.js.map +1 -0
- package/dist/ai-security/events.d.ts +77 -0
- package/dist/ai-security/events.js +153 -0
- package/dist/ai-security/events.js.map +1 -0
- package/dist/ai-security/hooks.d.ts +48 -0
- package/dist/ai-security/hooks.js +461 -0
- package/dist/ai-security/hooks.js.map +1 -0
- package/dist/ai-security/index.d.ts +209 -0
- package/dist/ai-security/index.js +587 -0
- package/dist/ai-security/index.js.map +1 -0
- package/dist/ai-security/injection-rules.json +1024 -0
- package/dist/ai-security/injection.d.ts +46 -0
- package/dist/ai-security/injection.js +635 -0
- package/dist/ai-security/injection.js.map +1 -0
- package/dist/ai-security/jailbreak.d.ts +38 -0
- package/dist/ai-security/jailbreak.js +73 -0
- package/dist/ai-security/jailbreak.js.map +1 -0
- package/dist/ai-security/limits.d.ts +84 -0
- package/dist/ai-security/limits.js +195 -0
- package/dist/ai-security/limits.js.map +1 -0
- package/dist/ai-security/mcp.d.ts +82 -0
- package/dist/ai-security/mcp.js +150 -0
- package/dist/ai-security/mcp.js.map +1 -0
- package/dist/ai-security/output.d.ts +107 -0
- package/dist/ai-security/output.js +390 -0
- package/dist/ai-security/output.js.map +1 -0
- package/dist/ai-security/policy.d.ts +125 -0
- package/dist/ai-security/policy.js +196 -0
- package/dist/ai-security/policy.js.map +1 -0
- package/dist/ai-security/rag.d.ts +70 -0
- package/dist/ai-security/rag.js +174 -0
- package/dist/ai-security/rag.js.map +1 -0
- package/dist/ai-security/tool-rules.json +203 -0
- package/dist/ai-security/tools.d.ts +46 -0
- package/dist/ai-security/tools.js +62 -0
- package/dist/ai-security/tools.js.map +1 -0
- package/dist/budgets/__tests__/budgets.test.d.ts +1 -0
- package/dist/budgets/__tests__/budgets.test.js +134 -0
- package/dist/budgets/__tests__/budgets.test.js.map +1 -0
- package/dist/budgets/auto.d.ts +25 -0
- package/dist/budgets/auto.js +63 -0
- package/dist/budgets/auto.js.map +1 -0
- package/dist/budgets/index.d.ts +110 -0
- package/dist/budgets/index.js +239 -0
- package/dist/budgets/index.js.map +1 -0
- package/dist/customer/__tests__/customer.test.d.ts +1 -0
- package/dist/customer/__tests__/customer.test.js +155 -0
- package/dist/customer/__tests__/customer.test.js.map +1 -0
- package/dist/customer/index.d.ts +48 -0
- package/dist/customer/index.js +132 -0
- package/dist/customer/index.js.map +1 -0
- package/dist/feedback/__tests__/feedback.test.d.ts +1 -0
- package/dist/feedback/__tests__/feedback.test.js +80 -0
- package/dist/feedback/__tests__/feedback.test.js.map +1 -0
- package/dist/feedback/index.d.ts +48 -0
- package/dist/feedback/index.js +97 -0
- package/dist/feedback/index.js.map +1 -0
- package/dist/flags/__tests__/flags.test.d.ts +1 -0
- package/dist/flags/__tests__/flags.test.js +301 -0
- package/dist/flags/__tests__/flags.test.js.map +1 -0
- package/dist/flags/client.d.ts +131 -0
- package/dist/flags/client.js +301 -0
- package/dist/flags/client.js.map +1 -0
- package/dist/flags/engine.d.ts +200 -0
- package/dist/flags/engine.js +677 -0
- package/dist/flags/engine.js.map +1 -0
- package/dist/flags/index.d.ts +4 -0
- package/dist/flags/index.js +16 -0
- package/dist/flags/index.js.map +1 -0
- package/dist/guard/__tests__/findings.test.js +1 -1
- package/dist/guard/__tests__/report.test.js +67 -0
- package/dist/guard/__tests__/report.test.js.map +1 -1
- package/dist/guard/heartbeat.d.ts +34 -0
- package/dist/guard/heartbeat.js +83 -0
- package/dist/guard/heartbeat.js.map +1 -0
- package/dist/guard/integration.d.ts +9 -0
- package/dist/guard/integration.js +4 -2
- package/dist/guard/integration.js.map +1 -1
- package/dist/guard/report.d.ts +24 -1
- package/dist/guard/report.js +67 -18
- package/dist/guard/report.js.map +1 -1
- package/dist/index.d.ts +88 -0
- package/dist/index.js +193 -2
- package/dist/index.js.map +1 -1
- package/dist/instrumentation/__tests__/langchain-wrapper.test.js +1 -1
- package/dist/instrumentation/__tests__/langchain-wrapper.test.js.map +1 -1
- package/dist/instrumentation/anthropic/wrapper.js +4 -0
- package/dist/instrumentation/anthropic/wrapper.js.map +1 -1
- package/dist/instrumentation/openai/wrapper.js +6 -0
- package/dist/instrumentation/openai/wrapper.js.map +1 -1
- package/dist/masking.d.ts +42 -0
- package/dist/masking.js +197 -0
- package/dist/masking.js.map +1 -0
- package/dist/otel/__tests__/app-logs-safety.test.d.ts +1 -0
- package/dist/otel/__tests__/app-logs-safety.test.js +129 -0
- package/dist/otel/__tests__/app-logs-safety.test.js.map +1 -0
- package/dist/otel/__tests__/app-logs.test.d.ts +1 -0
- package/dist/otel/__tests__/app-logs.test.js +269 -0
- package/dist/otel/__tests__/app-logs.test.js.map +1 -0
- package/dist/otel/__tests__/log-auto.test.d.ts +1 -0
- package/dist/otel/__tests__/log-auto.test.js +125 -0
- package/dist/otel/__tests__/log-auto.test.js.map +1 -0
- package/dist/otel/__tests__/log-bridges.test.d.ts +1 -0
- package/dist/otel/__tests__/log-bridges.test.js +170 -0
- package/dist/otel/__tests__/log-bridges.test.js.map +1 -0
- package/dist/otel/__tests__/system-metrics.test.d.ts +1 -0
- package/dist/otel/__tests__/system-metrics.test.js +79 -0
- package/dist/otel/__tests__/system-metrics.test.js.map +1 -0
- package/dist/otel/app-logs.d.ts +80 -0
- package/dist/otel/app-logs.js +195 -0
- package/dist/otel/app-logs.js.map +1 -0
- package/dist/otel/build-info.d.ts +2 -1
- package/dist/otel/build-info.js +3 -2
- package/dist/otel/build-info.js.map +1 -1
- package/dist/otel/events.d.ts +1 -0
- package/dist/otel/events.js +21 -12
- package/dist/otel/events.js.map +1 -1
- package/dist/otel/log-auto.d.ts +44 -0
- package/dist/otel/log-auto.js +200 -0
- package/dist/otel/log-auto.js.map +1 -0
- package/dist/otel/log-bridges.d.ts +54 -0
- package/dist/otel/log-bridges.js +307 -0
- package/dist/otel/log-bridges.js.map +1 -0
- package/dist/otel/system-metrics.d.ts +39 -0
- package/dist/otel/system-metrics.js +166 -0
- package/dist/otel/system-metrics.js.map +1 -0
- package/dist/otel/tracing.js +5 -1
- package/dist/otel/tracing.js.map +1 -1
- package/dist/protect/__tests__/protect.test.js +4 -0
- package/dist/protect/__tests__/protect.test.js.map +1 -1
- package/dist/protect/index.js +9 -4
- package/dist/protect/index.js.map +1 -1
- package/dist/rasp/__tests__/auto-http.test.d.ts +1 -0
- package/dist/rasp/__tests__/auto-http.test.js +136 -0
- package/dist/rasp/__tests__/auto-http.test.js.map +1 -0
- package/dist/rasp/__tests__/baseline.test.d.ts +1 -0
- package/dist/rasp/__tests__/baseline.test.js +86 -0
- package/dist/rasp/__tests__/baseline.test.js.map +1 -0
- package/dist/rasp/__tests__/benign-corpus.test.d.ts +1 -0
- package/dist/rasp/__tests__/benign-corpus.test.js +68 -0
- package/dist/rasp/__tests__/benign-corpus.test.js.map +1 -0
- package/dist/rasp/__tests__/capture-context.test.d.ts +1 -0
- package/dist/rasp/__tests__/capture-context.test.js +135 -0
- package/dist/rasp/__tests__/capture-context.test.js.map +1 -0
- package/dist/rasp/__tests__/code-hook.test.d.ts +1 -0
- package/dist/rasp/__tests__/code-hook.test.js +218 -0
- package/dist/rasp/__tests__/code-hook.test.js.map +1 -0
- package/dist/rasp/__tests__/config-client.test.d.ts +1 -0
- package/dist/rasp/__tests__/config-client.test.js +283 -0
- package/dist/rasp/__tests__/config-client.test.js.map +1 -0
- package/dist/rasp/__tests__/corpus.d.ts +13 -0
- package/dist/rasp/__tests__/corpus.js +111 -0
- package/dist/rasp/__tests__/corpus.js.map +1 -0
- package/dist/rasp/__tests__/custom-rules.test.d.ts +1 -0
- package/dist/rasp/__tests__/custom-rules.test.js +276 -0
- package/dist/rasp/__tests__/custom-rules.test.js.map +1 -0
- package/dist/rasp/__tests__/db-hook.test.d.ts +1 -0
- package/dist/rasp/__tests__/db-hook.test.js +438 -0
- package/dist/rasp/__tests__/db-hook.test.js.map +1 -0
- package/dist/rasp/__tests__/deception.test.d.ts +1 -0
- package/dist/rasp/__tests__/deception.test.js +567 -0
- package/dist/rasp/__tests__/deception.test.js.map +1 -0
- package/dist/rasp/__tests__/detect.test.d.ts +1 -0
- package/dist/rasp/__tests__/detect.test.js +276 -0
- package/dist/rasp/__tests__/detect.test.js.map +1 -0
- package/dist/rasp/__tests__/esm-exports.test.d.ts +1 -0
- package/dist/rasp/__tests__/esm-exports.test.js +20 -0
- package/dist/rasp/__tests__/esm-exports.test.js.map +1 -0
- package/dist/rasp/__tests__/fs-hook.test.d.ts +1 -0
- package/dist/rasp/__tests__/fs-hook.test.js +241 -0
- package/dist/rasp/__tests__/fs-hook.test.js.map +1 -0
- package/dist/rasp/__tests__/hook-contracts.test.d.ts +1 -0
- package/dist/rasp/__tests__/hook-contracts.test.js +201 -0
- package/dist/rasp/__tests__/hook-contracts.test.js.map +1 -0
- package/dist/rasp/__tests__/hook-kit.d.ts +42 -0
- package/dist/rasp/__tests__/hook-kit.js +139 -0
- package/dist/rasp/__tests__/hook-kit.js.map +1 -0
- package/dist/rasp/__tests__/http-hook.test.d.ts +1 -0
- package/dist/rasp/__tests__/http-hook.test.js +250 -0
- package/dist/rasp/__tests__/http-hook.test.js.map +1 -0
- package/dist/rasp/__tests__/iast.test.d.ts +1 -0
- package/dist/rasp/__tests__/iast.test.js +361 -0
- package/dist/rasp/__tests__/iast.test.js.map +1 -0
- package/dist/rasp/__tests__/include-hook.test.d.ts +1 -0
- package/dist/rasp/__tests__/include-hook.test.js +122 -0
- package/dist/rasp/__tests__/include-hook.test.js.map +1 -0
- package/dist/rasp/__tests__/middleware.test.d.ts +1 -0
- package/dist/rasp/__tests__/middleware.test.js +302 -0
- package/dist/rasp/__tests__/middleware.test.js.map +1 -0
- package/dist/rasp/__tests__/process-hook.test.d.ts +1 -0
- package/dist/rasp/__tests__/process-hook.test.js +238 -0
- package/dist/rasp/__tests__/process-hook.test.js.map +1 -0
- package/dist/rasp/__tests__/reporter.test.d.ts +1 -0
- package/dist/rasp/__tests__/reporter.test.js +114 -0
- package/dist/rasp/__tests__/reporter.test.js.map +1 -0
- package/dist/rasp/__tests__/route.test.d.ts +1 -0
- package/dist/rasp/__tests__/route.test.js +46 -0
- package/dist/rasp/__tests__/route.test.js.map +1 -0
- package/dist/rasp/__tests__/rule-expr.test.d.ts +1 -0
- package/dist/rasp/__tests__/rule-expr.test.js +151 -0
- package/dist/rasp/__tests__/rule-expr.test.js.map +1 -0
- package/dist/rasp/__tests__/rule-phases.test.d.ts +1 -0
- package/dist/rasp/__tests__/rule-phases.test.js +271 -0
- package/dist/rasp/__tests__/rule-phases.test.js.map +1 -0
- package/dist/rasp/__tests__/sinks-hook.test.d.ts +1 -0
- package/dist/rasp/__tests__/sinks-hook.test.js +177 -0
- package/dist/rasp/__tests__/sinks-hook.test.js.map +1 -0
- package/dist/rasp/__tests__/targets.test.d.ts +1 -0
- package/dist/rasp/__tests__/targets.test.js +82 -0
- package/dist/rasp/__tests__/targets.test.js.map +1 -0
- package/dist/rasp/__tests__/template-hook.test.d.ts +1 -0
- package/dist/rasp/__tests__/template-hook.test.js +254 -0
- package/dist/rasp/__tests__/template-hook.test.js.map +1 -0
- package/dist/rasp/__tests__/upload.test.d.ts +1 -0
- package/dist/rasp/__tests__/upload.test.js +81 -0
- package/dist/rasp/__tests__/upload.test.js.map +1 -0
- package/dist/rasp/__tests__/vpatch-engine.test.d.ts +1 -0
- package/dist/rasp/__tests__/vpatch-engine.test.js +177 -0
- package/dist/rasp/__tests__/vpatch-engine.test.js.map +1 -0
- package/dist/rasp/auto-http.d.ts +6 -0
- package/dist/rasp/auto-http.js +194 -0
- package/dist/rasp/auto-http.js.map +1 -0
- package/dist/rasp/baseline.d.ts +35 -0
- package/dist/rasp/baseline.js +91 -0
- package/dist/rasp/baseline.js.map +1 -0
- package/dist/rasp/capture.d.ts +76 -0
- package/dist/rasp/capture.js +304 -0
- package/dist/rasp/capture.js.map +1 -0
- package/dist/rasp/config-client.d.ts +122 -0
- package/dist/rasp/config-client.js +337 -0
- package/dist/rasp/config-client.js.map +1 -0
- package/dist/rasp/context.d.ts +65 -0
- package/dist/rasp/context.js +68 -0
- package/dist/rasp/context.js.map +1 -0
- package/dist/rasp/custom-rules.d.ts +68 -0
- package/dist/rasp/custom-rules.js +222 -0
- package/dist/rasp/custom-rules.js.map +1 -0
- package/dist/rasp/data/default-rules.json +199 -0
- package/dist/rasp/data/default-sinks.json +4 -0
- package/dist/rasp/data/dnslog-domains.json +11 -0
- package/dist/rasp/deception.d.ts +70 -0
- package/dist/rasp/deception.js +220 -0
- package/dist/rasp/deception.js.map +1 -0
- package/dist/rasp/detect.d.ts +68 -0
- package/dist/rasp/detect.js +671 -0
- package/dist/rasp/detect.js.map +1 -0
- package/dist/rasp/engine.d.ts +107 -0
- package/dist/rasp/engine.js +330 -0
- package/dist/rasp/engine.js.map +1 -0
- package/dist/rasp/hooks/code.d.ts +20 -0
- package/dist/rasp/hooks/code.js +334 -0
- package/dist/rasp/hooks/code.js.map +1 -0
- package/dist/rasp/hooks/db.d.ts +48 -0
- package/dist/rasp/hooks/db.js +322 -0
- package/dist/rasp/hooks/db.js.map +1 -0
- package/dist/rasp/hooks/esm.d.ts +7 -0
- package/dist/rasp/hooks/esm.js +18 -0
- package/dist/rasp/hooks/esm.js.map +1 -0
- package/dist/rasp/hooks/fs.d.ts +3 -0
- package/dist/rasp/hooks/fs.js +206 -0
- package/dist/rasp/hooks/fs.js.map +1 -0
- package/dist/rasp/hooks/http.d.ts +4 -0
- package/dist/rasp/hooks/http.js +213 -0
- package/dist/rasp/hooks/http.js.map +1 -0
- package/dist/rasp/hooks/include.d.ts +9 -0
- package/dist/rasp/hooks/include.js +79 -0
- package/dist/rasp/hooks/include.js.map +1 -0
- package/dist/rasp/hooks/marks.d.ts +2 -0
- package/dist/rasp/hooks/marks.js +16 -0
- package/dist/rasp/hooks/marks.js.map +1 -0
- package/dist/rasp/hooks/process.d.ts +22 -0
- package/dist/rasp/hooks/process.js +191 -0
- package/dist/rasp/hooks/process.js.map +1 -0
- package/dist/rasp/hooks/sinks.d.ts +54 -0
- package/dist/rasp/hooks/sinks.js +329 -0
- package/dist/rasp/hooks/sinks.js.map +1 -0
- package/dist/rasp/hooks/template.d.ts +21 -0
- package/dist/rasp/hooks/template.js +152 -0
- package/dist/rasp/hooks/template.js.map +1 -0
- package/dist/rasp/iast-rules.d.ts +82 -0
- package/dist/rasp/iast-rules.js +240 -0
- package/dist/rasp/iast-rules.js.map +1 -0
- package/dist/rasp/iast-scope.d.ts +29 -0
- package/dist/rasp/iast-scope.js +54 -0
- package/dist/rasp/iast-scope.js.map +1 -0
- package/dist/rasp/iast.d.ts +81 -0
- package/dist/rasp/iast.js +272 -0
- package/dist/rasp/iast.js.map +1 -0
- package/dist/rasp/index.d.ts +52 -0
- package/dist/rasp/index.js +280 -0
- package/dist/rasp/index.js.map +1 -0
- package/dist/rasp/lexers.d.ts +32 -0
- package/dist/rasp/lexers.js +444 -0
- package/dist/rasp/lexers.js.map +1 -0
- package/dist/rasp/middleware.d.ts +155 -0
- package/dist/rasp/middleware.js +568 -0
- package/dist/rasp/middleware.js.map +1 -0
- package/dist/rasp/model-output.d.ts +14 -0
- package/dist/rasp/model-output.js +107 -0
- package/dist/rasp/model-output.js.map +1 -0
- package/dist/rasp/normalize.d.ts +23 -0
- package/dist/rasp/normalize.js +140 -0
- package/dist/rasp/normalize.js.map +1 -0
- package/dist/rasp/reporter.d.ts +60 -0
- package/dist/rasp/reporter.js +230 -0
- package/dist/rasp/reporter.js.map +1 -0
- package/dist/rasp/rule-expr.d.ts +236 -0
- package/dist/rasp/rule-expr.js +1253 -0
- package/dist/rasp/rule-expr.js.map +1 -0
- package/dist/rasp/template.d.ts +53 -0
- package/dist/rasp/template.js +336 -0
- package/dist/rasp/template.js.map +1 -0
- package/dist/rasp/types.d.ts +110 -0
- package/dist/rasp/types.js +51 -0
- package/dist/rasp/types.js.map +1 -0
- package/dist/rasp/vpatch.d.ts +55 -0
- package/dist/rasp/vpatch.js +120 -0
- package/dist/rasp/vpatch.js.map +1 -0
- package/dist/register.js +52 -11
- package/dist/register.js.map +1 -1
- package/dist/semantic-convention.d.ts +2 -0
- package/dist/semantic-convention.js +5 -0
- package/dist/semantic-convention.js.map +1 -1
- package/dist/types.d.ts +75 -1
- package/dist/zero-code.d.ts +8 -0
- package/dist/zero-code.js +38 -0
- package/dist/zero-code.js.map +1 -0
- package/package.json +18 -2
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* MCP and tool pinning (OWASP LLM03 supply chain): which MCP servers and
|
|
4
|
+
* tools an app may use, and what each tool said about itself when it was
|
|
5
|
+
* approved.
|
|
6
|
+
*
|
|
7
|
+
* A tool is pinned by the SHA-256 of its description and of its input
|
|
8
|
+
* schema (canonical JSON). When a server lists its tools the SDK compares
|
|
9
|
+
* them with the pins from the platform:
|
|
10
|
+
*
|
|
11
|
+
* mcp_unknown_server a server not on the allow-list (by name, and by
|
|
12
|
+
* publisher and version when the pin names them)
|
|
13
|
+
* mcp_unknown_tool a tool with no pin
|
|
14
|
+
* mcp_tool_changed a pinned tool whose description or schema changed
|
|
15
|
+
* since approval: the "rug pull"
|
|
16
|
+
* mcp_tool_poisoned a description that reads like tool poisoning
|
|
17
|
+
* (scanned when first seen and on every change)
|
|
18
|
+
*
|
|
19
|
+
* Every tool seen is reported to the platform (name, hashes and the
|
|
20
|
+
* description itself, which is the tool author's text, not user data) so an
|
|
21
|
+
* operator can approve it or its change; approval adds the pin. With mode
|
|
22
|
+
* enforce and blockUnknown, unknown, changed and poisoned tools are removed
|
|
23
|
+
* from the list the model sees and calls to them are refused.
|
|
24
|
+
*
|
|
25
|
+
* The description scan is the shared injection detector (source
|
|
26
|
+
* tool_description) plus tool-rules.json, rules adapted from Cisco AI
|
|
27
|
+
* Defense's mcp-scanner (Apache-2.0), plus a cross-tool shadowing check: a
|
|
28
|
+
* description that names another tool of the session with a directive.
|
|
29
|
+
*
|
|
30
|
+
* Must stay in sync with: sdk/python/src/shield360/ai_security/_mcp.py
|
|
31
|
+
*/
|
|
32
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
33
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
34
|
+
};
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.INJECTION_RULES_VERSION = exports.schemaHash = exports.descriptionHash = exports.TOOL_RULES_VERSION = void 0;
|
|
37
|
+
exports.sha256 = sha256;
|
|
38
|
+
exports.canonicalJson = canonicalJson;
|
|
39
|
+
exports.scanToolDescription = scanToolDescription;
|
|
40
|
+
exports.checkServerTools = checkServerTools;
|
|
41
|
+
const crypto_1 = require("crypto");
|
|
42
|
+
const tool_rules_json_1 = __importDefault(require("./tool-rules.json"));
|
|
43
|
+
const injection_1 = require("./injection");
|
|
44
|
+
Object.defineProperty(exports, "INJECTION_RULES_VERSION", { enumerable: true, get: function () { return injection_1.INJECTION_RULES_VERSION; } });
|
|
45
|
+
const RULES = tool_rules_json_1.default.rules.map((r) => ({ ...r, compiled: r.patterns.map((p) => new RegExp(p, 'iu')) }));
|
|
46
|
+
exports.TOOL_RULES_VERSION = tool_rules_json_1.default.version;
|
|
47
|
+
const TOOL_THRESHOLD = tool_rules_json_1.default.threshold;
|
|
48
|
+
function sha256(text) {
|
|
49
|
+
return (0, crypto_1.createHash)('sha256').update(text, 'utf8').digest('hex');
|
|
50
|
+
}
|
|
51
|
+
/** JSON with object keys sorted, so the same schema always hashes the same. */
|
|
52
|
+
function canonicalJson(value) {
|
|
53
|
+
if (value === undefined)
|
|
54
|
+
return 'null';
|
|
55
|
+
if (value === null || typeof value !== 'object')
|
|
56
|
+
return JSON.stringify(value) ?? 'null';
|
|
57
|
+
if (Array.isArray(value))
|
|
58
|
+
return `[${value.map(canonicalJson).join(',')}]`;
|
|
59
|
+
const keys = Object.keys(value).sort();
|
|
60
|
+
return `{${keys.filter((k) => value[k] !== undefined).map((k) => `${JSON.stringify(k)}:${canonicalJson(value[k])}`).join(',')}}`;
|
|
61
|
+
}
|
|
62
|
+
const descriptionHash = (description) => sha256((description || '').trim());
|
|
63
|
+
exports.descriptionHash = descriptionHash;
|
|
64
|
+
const schemaHash = (schema) => sha256(canonicalJson(schema ?? null));
|
|
65
|
+
exports.schemaHash = schemaHash;
|
|
66
|
+
/**
|
|
67
|
+
* How much a tool description reads like tool poisoning: the injection
|
|
68
|
+
* detector's rules for tool descriptions, the tool rules, and cross-tool
|
|
69
|
+
* shadowing (another tool of the session named with a directive).
|
|
70
|
+
*/
|
|
71
|
+
function scanToolDescription(description, otherTools = []) {
|
|
72
|
+
const text = (description || '').slice(0, 20000);
|
|
73
|
+
if (!text)
|
|
74
|
+
return { score: 0, detected: false, rules: [] };
|
|
75
|
+
const inj = (0, injection_1.scanForInjection)(text, { source: 'tool_description' });
|
|
76
|
+
let keep = 1 - inj.score;
|
|
77
|
+
const rules = inj.matches.map((m) => m.rule);
|
|
78
|
+
for (const rule of RULES) {
|
|
79
|
+
if (rule.compiled.some((re) => re.test(text))) {
|
|
80
|
+
rules.push(rule.id);
|
|
81
|
+
keep *= 1 - rule.weight;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const lower = text.toLowerCase();
|
|
85
|
+
const shadowed = otherTools.find((name) => {
|
|
86
|
+
const n = name.toLowerCase();
|
|
87
|
+
if (n.length < 3)
|
|
88
|
+
return false;
|
|
89
|
+
const at = lower.indexOf(n);
|
|
90
|
+
if (at < 0)
|
|
91
|
+
return false;
|
|
92
|
+
const around = lower.slice(Math.max(0, at - 120), at + n.length + 120);
|
|
93
|
+
return /\b(?:always|must|instead|never|before|whenever|override|replace|redirect)\b/.test(around);
|
|
94
|
+
});
|
|
95
|
+
if (shadowed) {
|
|
96
|
+
rules.push('tool.cross_tool_shadowing');
|
|
97
|
+
keep *= 1 - 0.6;
|
|
98
|
+
}
|
|
99
|
+
const score = Math.round((1 - keep) * 1000) / 1000;
|
|
100
|
+
return { score, detected: score >= Math.min(TOOL_THRESHOLD, inj.threshold), rules: Array.from(new Set(rules)).sort() };
|
|
101
|
+
}
|
|
102
|
+
/** Compares a server's tools with the pins. Pure: no events, no network. */
|
|
103
|
+
function checkServerTools(policy, server, tools) {
|
|
104
|
+
const enforce = policy.mode === 'enforce';
|
|
105
|
+
const pin = policy.servers.find((s) => s.name === server.name);
|
|
106
|
+
let allowed = true;
|
|
107
|
+
let reason;
|
|
108
|
+
if (!pin) {
|
|
109
|
+
allowed = false;
|
|
110
|
+
reason = 'unknown';
|
|
111
|
+
}
|
|
112
|
+
else if (pin.publisher && pin.publisher !== server.publisher) {
|
|
113
|
+
allowed = false;
|
|
114
|
+
reason = 'publisher';
|
|
115
|
+
}
|
|
116
|
+
else if (pin.version && pin.version !== server.version) {
|
|
117
|
+
allowed = false;
|
|
118
|
+
reason = 'version';
|
|
119
|
+
}
|
|
120
|
+
// An unpinned server is refused only with blockUnknown; a pinned one that changed publisher or version always is.
|
|
121
|
+
const refuseServer = enforce && !allowed && (reason !== 'unknown' || policy.blockUnknown);
|
|
122
|
+
const pins = new Map();
|
|
123
|
+
for (const t of policy.tools)
|
|
124
|
+
if (t.server === server.name)
|
|
125
|
+
pins.set(t.name, t);
|
|
126
|
+
const names = tools.map((t) => t.name);
|
|
127
|
+
const verdicts = tools.map((tool) => {
|
|
128
|
+
const dh = (0, exports.descriptionHash)(tool.description);
|
|
129
|
+
const sh = (0, exports.schemaHash)(tool.inputSchema);
|
|
130
|
+
const p = pins.get(tool.name);
|
|
131
|
+
let status = 'pinned';
|
|
132
|
+
const changed = [];
|
|
133
|
+
if (!p)
|
|
134
|
+
status = 'unknown';
|
|
135
|
+
else {
|
|
136
|
+
if (p.descriptionHash !== dh)
|
|
137
|
+
changed.push('description');
|
|
138
|
+
if (p.schemaHash !== sh)
|
|
139
|
+
changed.push('schema');
|
|
140
|
+
if (changed.length)
|
|
141
|
+
status = 'changed';
|
|
142
|
+
}
|
|
143
|
+
// Scanned when first seen and when changed; a pinned, unchanged tool was scanned at approval.
|
|
144
|
+
const poisoning = status === 'pinned' ? { score: 0, detected: false, rules: [] } : scanToolDescription(tool.description || '', names.filter((n) => n !== tool.name));
|
|
145
|
+
const refuse = refuseServer || (enforce && (poisoning.detected || status === 'changed' || (status === 'unknown' && policy.blockUnknown)));
|
|
146
|
+
return { tool: tool.name, descriptionHash: dh, schemaHash: sh, status, ...(changed.length ? { changed } : {}), poisoning, refuse };
|
|
147
|
+
});
|
|
148
|
+
return { server: server.name, allowed, ...(reason ? { reason } : {}), tools: verdicts };
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=mcp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp.js","sourceRoot":"","sources":["../../src/ai-security/mcp.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output scanning (OWASP LLM02 sensitive information disclosure, LLM05
|
|
3
|
+
* improper output handling, LLM07 system prompt leakage): what a model
|
|
4
|
+
* answered, checked before the app returns it.
|
|
5
|
+
*
|
|
6
|
+
* pii / secret built-in kinds (e-mail, SSN, payment card, phone; API
|
|
7
|
+
* keys, tokens, private keys, connection strings)
|
|
8
|
+
* custom_kind the organisation's own kinds of data
|
|
9
|
+
* cross_tenant identifiers of another tenant that reached this
|
|
10
|
+
* request's context (RAG tenant check, tagContext)
|
|
11
|
+
* exfil_link markdown/HTML images to domains not on the allow-list
|
|
12
|
+
* (renderers fetch them unasked), and links or images
|
|
13
|
+
* carrying data in their query string or path
|
|
14
|
+
* system_prompt_leak runs of the request's own system prompt (distinctive
|
|
15
|
+
* 6-word shingles) appearing in the answer
|
|
16
|
+
* canary_leak the configured canary marker appearing in the answer
|
|
17
|
+
*
|
|
18
|
+
* Findings carry a masked token and a keyed fingerprint, never the value.
|
|
19
|
+
* redact replaces each finding's span with its token (or "[link removed]");
|
|
20
|
+
* block withholds the answer. A leaked system prompt is withheld in enforce
|
|
21
|
+
* mode whatever the action: a redacted prompt still leaks its structure.
|
|
22
|
+
*
|
|
23
|
+
* OutputStreamScanner checks a streamed answer as it arrives with bounded
|
|
24
|
+
* memory: it holds back the last HOLDBACK characters so a value split across
|
|
25
|
+
* chunks can still be redacted, and scans a sliding window.
|
|
26
|
+
*
|
|
27
|
+
* Must stay in sync with: sdk/python/src/shield360/ai_security/_output.py
|
|
28
|
+
*/
|
|
29
|
+
import type { AiSecurityPolicy, CustomDataKindDef } from './policy';
|
|
30
|
+
export type OutputFindingKind = 'pii' | 'secret' | 'custom_kind' | 'cross_tenant' | 'exfil_link' | 'system_prompt_leak' | 'canary_leak';
|
|
31
|
+
export type OutputFinding = {
|
|
32
|
+
kind: OutputFindingKind;
|
|
33
|
+
/** The label: email, payment_card, api_key, a custom kind's id, image, query_data... */
|
|
34
|
+
label: string;
|
|
35
|
+
start: number;
|
|
36
|
+
end: number;
|
|
37
|
+
masked: string;
|
|
38
|
+
fingerprint?: string;
|
|
39
|
+
};
|
|
40
|
+
export type OutputScanOptions = {
|
|
41
|
+
policy: AiSecurityPolicy['output'];
|
|
42
|
+
customKinds?: readonly CustomDataKindDef[];
|
|
43
|
+
/** The request's system prompt, for the leakage check. */
|
|
44
|
+
systemPrompt?: string | null;
|
|
45
|
+
/** Lower-cased identifier -> other tenant. */
|
|
46
|
+
foreign?: ReadonlyMap<string, string>;
|
|
47
|
+
};
|
|
48
|
+
export type OutputScanResult = {
|
|
49
|
+
findings: OutputFinding[];
|
|
50
|
+
/** The text with every finding replaced by its token. */
|
|
51
|
+
redacted: string;
|
|
52
|
+
/** Whether enforce mode must withhold the answer whatever the action. */
|
|
53
|
+
mustBlock: boolean;
|
|
54
|
+
};
|
|
55
|
+
export declare function hostAllowed(host: string, allowed: readonly string[]): boolean;
|
|
56
|
+
/** Whether a URL carries data: a long query value or path segment that looks encoded, or a sensitive value. */
|
|
57
|
+
export declare function urlCarriesData(url: string): boolean;
|
|
58
|
+
/** The distinctive 6-word shingles of a system prompt: those with at least two words that are not stop words. */
|
|
59
|
+
export declare function promptShingles(prompt: string, max?: number): Set<string>;
|
|
60
|
+
/** A leak: at least 3 shingles and at least a tenth of the prompt's (or 25, whichever is less). */
|
|
61
|
+
export declare function isLeak(matched: number, total: number): boolean;
|
|
62
|
+
export declare function scanOutput(text: string, opts: OutputScanOptions): OutputScanResult;
|
|
63
|
+
/** Replaces finding spans with their tokens; overlapping spans merge, the earliest finding's token wins. */
|
|
64
|
+
export declare function redact(text: string, findings: readonly OutputFinding[]): string;
|
|
65
|
+
/** Characters held back before release, so a value split across chunks is still caught whole. */
|
|
66
|
+
export declare const HOLDBACK = 256;
|
|
67
|
+
/** Characters of already-released text kept for context (prompt shingles, links that started earlier). */
|
|
68
|
+
export declare const WINDOW = 2048;
|
|
69
|
+
/** Pending characters beyond the holdback before a scan runs: fewer scans of the window, at most this much more delay. */
|
|
70
|
+
export declare const RELEASE_STEP = 128;
|
|
71
|
+
export type StreamVerdict = {
|
|
72
|
+
text: string;
|
|
73
|
+
findings: OutputFinding[];
|
|
74
|
+
blocked: boolean;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Scans a streamed answer incrementally. push() returns what may be released
|
|
78
|
+
* now (redacted when asked); end() returns the rest. Memory is bounded by
|
|
79
|
+
* WINDOW + HOLDBACK + one chunk; the prompt-leak count needs only the
|
|
80
|
+
* shingles already seen. When blocking, nothing more is released once a
|
|
81
|
+
* finding that blocks appears.
|
|
82
|
+
*/
|
|
83
|
+
export declare class OutputStreamScanner {
|
|
84
|
+
private readonly opts;
|
|
85
|
+
private readonly enforce;
|
|
86
|
+
private pending;
|
|
87
|
+
private released;
|
|
88
|
+
/** Characters released so far, for stable finding ids. */
|
|
89
|
+
private offset;
|
|
90
|
+
private readonly shingles;
|
|
91
|
+
private readonly leaked;
|
|
92
|
+
private tailWords;
|
|
93
|
+
/** A word cut by the chunk boundary, completed by the next chunk. */
|
|
94
|
+
private partial;
|
|
95
|
+
private reported;
|
|
96
|
+
blocked: boolean;
|
|
97
|
+
readonly findings: OutputFinding[];
|
|
98
|
+
constructor(opts: OutputScanOptions, enforce: {
|
|
99
|
+
redact: boolean;
|
|
100
|
+
block: boolean;
|
|
101
|
+
});
|
|
102
|
+
push(chunk: string): StreamVerdict;
|
|
103
|
+
end(): StreamVerdict;
|
|
104
|
+
private countShingles;
|
|
105
|
+
private release;
|
|
106
|
+
private verdict;
|
|
107
|
+
}
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Output scanning (OWASP LLM02 sensitive information disclosure, LLM05
|
|
4
|
+
* improper output handling, LLM07 system prompt leakage): what a model
|
|
5
|
+
* answered, checked before the app returns it.
|
|
6
|
+
*
|
|
7
|
+
* pii / secret built-in kinds (e-mail, SSN, payment card, phone; API
|
|
8
|
+
* keys, tokens, private keys, connection strings)
|
|
9
|
+
* custom_kind the organisation's own kinds of data
|
|
10
|
+
* cross_tenant identifiers of another tenant that reached this
|
|
11
|
+
* request's context (RAG tenant check, tagContext)
|
|
12
|
+
* exfil_link markdown/HTML images to domains not on the allow-list
|
|
13
|
+
* (renderers fetch them unasked), and links or images
|
|
14
|
+
* carrying data in their query string or path
|
|
15
|
+
* system_prompt_leak runs of the request's own system prompt (distinctive
|
|
16
|
+
* 6-word shingles) appearing in the answer
|
|
17
|
+
* canary_leak the configured canary marker appearing in the answer
|
|
18
|
+
*
|
|
19
|
+
* Findings carry a masked token and a keyed fingerprint, never the value.
|
|
20
|
+
* redact replaces each finding's span with its token (or "[link removed]");
|
|
21
|
+
* block withholds the answer. A leaked system prompt is withheld in enforce
|
|
22
|
+
* mode whatever the action: a redacted prompt still leaks its structure.
|
|
23
|
+
*
|
|
24
|
+
* OutputStreamScanner checks a streamed answer as it arrives with bounded
|
|
25
|
+
* memory: it holds back the last HOLDBACK characters so a value split across
|
|
26
|
+
* chunks can still be redacted, and scans a sliding window.
|
|
27
|
+
*
|
|
28
|
+
* Must stay in sync with: sdk/python/src/shield360/ai_security/_output.py
|
|
29
|
+
*/
|
|
30
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
+
exports.OutputStreamScanner = exports.RELEASE_STEP = exports.WINDOW = exports.HOLDBACK = void 0;
|
|
32
|
+
exports.hostAllowed = hostAllowed;
|
|
33
|
+
exports.urlCarriesData = urlCarriesData;
|
|
34
|
+
exports.promptShingles = promptShingles;
|
|
35
|
+
exports.isLeak = isLeak;
|
|
36
|
+
exports.scanOutput = scanOutput;
|
|
37
|
+
exports.redact = redact;
|
|
38
|
+
const findings_1 = require("../guard/findings");
|
|
39
|
+
const custom_kinds_1 = require("./custom-kinds");
|
|
40
|
+
const luhn = (v) => {
|
|
41
|
+
const d = v.replace(/\D/g, '');
|
|
42
|
+
if (d.length < 13 || d.length > 19)
|
|
43
|
+
return false;
|
|
44
|
+
let sum = 0;
|
|
45
|
+
for (let i = 0; i < d.length; i++) {
|
|
46
|
+
let n = d.charCodeAt(d.length - 1 - i) - 48;
|
|
47
|
+
if (i % 2 === 1) {
|
|
48
|
+
n *= 2;
|
|
49
|
+
if (n > 9)
|
|
50
|
+
n -= 9;
|
|
51
|
+
}
|
|
52
|
+
sum += n;
|
|
53
|
+
}
|
|
54
|
+
return sum % 10 === 0;
|
|
55
|
+
};
|
|
56
|
+
const issuableSsn = (v) => {
|
|
57
|
+
const [a, b, c] = v.split('-');
|
|
58
|
+
return a !== '000' && a !== '666' && a[0] !== '9' && b !== '00' && c !== '0000';
|
|
59
|
+
};
|
|
60
|
+
// High-precision shapes only: an output check that flags every hex string is switched off by the people it annoys.
|
|
61
|
+
const PATTERNS = [
|
|
62
|
+
{ label: 'openai-api-key', kind: 'secret', re: /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}/g },
|
|
63
|
+
{ label: 'anthropic-api-key', kind: 'secret', re: /\bsk-ant-[A-Za-z0-9_-]{20,}/g },
|
|
64
|
+
{ label: 'aws-access-key', kind: 'secret', re: /(?<![A-Za-z0-9/+=])(?:AKIA|ASIA)[0-9A-Z]{16}(?![A-Za-z0-9/+=])/g },
|
|
65
|
+
{ label: 'gcp-api-key', kind: 'secret', re: /\bAIza[0-9A-Za-z_-]{35}/g },
|
|
66
|
+
{ label: 'github-token', kind: 'secret', re: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{36,}/g },
|
|
67
|
+
{ label: 'github-fine-grained', kind: 'secret', re: /\bgithub_pat_[A-Za-z0-9_]{22,}/g },
|
|
68
|
+
{ label: 'stripe-key', kind: 'secret', re: /\b(?:sk|rk)_live_[A-Za-z0-9]{24,}/g },
|
|
69
|
+
{ label: 'slack-token', kind: 'secret', re: /\bxox[bpoas]-[A-Za-z0-9-]{10,}/g },
|
|
70
|
+
{ label: 'private-key', kind: 'secret', re: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY-----/g },
|
|
71
|
+
{ label: 'connection-string', kind: 'secret', re: /\b(?:mongodb(?:\+srv)?|postgres(?:ql)?|mysql|redis|amqps?):\/\/[^\s:@/]+:[^\s@/]+@[^\s]+/g },
|
|
72
|
+
{ label: 'jwt', kind: 'secret', re: /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g },
|
|
73
|
+
{ label: 'email', kind: 'pii', re: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}\b/g },
|
|
74
|
+
{ label: 'ssn', kind: 'pii', re: /(?<![0-9-])[0-9]{3}-[0-9]{2}-[0-9]{4}(?![0-9-])/g, check: issuableSsn },
|
|
75
|
+
{ label: 'credit-card', kind: 'pii', re: /(?<![0-9])(?:4[0-9]{3}|5[1-5][0-9]{2}|3[47][0-9]{2}|6(?:011|5[0-9]{2}))(?:[ -]?[0-9]{4}){2}[ -]?[0-9]{3,4}(?![0-9])/g, check: luhn },
|
|
76
|
+
{ label: 'phone-us', kind: 'pii', re: /(?<![0-9])(?:\+1[ .-]?)?\(?[2-9][0-9]{2}\)?[ .-][0-9]{3}[ .-][0-9]{4}(?![0-9])/g },
|
|
77
|
+
];
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Links.
|
|
80
|
+
const MD_IMAGE = /!\[[^\]\n]{0,200}\]\(\s*<?(https?:\/\/[^\s)>]+)>?[^)\n]{0,200}\)/gi;
|
|
81
|
+
const HTML_IMAGE = /<img\b[^>\n]{0,300}?\bsrc\s*=\s*["']?(https?:\/\/[^\s"'>]+)/gi;
|
|
82
|
+
const ANY_URL = /\bhttps?:\/\/[^\s<>"'()\]]+/gi;
|
|
83
|
+
function hostAllowed(host, allowed) {
|
|
84
|
+
const h = host.toLowerCase().replace(/\.$/, '');
|
|
85
|
+
return allowed.some((d) => (d.startsWith('*.') ? h === d.slice(2) || h.endsWith(d.slice(1)) : h === d));
|
|
86
|
+
}
|
|
87
|
+
/** Whether a URL carries data: a long query value or path segment that looks encoded, or a sensitive value. */
|
|
88
|
+
function urlCarriesData(url) {
|
|
89
|
+
let parsed;
|
|
90
|
+
try {
|
|
91
|
+
parsed = new URL(url);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
const values = [];
|
|
97
|
+
parsed.searchParams.forEach((v) => values.push(v));
|
|
98
|
+
if (parsed.hash.length > 1)
|
|
99
|
+
values.push(decodeSafe(parsed.hash.slice(1)));
|
|
100
|
+
for (const seg of parsed.pathname.split('/'))
|
|
101
|
+
values.push(decodeSafe(seg));
|
|
102
|
+
return values.some((v) => {
|
|
103
|
+
if (v.length >= 40 && /^[A-Za-z0-9+/=_-]+$/.test(v))
|
|
104
|
+
return true; // base64 / hex blob
|
|
105
|
+
if (v.length >= 24 && /\s/.test(v) && /[a-z]{3,}/i.test(v))
|
|
106
|
+
return true; // prose in a URL
|
|
107
|
+
return PATTERNS.some((p) => {
|
|
108
|
+
p.re.lastIndex = 0;
|
|
109
|
+
const m = p.re.exec(v);
|
|
110
|
+
return !!m && (!p.check || p.check(m[0]));
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function decodeSafe(s) {
|
|
115
|
+
try {
|
|
116
|
+
return decodeURIComponent(s.replace(/\+/g, ' '));
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return s;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// System prompt shingles.
|
|
124
|
+
const SHINGLE = 6;
|
|
125
|
+
const STOP = new Set('a an and are as at be by for from has have i if in into is it its of on or that the their them then there these they this to was were will with you your we our us not no do does can should must may'.split(' '));
|
|
126
|
+
const words = (text) => text.toLowerCase().match(/[\p{L}\p{N}]+/gu) || [];
|
|
127
|
+
/** The distinctive 6-word shingles of a system prompt: those with at least two words that are not stop words. */
|
|
128
|
+
function promptShingles(prompt, max = 5000) {
|
|
129
|
+
const w = words(prompt);
|
|
130
|
+
const out = new Set();
|
|
131
|
+
for (let i = 0; i + SHINGLE <= w.length && out.size < max; i++) {
|
|
132
|
+
const s = w.slice(i, i + SHINGLE);
|
|
133
|
+
if (s.filter((x) => !STOP.has(x)).length >= 2)
|
|
134
|
+
out.add(s.join(' '));
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
/** How many of the prompt's shingles appear in a text (each counted once). */
|
|
139
|
+
function leakedShingles(text, shingles) {
|
|
140
|
+
if (!shingles.size)
|
|
141
|
+
return 0;
|
|
142
|
+
const w = words(text);
|
|
143
|
+
const seen = new Set();
|
|
144
|
+
for (let i = 0; i + SHINGLE <= w.length; i++) {
|
|
145
|
+
const s = w.slice(i, i + SHINGLE).join(' ');
|
|
146
|
+
if (shingles.has(s))
|
|
147
|
+
seen.add(s);
|
|
148
|
+
}
|
|
149
|
+
return seen.size;
|
|
150
|
+
}
|
|
151
|
+
/** A leak: at least 3 shingles and at least a tenth of the prompt's (or 25, whichever is less). */
|
|
152
|
+
function isLeak(matched, total) {
|
|
153
|
+
if (total === 0 || matched < 3)
|
|
154
|
+
return false;
|
|
155
|
+
return matched >= Math.min(25, Math.max(3, Math.ceil(total * 0.1)));
|
|
156
|
+
}
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// The scan.
|
|
159
|
+
function finding(kind, label, start, end, value) {
|
|
160
|
+
if (kind === 'exfil_link')
|
|
161
|
+
return { kind, label, start, end, masked: '[link removed]' };
|
|
162
|
+
const f = (0, findings_1.makeFinding)(kind === 'custom_kind' ? `custom_${label}` : kind === 'cross_tenant' ? 'cross_tenant' : label, value);
|
|
163
|
+
return { kind, label, start, end, masked: f.masked, ...(f.fingerprint ? { fingerprint: f.fingerprint } : {}) };
|
|
164
|
+
}
|
|
165
|
+
const MAX_SCAN = 200000;
|
|
166
|
+
function scanOutput(text, opts) {
|
|
167
|
+
const p = opts.policy;
|
|
168
|
+
const t = typeof text === 'string' ? (text.length > MAX_SCAN ? text.slice(0, MAX_SCAN) : text) : '';
|
|
169
|
+
const out = [];
|
|
170
|
+
let mustBlock = false;
|
|
171
|
+
if (!t)
|
|
172
|
+
return { findings: out, redacted: typeof text === 'string' ? text : '', mustBlock };
|
|
173
|
+
for (const pat of PATTERNS) {
|
|
174
|
+
if ((pat.kind === 'pii' && !p.pii) || (pat.kind === 'secret' && !p.secrets))
|
|
175
|
+
continue;
|
|
176
|
+
pat.re.lastIndex = 0;
|
|
177
|
+
let m;
|
|
178
|
+
while ((m = pat.re.exec(t)) && out.length < 200) {
|
|
179
|
+
if (pat.check && !pat.check(m[0]))
|
|
180
|
+
continue;
|
|
181
|
+
out.push(finding(pat.kind, pat.label, m.index, m.index + m[0].length, m[0]));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (p.customKinds && opts.customKinds?.length) {
|
|
185
|
+
for (const f of (0, custom_kinds_1.findCustomKinds)(t, opts.customKinds).slice(0, 100))
|
|
186
|
+
out.push(finding('custom_kind', f.id, f.start, f.end, t.slice(f.start, f.end)));
|
|
187
|
+
}
|
|
188
|
+
if (p.crossTenant && opts.foreign?.size) {
|
|
189
|
+
const lower = t.toLowerCase();
|
|
190
|
+
opts.foreign.forEach((_tenant, id) => {
|
|
191
|
+
let at = lower.indexOf(id);
|
|
192
|
+
while (at >= 0 && out.length < 300) {
|
|
193
|
+
out.push(finding('cross_tenant', 'identifier', at, at + id.length, t.slice(at, at + id.length)));
|
|
194
|
+
at = lower.indexOf(id, at + id.length);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
if (p.exfilLinks) {
|
|
199
|
+
const images = new Set();
|
|
200
|
+
for (const re of [MD_IMAGE, HTML_IMAGE]) {
|
|
201
|
+
re.lastIndex = 0;
|
|
202
|
+
let m;
|
|
203
|
+
while ((m = re.exec(t))) {
|
|
204
|
+
const url = m[1];
|
|
205
|
+
const start = m.index + m[0].indexOf(url);
|
|
206
|
+
images.add(start);
|
|
207
|
+
let host = '';
|
|
208
|
+
try {
|
|
209
|
+
host = new URL(url).hostname;
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const allowed = p.allowedDomains.length > 0 && hostAllowed(host, p.allowedDomains);
|
|
215
|
+
if (allowed)
|
|
216
|
+
continue;
|
|
217
|
+
if (p.allowedDomains.length > 0 || urlCarriesData(url))
|
|
218
|
+
out.push(finding('exfil_link', 'image', start, start + url.length, url));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
ANY_URL.lastIndex = 0;
|
|
222
|
+
let m;
|
|
223
|
+
while ((m = ANY_URL.exec(t))) {
|
|
224
|
+
if (images.has(m.index))
|
|
225
|
+
continue;
|
|
226
|
+
const url = m[0].replace(/[.,;:!?]+$/, '');
|
|
227
|
+
let host = '';
|
|
228
|
+
try {
|
|
229
|
+
host = new URL(url).hostname;
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (p.allowedDomains.length > 0 && hostAllowed(host, p.allowedDomains))
|
|
235
|
+
continue;
|
|
236
|
+
if (urlCarriesData(url))
|
|
237
|
+
out.push(finding('exfil_link', 'query_data', m.index, m.index + url.length, url));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (p.canary) {
|
|
241
|
+
const at = t.indexOf(p.canary);
|
|
242
|
+
if (at >= 0) {
|
|
243
|
+
out.push({ kind: 'canary_leak', label: 'canary', start: at, end: at + p.canary.length, masked: '[canary]' });
|
|
244
|
+
mustBlock = true;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (p.systemPromptLeak && opts.systemPrompt) {
|
|
248
|
+
const shingles = promptShingles(opts.systemPrompt);
|
|
249
|
+
const n = leakedShingles(t, shingles);
|
|
250
|
+
if (isLeak(n, shingles.size)) {
|
|
251
|
+
out.push({ kind: 'system_prompt_leak', label: `${n}/${shingles.size}`, start: 0, end: 0, masked: '[system prompt]' });
|
|
252
|
+
mustBlock = true;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return { findings: out, redacted: redact(text, out), mustBlock };
|
|
256
|
+
}
|
|
257
|
+
/** Replaces finding spans with their tokens; overlapping spans merge, the earliest finding's token wins. */
|
|
258
|
+
function redact(text, findings) {
|
|
259
|
+
const spans = findings
|
|
260
|
+
.filter((f) => f.end > f.start)
|
|
261
|
+
.slice()
|
|
262
|
+
.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
263
|
+
const merged = [];
|
|
264
|
+
for (const f of spans) {
|
|
265
|
+
const last = merged[merged.length - 1];
|
|
266
|
+
if (last && f.start < last.end)
|
|
267
|
+
last.end = Math.max(last.end, f.end);
|
|
268
|
+
else
|
|
269
|
+
merged.push({ start: f.start, end: f.end, masked: f.masked });
|
|
270
|
+
}
|
|
271
|
+
let s = text;
|
|
272
|
+
for (let i = merged.length - 1; i >= 0; i--)
|
|
273
|
+
s = s.slice(0, merged[i].start) + merged[i].masked + s.slice(merged[i].end);
|
|
274
|
+
return s;
|
|
275
|
+
}
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
// Streaming.
|
|
278
|
+
/** Characters held back before release, so a value split across chunks is still caught whole. */
|
|
279
|
+
exports.HOLDBACK = 256;
|
|
280
|
+
/** Characters of already-released text kept for context (prompt shingles, links that started earlier). */
|
|
281
|
+
exports.WINDOW = 2048;
|
|
282
|
+
/** Pending characters beyond the holdback before a scan runs: fewer scans of the window, at most this much more delay. */
|
|
283
|
+
exports.RELEASE_STEP = 128;
|
|
284
|
+
/**
|
|
285
|
+
* Scans a streamed answer incrementally. push() returns what may be released
|
|
286
|
+
* now (redacted when asked); end() returns the rest. Memory is bounded by
|
|
287
|
+
* WINDOW + HOLDBACK + one chunk; the prompt-leak count needs only the
|
|
288
|
+
* shingles already seen. When blocking, nothing more is released once a
|
|
289
|
+
* finding that blocks appears.
|
|
290
|
+
*/
|
|
291
|
+
class OutputStreamScanner {
|
|
292
|
+
constructor(opts, enforce) {
|
|
293
|
+
this.opts = opts;
|
|
294
|
+
this.enforce = enforce;
|
|
295
|
+
this.pending = '';
|
|
296
|
+
this.released = '';
|
|
297
|
+
/** Characters released so far, for stable finding ids. */
|
|
298
|
+
this.offset = 0;
|
|
299
|
+
this.leaked = new Set();
|
|
300
|
+
this.tailWords = [];
|
|
301
|
+
/** A word cut by the chunk boundary, completed by the next chunk. */
|
|
302
|
+
this.partial = '';
|
|
303
|
+
this.reported = new Set();
|
|
304
|
+
this.blocked = false;
|
|
305
|
+
this.findings = [];
|
|
306
|
+
this.shingles = opts.policy.systemPromptLeak && opts.systemPrompt ? promptShingles(opts.systemPrompt) : new Set();
|
|
307
|
+
}
|
|
308
|
+
push(chunk) {
|
|
309
|
+
if (this.blocked)
|
|
310
|
+
return { text: '', findings: [], blocked: true };
|
|
311
|
+
this.pending += chunk || '';
|
|
312
|
+
this.countShingles(chunk || '');
|
|
313
|
+
if (this.pending.length <= exports.HOLDBACK + exports.RELEASE_STEP)
|
|
314
|
+
return this.verdict('', []);
|
|
315
|
+
return this.release(this.pending.length - exports.HOLDBACK);
|
|
316
|
+
}
|
|
317
|
+
end() {
|
|
318
|
+
if (this.blocked)
|
|
319
|
+
return { text: '', findings: [], blocked: true };
|
|
320
|
+
this.countShingles(' ');
|
|
321
|
+
return this.release(this.pending.length);
|
|
322
|
+
}
|
|
323
|
+
countShingles(chunk) {
|
|
324
|
+
if (!this.shingles.size)
|
|
325
|
+
return;
|
|
326
|
+
const text = this.partial + chunk;
|
|
327
|
+
const cut = text.match(/[\p{L}\p{N}]+$/u);
|
|
328
|
+
this.partial = cut ? cut[0].slice(-64) : '';
|
|
329
|
+
const w = this.tailWords.concat(words(cut ? text.slice(0, text.length - cut[0].length) : text));
|
|
330
|
+
for (let i = 0; i + SHINGLE <= w.length; i++) {
|
|
331
|
+
const s = w.slice(i, i + SHINGLE).join(' ');
|
|
332
|
+
if (this.shingles.has(s))
|
|
333
|
+
this.leaked.add(s);
|
|
334
|
+
}
|
|
335
|
+
this.tailWords = w.slice(-(SHINGLE - 1));
|
|
336
|
+
}
|
|
337
|
+
release(upTo) {
|
|
338
|
+
// Scan the released tail plus everything pending, so a match straddling the cut is seen whole.
|
|
339
|
+
const ctx = this.released.slice(-exports.WINDOW);
|
|
340
|
+
const scanText = ctx + this.pending;
|
|
341
|
+
const { findings } = scanOutput(scanText, { ...this.opts, systemPrompt: null });
|
|
342
|
+
const fresh = [];
|
|
343
|
+
for (const f of findings) {
|
|
344
|
+
// Only findings that end in the part being released are judged now; later ones wait for more text.
|
|
345
|
+
const start = f.start - ctx.length;
|
|
346
|
+
const end = f.end - ctx.length;
|
|
347
|
+
if (end <= 0)
|
|
348
|
+
continue; // seen and handled in an earlier window
|
|
349
|
+
if (start >= upTo && upTo < this.pending.length)
|
|
350
|
+
continue;
|
|
351
|
+
const id = `${f.kind}:${f.label}:${f.masked}:${start + this.offset}`;
|
|
352
|
+
if (this.reported.has(id))
|
|
353
|
+
continue;
|
|
354
|
+
this.reported.add(id);
|
|
355
|
+
fresh.push({ ...f, start, end });
|
|
356
|
+
}
|
|
357
|
+
if (isLeak(this.leaked.size, this.shingles.size) && !this.reported.has('leak')) {
|
|
358
|
+
this.reported.add('leak');
|
|
359
|
+
fresh.push({ kind: 'system_prompt_leak', label: `${this.leaked.size}/${this.shingles.size}`, start: 0, end: 0, masked: '[system prompt]' });
|
|
360
|
+
}
|
|
361
|
+
const blocks = fresh.some((f) => f.kind === 'system_prompt_leak' || f.kind === 'canary_leak');
|
|
362
|
+
if (fresh.length && this.enforce.block)
|
|
363
|
+
this.blocked = true;
|
|
364
|
+
if (blocks && (this.enforce.block || this.enforce.redact))
|
|
365
|
+
this.blocked = true;
|
|
366
|
+
if (this.blocked) {
|
|
367
|
+
this.findings.push(...fresh);
|
|
368
|
+
this.pending = '';
|
|
369
|
+
return { text: '', findings: fresh, blocked: true };
|
|
370
|
+
}
|
|
371
|
+
// Extend the cut so no finding is split: a value that crosses it is released whole (redacted) now.
|
|
372
|
+
let cut = upTo;
|
|
373
|
+
for (const f of fresh)
|
|
374
|
+
if (f.start < cut && f.end > cut)
|
|
375
|
+
cut = Math.min(this.pending.length, f.end);
|
|
376
|
+
const part = this.pending.slice(0, cut);
|
|
377
|
+
const inPart = fresh.filter((f) => f.start >= 0 && f.end <= cut);
|
|
378
|
+
const text = this.enforce.redact ? redact(part, inPart) : part;
|
|
379
|
+
this.pending = this.pending.slice(cut);
|
|
380
|
+
this.released = (this.released + part).slice(-exports.WINDOW);
|
|
381
|
+
this.offset += part.length;
|
|
382
|
+
this.findings.push(...fresh);
|
|
383
|
+
return this.verdict(text, fresh);
|
|
384
|
+
}
|
|
385
|
+
verdict(text, findings) {
|
|
386
|
+
return { text, findings, blocked: this.blocked };
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
exports.OutputStreamScanner = OutputStreamScanner;
|
|
390
|
+
//# sourceMappingURL=output.js.map
|