diff --git a/.dockerignore b/.dockerignore index 1ced1087f0..afba16287c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -303,6 +303,7 @@ docs/build/ # PyBuilder target/ **/target/ +**/target/** # Jupyter Notebook .ipynb_checkpoints diff --git a/.secrets.baseline b/.secrets.baseline index fcf491817d..531dde4201 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -1,9 +1,9 @@ { "exclude": { - "files": "^.secrets.baseline$|package-lock.json", + "files": "package-lock.json|Cargo.lock|^.secrets.baseline$|scripts/sign_image.sh|scripts/zap|sonar-project.properties|^/Users/brian/dev/github.ibm.com/contextforge-org/sps-pipeline-config/.secrets.baseline$|^./.secrets.baseline$", "lines": null }, - "generated_at": "2026-04-02T14:06:19Z", + "generated_at": "2026-04-02T15:29:25Z", "plugins_used": [ { "name": "AWSKeyDetector" @@ -366,11 +366,11 @@ }, { "hashed_secret": "43fc45734b96bcb1b6cef373e949eb3524ae199b", - "is_secret": false, "is_verified": false, "line_number": 1485, "type": "Secret Keyword", - "verified_result": null + "verified_result": null, + "is_secret": false }, { "hashed_secret": "9d989e8d27dc9e0ec3389fc855f142c3d40f0c50", @@ -384,7 +384,7 @@ "hashed_secret": "d3ac7a4ef1a838b4134f2f6e7f3c0d249d74b674", "is_secret": false, "is_verified": false, - "line_number": 5992, + "line_number": 6075, "type": "Secret Keyword", "verified_result": null }, @@ -392,7 +392,7 @@ "hashed_secret": "5932862bcd24dd27d0dc0407ec94fe9d6ea24aeb", "is_secret": false, "is_verified": false, - "line_number": 6489, + "line_number": 6572, "type": "Secret Keyword", "verified_result": null }, @@ -400,7 +400,7 @@ "hashed_secret": "c77c805e32f173e4321ee9187de9c29cb3804513", "is_secret": false, "is_verified": false, - "line_number": 6501, + "line_number": 6584, "type": "Secret Keyword", "verified_result": null }, @@ -408,7 +408,7 @@ "hashed_secret": "8fe3df8a68ddd0d4ab2214186cbb8e38ccd0e06a", "is_secret": false, "is_verified": false, - "line_number": 6573, + "line_number": 6656, "type": "Secret Keyword", "verified_result": null }, @@ -416,7 +416,7 @@ "hashed_secret": "93ac8946882128457cd9e283b30ca851945e6690", "is_secret": false, "is_verified": false, - "line_number": 7676, + "line_number": 7759, "type": "Secret Keyword", "verified_result": null } @@ -1094,27 +1094,27 @@ "docker-compose.with-langfuse.yml": [ { "hashed_secret": "cb58df830a45cc33df1a313e616ecad78cd796c5", - "is_secret": false, "is_verified": false, "line_number": 125, "type": "Secret Keyword", - "verified_result": null + "verified_result": null, + "is_secret": false }, { "hashed_secret": "2e0c522bfe4e7885492862df2e0b987c0ca02623", - "is_secret": false, "is_verified": false, "line_number": 148, "type": "Secret Keyword", - "verified_result": null + "verified_result": null, + "is_secret": false }, { "hashed_secret": "d9d007c8de197b3f36a3a0ba4f13c0f7df175d5a", - "is_secret": false, "is_verified": false, "line_number": 312, "type": "Secret Keyword", - "verified_result": null + "verified_result": null, + "is_secret": false } ], "docker-compose.yml": [ @@ -2638,11 +2638,11 @@ "docs/docs/manage/observability/langfuse.md": [ { "hashed_secret": "0c6e5ac9cb218c0a666019d0814c5029b374fb17", - "is_secret": false, "is_verified": false, "line_number": 221, "type": "Secret Keyword", - "verified_result": null + "verified_result": null, + "is_secret": false } ], "docs/docs/manage/observability/observability.md": [ @@ -5081,20155 +5081,6257 @@ "verified_result": null } ], - "mcp-servers/rust/fast-test-server/Cargo.lock": [ + "mcpgateway/admin.py": [ { - "hashed_secret": "e0f930ce4dc6ee91bd9c13f93bdd411a875847ad", + "hashed_secret": "fa9beb99e4029ad5a6615399e7bbae21356086b3", "is_secret": false, "is_verified": false, - "line_number": 9, - "type": "Hex High Entropy String", + "line_number": 4256, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "0c2b06b30faf95e40c75aaaefa957d835c1df392", + "hashed_secret": "559b05f1b2863e725b76e216ac3dadecbf92e244", "is_secret": false, "is_verified": false, - "line_number": 18, - "type": "Hex High Entropy String", + "line_number": 4864, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "053152e6d6ec523a570bc615219d6c829a0d455b", + "hashed_secret": "a8af4759392d4f7496d613174f33afe2074a4b8d", "is_secret": false, "is_verified": false, - "line_number": 27, - "type": "Hex High Entropy String", + "line_number": 4866, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "94da59c445b7bfedb03dfc1ee4ade601cee2c067", + "hashed_secret": "85b60d811d16ff56b3654587d4487f713bfa33b7", "is_secret": false, "is_verified": false, - "line_number": 33, - "type": "Hex High Entropy String", + "line_number": 14931, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "mcpgateway/alembic.ini": [ { - "hashed_secret": "07781af8811774ce3bf3152bbfa1c98d7ea03c61", + "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", "is_secret": false, "is_verified": false, - "line_number": 44, - "type": "Hex High Entropy String", + "line_number": 87, + "type": "Basic Auth Credentials", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/04cda6733305_add_admin_types_to_email_users.py": [ { - "hashed_secret": "ece49906f7a72ed3cfb012e5e2e1209e1b226172", + "hashed_secret": "c052f9f7b5b94886b423384e4938ed8388cd77b4", "is_secret": false, "is_verified": false, - "line_number": 50, + "line_number": 28, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/0f81d4a5efe0_new_table_email_team_member_history_for_.py": [ { - "hashed_secret": "388b28008bbc41d55a4ab30e40375d58ef630445", + "hashed_secret": "aeaf4ea9e7c81ad714da7db03735fd1d053414c7", "is_secret": false, "is_verified": false, - "line_number": 56, + "line_number": 20, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/14ac971cee42_add_user_context_to_oauth_tokens.py": [ { - "hashed_secret": "181759bd3798be44125fc8c50e62ba054d318783", + "hashed_secret": "8e70fb96660fbce85c4be348188c03c3d98a5e95", "is_secret": false, "is_verified": false, - "line_number": 89, + "line_number": 19, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/191a2def08d7_resource_rename_template_to_uri_template.py": [ { - "hashed_secret": "b25a21f1a0e2fc65dc90424e07d7210587e8b296", + "hashed_secret": "36b0556065adc2fdd8a15b7ffac028a043c34595", "is_secret": false, "is_verified": false, - "line_number": 108, + "line_number": 17, "type": "Hex High Entropy String", "verified_result": null }, { - "hashed_secret": "457b24d26a9cd360a12b56f87c6f1daf3d30a895", + "hashed_secret": "0c1c30eba610778ef5e68a58883a052733a22bd4", "is_secret": false, "is_verified": false, - "line_number": 114, + "line_number": 18, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/1fc1795f6983_merge_a2a_and_custom_name_changes.py": [ { - "hashed_secret": "b4808fb54c15b4bfe5268dd5ef3669d35302dac8", + "hashed_secret": "b6d082a6cc1261d009b34cddd19dca4828f1cb7d", "is_secret": false, "is_verified": false, - "line_number": 120, + "line_number": 18, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/34492f99a0c4_add_comprehensive_metadata_to_all_.py": [ { - "hashed_secret": "20af2a76b19cc7a1a93269f1e5f876f58bd266cc", + "hashed_secret": "9743f9059c52643b7cc917663462e853fb36f69d", "is_secret": false, "is_verified": false, - "line_number": 126, + "line_number": 23, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/356a2d4eed6f_uuid_change_for_prompt_and_resources.py": [ { - "hashed_secret": "435b5a83c55f72c24e5bd183795441de1a767dc2", + "hashed_secret": "c81482140a7bf22e957d89f358479e90adef80dc", "is_secret": false, "is_verified": false, - "line_number": 132, + "line_number": 20, "type": "Hex High Entropy String", "verified_result": null }, { - "hashed_secret": "eac7344fb138e2bc70ec74dd15a52fe0c253887a", + "hashed_secret": "c561aac847538846232699cfaed7fc7fabd1d134", "is_secret": false, "is_verified": false, - "line_number": 142, + "line_number": 21, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/3b17fdc40a8d_add_passthrough_headers_to_gateways_and_.py": [ { - "hashed_secret": "a01b71d602bc5018e96db1a555a02018f474f151", + "hashed_secret": "044c1c4f5cc1f89969f46018b2bacbe8918aaf1a", "is_secret": false, "is_verified": false, - "line_number": 148, + "line_number": 22, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/3c89a45f32e5_add_grpc_services_table.py": [ { - "hashed_secret": "aea69a0db4b6fb9c1801ecd9266c98e51a5b6515", + "hashed_secret": "83af00f2e79329f549c1438dec118e0cba4ad111", "is_secret": false, "is_verified": false, - "line_number": 162, + "line_number": 18, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/43c07ed25a24_add_oauth_fields_to_servers.py": [ { - "hashed_secret": "9cdec82ff07c4062846684c9ba65e4dc199923d2", + "hashed_secret": "48ffbad96aa9c2b33f9486f5a3c2108198acb518", "is_secret": false, "is_verified": false, - "line_number": 168, + "line_number": 21, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/4e6273136e56_normalize_registered_oauth_client_issuer.py": [ { - "hashed_secret": "9264404b12f829d6dfbfdac1825b97e54fcbb535", + "hashed_secret": "2d40811ded1703d949aca3c40d77175a5c568c05", "is_secret": false, "is_verified": false, - "line_number": 178, + "line_number": 24, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/5f3c681b05e1_add_index_for_team_name.py": [ { - "hashed_secret": "df7d483ec7a8e3b39542662290f8d52a71439b7a", + "hashed_secret": "e6ec1410341b496819aeb85882509889c33017af", "is_secret": false, "is_verified": false, - "line_number": 191, + "line_number": 20, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/61ee11c482d6_add_code_verifier_to_oauth_states_for_.py": [ { - "hashed_secret": "1652117d4ce1279d3c4dc2c3dd53cfea2d615645", + "hashed_secret": "aeaf4ea9e7c81ad714da7db03735fd1d053414c7", "is_secret": false, "is_verified": false, - "line_number": 202, + "line_number": 19, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/64acf94cb7f2_cleanup_inactive_duplicate_user_roles.py": [ { - "hashed_secret": "e3423649a06049806d8883193ffa13f6488898ba", + "hashed_secret": "dc7c15edd98c7c7ce4cd107fb4ccb17329510002", "is_secret": false, "is_verified": false, - "line_number": 208, + "line_number": 24, "type": "Hex High Entropy String", "verified_result": null }, { - "hashed_secret": "39e1dc7e8bd1f918717fde66f30bc3884780a612", + "hashed_secret": "52294c20155b397fbf140baa0cfb6d9400c82c25", "is_secret": false, "is_verified": false, - "line_number": 214, + "line_number": 25, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/733159a4fa74_add_display_name_to_tools.py": [ { - "hashed_secret": "4d2c6fb73af42ea62f965535a82e83df5411d419", + "hashed_secret": "b6d082a6cc1261d009b34cddd19dca4828f1cb7d", "is_secret": false, "is_verified": false, - "line_number": 243, + "line_number": 23, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/77243f5bfce5_add_tool_id_to_a2a_agents.py": [ { - "hashed_secret": "fc40fb75e9edfd1c082708dbcc1384d9e68f90f7", + "hashed_secret": "21562fcd9707470b4fa7cec05458e86caeb25fda", "is_secret": false, "is_verified": false, - "line_number": 249, + "line_number": 18, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/8a16a77260f0_adding_original_description_column_to_.py": [ { - "hashed_secret": "5dd5a3b90a4aec184d6390d782a283adf53b87f9", + "hashed_secret": "10b36c8089b5791d82f0700828fd23a9c782101f", "is_secret": false, "is_verified": false, - "line_number": 255, + "line_number": 19, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/8a2934be50c0_rest_pass_api_fld_tools.py": [ { - "hashed_secret": "8d05fb42532fecd995f8eff2190f412088d6178f", + "hashed_secret": "c84583fef8c1325676bd098d09cfc64f185792f7", "is_secret": false, "is_verified": false, - "line_number": 264, + "line_number": 18, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/9e028ecf59c4_tag_records_changes_list_str_to_list_.py": [ { - "hashed_secret": "89cf74f6e03a27de9d661324442e0b372ad9eefb", + "hashed_secret": "c561aac847538846232699cfaed7fc7fabd1d134", "is_secret": false, "is_verified": false, - "line_number": 279, + "line_number": 19, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/9f5d93ced2b3_add_llm_permissions_to_default_roles.py": [ { - "hashed_secret": "e72195ef7791709c24e8be4349842896a88bab07", + "hashed_secret": "99927e177522cd7b4b713ac9fe99968d2f71b829", "is_secret": false, "is_verified": false, - "line_number": 289, + "line_number": 26, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/a23a08d61eb0_add_observability_tables.py": [ { - "hashed_secret": "f2c63a981fbb2e2b39e4c05d8ec787e3befa3b07", + "hashed_secret": "9d88d7eece452a76b2955ecfbb251f23cf7b7905", "is_secret": false, "is_verified": false, - "line_number": 295, + "line_number": 18, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/a3c38b6c2437_fix_a2a_agents_auth_value.py": [ { - "hashed_secret": "f5cdeb5a0b086c5a674eb8ad81bf17fd63bd0a01", + "hashed_secret": "52294c20155b397fbf140baa0cfb6d9400c82c25", "is_secret": false, "is_verified": false, - "line_number": 306, + "line_number": 31, "type": "Hex High Entropy String", "verified_result": null }, { - "hashed_secret": "2ebad4ca4792566abd1ba0d665249302119cb457", + "hashed_secret": "9b094435d625c9385a1bb083604f87fa7c225da0", "is_secret": false, "is_verified": false, - "line_number": 312, + "line_number": 32, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/a4f1c7d8e9b0_add_app_user_email_to_oauth_states.py": [ { - "hashed_secret": "7f0ff8004a236b8bba117372cb42ca731446c4b6", + "hashed_secret": "5ebf53ef06815a59e9fd0378d3a19dff8b0ac28a", "is_secret": false, "is_verified": false, - "line_number": 323, + "line_number": 17, "type": "Hex High Entropy String", "verified_result": null }, { - "hashed_secret": "c4bf225e39c6d664754ca87cb8751f8a2a093134", + "hashed_secret": "99927e177522cd7b4b713ac9fe99968d2f71b829", "is_secret": false, "is_verified": false, - "line_number": 329, + "line_number": 18, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/a7f3c9e1b2d4_add_title_to_tools_resources_prompts.py": [ { - "hashed_secret": "8ee82c36fc0af17f1fdeb940d2bca6424bf2e453", - "is_secret": false, + "hashed_secret": "2e6b7b27cad43ed0908be745231a98936aad5293", "is_verified": false, - "line_number": 335, + "line_number": 17, "type": "Hex High Entropy String", - "verified_result": null - }, + "verified_result": null, + "is_secret": false + } + ], + "mcpgateway/alembic/versions/a8f3b2c1d4e5_add_gateway_refresh_fields.py": [ { - "hashed_secret": "fb738e4a202642582d5a12f41a884e26001c4fa6", + "hashed_secret": "19c9ebc4037f02acb15d950b258d51e9eb249966", "is_secret": false, "is_verified": false, - "line_number": 352, + "line_number": 19, "type": "Hex High Entropy String", "verified_result": null }, { - "hashed_secret": "fb98da5b813e3ae31d0e430526334ae9f010a546", + "hashed_secret": "21562fcd9707470b4fa7cec05458e86caeb25fda", "is_secret": false, "is_verified": false, - "line_number": 363, + "line_number": 20, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/aac21d6f9522_merge_ca_cert_and_observability_heads.py": [ { - "hashed_secret": "a7bb22eb6421d498f26d952fe0635a1d04a27bcd", + "hashed_secret": "74cf584eb4fffdfa472c808000adb71841786f3e", "is_secret": false, "is_verified": false, - "line_number": 375, + "line_number": 14, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/abf8ac3b6008_add_admin_overview_and_servers_use_to_.py": [ { - "hashed_secret": "399da540bd166b0c7bb3a2815bb400eb11e5638d", + "hashed_secret": "dc7c15edd98c7c7ce4cd107fb4ccb17329510002", "is_secret": false, "is_verified": false, - "line_number": 388, + "line_number": 32, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/add_oauth_tokens_table.py": [ { - "hashed_secret": "f3d3c26ed24a648081ced9182bb1b19f7d78e68d", + "hashed_secret": "3333a8285ac54459f46f2ef561184275586dd1a7", "is_secret": false, "is_verified": false, - "line_number": 397, + "line_number": 23, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/b2d9c6e4f1a7_add_explicit_team_and_token_permissions.py": [ { - "hashed_secret": "1d591d1374352e720051c9d3f7b1e2c5557d5fd6", + "hashed_secret": "c4770f062e9bfa8ec054475f98315913da1d16fd", "is_secret": false, "is_verified": false, - "line_number": 403, + "line_number": 25, "type": "Hex High Entropy String", "verified_result": null }, { - "hashed_secret": "0fa72c3d33bbc7feb5618105d7b5997001614612", + "hashed_secret": "5ebf53ef06815a59e9fd0378d3a19dff8b0ac28a", "is_secret": false, "is_verified": false, - "line_number": 409, + "line_number": 26, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/b9e496e91e71_merge_gateway_refresh_and_sso_provider_.py": [ { - "hashed_secret": "c9fc785a64064e9f54662a39349af4f8ca9fc333", + "hashed_secret": "19c9ebc4037f02acb15d950b258d51e9eb249966", "is_secret": false, "is_verified": false, - "line_number": 419, + "line_number": 14, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/ba202ac1665f_migrate_user_roles_to_configurable_.py": [ { - "hashed_secret": "ce3dad8a04c408de2c43ad7c3ed460e21897bc5d", + "hashed_secret": "10b36c8089b5791d82f0700828fd23a9c782101f", "is_secret": false, "is_verified": false, - "line_number": 429, + "line_number": 39, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/c96c11c111b4_create_index_on_user_name.py": [ { - "hashed_secret": "92f04de23131564ceddfbbd126b98292b0934577", + "hashed_secret": "21562fcd9707470b4fa7cec05458e86caeb25fda", "is_secret": false, "is_verified": false, - "line_number": 442, + "line_number": 29, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/cfc3d6aa0fb2_consolidated_multiuser_team_rbac_.py": [ { - "hashed_secret": "3976cf14dbb91cafcaaddad7d5b88cf44aba7b58", + "hashed_secret": "f216ad54219955ae0c4c306010eee4c60b0ec6e1", "is_secret": false, "is_verified": false, - "line_number": 448, + "line_number": 30, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/d9e0f1a2b3c4_change_token_uniqueness_to_per_team.py": [ { - "hashed_secret": "1f969023518451bb7f23d59ff7c96c0a6a259877", + "hashed_secret": "45fc9ab5f41816c6979df8bc85ffdc160a0e696a", "is_secret": false, "is_verified": false, - "line_number": 454, + "line_number": 29, "type": "Hex High Entropy String", "verified_result": null }, { - "hashed_secret": "b46c956f080e29b6f05c5538b6f134e2417d839c", + "hashed_secret": "c4770f062e9bfa8ec054475f98315913da1d16fd", "is_secret": false, "is_verified": false, - "line_number": 475, + "line_number": 30, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/e182847d89e6_unique_constraints_changes_for_gateways_.py": [ { - "hashed_secret": "ace4c15ef5d5b8c769f9d24cf4bf1de85fecb2c7", + "hashed_secret": "8e70fb96660fbce85c4be348188c03c3d98a5e95", "is_secret": false, "is_verified": false, - "line_number": 490, + "line_number": 22, "type": "Hex High Entropy String", "verified_result": null }, { - "hashed_secret": "9bfd048ce5ceb106ba035489cea34b77d79814e4", + "hashed_secret": "f216ad54219955ae0c4c306010eee4c60b0ec6e1", "is_secret": false, "is_verified": false, - "line_number": 505, + "line_number": 23, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/e1f2a3b4c5d6_add_grant_source_to_user_roles.py": [ { - "hashed_secret": "7ed5883c89e869acd5ac1fb9e12730f4a42b29c4", + "hashed_secret": "9b094435d625c9385a1bb083604f87fa7c225da0", "is_secret": false, "is_verified": false, - "line_number": 514, + "line_number": 22, "type": "Hex High Entropy String", "verified_result": null }, { - "hashed_secret": "c6818e944c28f358430e50279f8552ba70d43b1c", + "hashed_secret": "45fc9ab5f41816c6979df8bc85ffdc160a0e696a", "is_secret": false, "is_verified": false, - "line_number": 520, + "line_number": 23, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/e5a59c16e041_unique_const_changes_for_prompt_and_.py": [ { - "hashed_secret": "4faeea7fc4d0ed985b094fe85a90aab7fc3d7ae8", + "hashed_secret": "81d89c741bbf0978e92cb9852870242cc88c7db2", "is_secret": false, "is_verified": false, - "line_number": 526, + "line_number": 18, "type": "Hex High Entropy String", "verified_result": null }, { - "hashed_secret": "dc02a9de5e15472f3ab7614b78d04a3f9c42aa61", + "hashed_secret": "c84583fef8c1325676bd098d09cfc64f185792f7", "is_secret": false, "is_verified": false, - "line_number": 538, + "line_number": 19, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/eb17fd368f9d_merge_passthrough_headers_and_tags_.py": [ { - "hashed_secret": "93408d219072bd897710a546d04acbdf8f518992", + "hashed_secret": "9743f9059c52643b7cc917663462e853fb36f69d", "is_secret": false, "is_verified": false, - "line_number": 544, + "line_number": 18, "type": "Hex High Entropy String", "verified_result": null }, { - "hashed_secret": "190d967672500a817f10cf8c5f8ae7f1f73c006a", + "hashed_secret": "044c1c4f5cc1f89969f46018b2bacbe8918aaf1a", "is_secret": false, "is_verified": false, - "line_number": 554, + "line_number": 19, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/ee288b094280_add_auth_query_params_to_gateways.py": [ { - "hashed_secret": "8977d964704890333a861cc91fb4738b73ca2c65", + "hashed_secret": "48ffbad96aa9c2b33f9486f5a3c2108198acb518", "is_secret": false, "is_verified": false, - "line_number": 560, + "line_number": 21, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/f1a2b3c4d5e6_add_auth_query_params_to_a2a_agents.py": [ { - "hashed_secret": "29cc4d113d2b819fd175fde15139a9047b4714ae", + "hashed_secret": "2d40811ded1703d949aca3c40d77175a5c568c05", "is_secret": false, "is_verified": false, - "line_number": 566, + "line_number": 20, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/f3a3a3d901b8_remove_gateway_url_unique_constraint.py": [ { - "hashed_secret": "2e270dc90abd37a31d83eec5f1e49c3a6ec79e76", + "hashed_secret": "0c1c30eba610778ef5e68a58883a052733a22bd4", "is_secret": false, "is_verified": false, - "line_number": 572, + "line_number": 24, "type": "Hex High Entropy String", "verified_result": null }, { - "hashed_secret": "2d70a27f9bdfdb0ebea3fa79f7962fa957bd11fa", + "hashed_secret": "74cf584eb4fffdfa472c808000adb71841786f3e", "is_secret": false, "is_verified": false, - "line_number": 578, + "line_number": 25, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/f8c9d3e2a1b4_add_oauth_config_to_gateways.py": [ { - "hashed_secret": "b7db53b4da996a7c3f03e516ecfb7b6663ad3a67", + "hashed_secret": "3333a8285ac54459f46f2ef561184275586dd1a7", "is_secret": false, "is_verified": false, - "line_number": 584, + "line_number": 22, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/g1a2b3c4d5e6_add_pagination_indexes.py": [ { - "hashed_secret": "551358ac7f3ecd5113292ebdd3a797bc72f72f08", + "hashed_secret": "81d89c741bbf0978e92cb9852870242cc88c7db2", "is_secret": false, "is_verified": false, - "line_number": 593, + "line_number": 19, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/h2b3c4d5e6f7_add_oauth_config_to_a2a_agents.py": [ { - "hashed_secret": "52174d432f72b603ab3aeb65e046d8842f64eba8", + "hashed_secret": "83af00f2e79329f549c1438dec118e0cba4ad111", "is_secret": false, "is_verified": false, - "line_number": 599, + "line_number": 23, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/i3c4d5e6f7g8_add_observability_performance_indexes.py": [ { - "hashed_secret": "6b855f1f95b14579ef94fd09bab1c34327526c1e", + "hashed_secret": "9d88d7eece452a76b2955ecfbb251f23cf7b7905", "is_secret": false, "is_verified": false, - "line_number": 605, + "line_number": 18, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/k5e6f7g8h9i0_add_structured_logging_tables.py": [ { - "hashed_secret": "22c495789b158bf96a04f9bd11015ad54eace5c6", + "hashed_secret": "c81482140a7bf22e957d89f358479e90adef80dc", "is_secret": false, "is_verified": false, - "line_number": 611, + "line_number": 16, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/u5f6g7h8i9j0_add_provider_metadata_to_sso_providers.py": [ { - "hashed_secret": "dc2e94f24b05f113bf345bb2afbcb6e26645cd5c", + "hashed_secret": "e6ec1410341b496819aeb85882509889c33017af", "is_secret": false, "is_verified": false, - "line_number": 622, + "line_number": 28, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/v1a2b3c4d5e6_assign_default_viewer_role_to_existing_users.py": [ { - "hashed_secret": "ebbea2a8b6a37ded68465f81f315d4a23c27a8e8", + "hashed_secret": "c052f9f7b5b94886b423384e4938ed8388cd77b4", "is_secret": false, "is_verified": false, - "line_number": 631, + "line_number": 39, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/alembic/versions/z1a2b3c4d5e6_add_password_change_required.py": [ { - "hashed_secret": "9f45c3d9955af35039612741be99a94f2e719c26", + "hashed_secret": "36b0556065adc2fdd8a15b7ffac028a043c34595", "is_secret": false, "is_verified": false, - "line_number": 641, + "line_number": 16, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/cache/session_registry.py": [ { - "hashed_secret": "a4cf867673699f5dc3d18038b5a87c2a7bf4dfd3", + "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", "is_secret": false, "is_verified": false, - "line_number": 647, - "type": "Hex High Entropy String", + "line_number": 152, + "type": "Basic Auth Credentials", "verified_result": null - }, + } + ], + "mcpgateway/common/validators.py": [ { - "hashed_secret": "a02ff67f8e504e59af8eff72afb26fda27037a42", + "hashed_secret": "c377074d6473f35a91001981355da793dc808ffd", "is_secret": false, "is_verified": false, - "line_number": 653, + "line_number": 700, "type": "Hex High Entropy String", "verified_result": null }, { - "hashed_secret": "d978e33d84309bf97aa5af7e47486be164c0787e", + "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", "is_secret": false, "is_verified": false, - "line_number": 659, - "type": "Hex High Entropy String", + "line_number": 1102, + "type": "Basic Auth Credentials", "verified_result": null - }, + } + ], + "mcpgateway/config.py": [ { - "hashed_secret": "f2ceefa876785b2d7376d2af71e760c71ec0109e", + "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", "is_secret": false, "is_verified": false, - "line_number": 665, - "type": "Hex High Entropy String", + "line_number": 221, + "type": "Basic Auth Credentials", "verified_result": null }, { - "hashed_secret": "978bcf06dc12b74b1579b446ae082265db1358f6", + "hashed_secret": "ff37a98a9963d347e9749a5c1b3936a4a245a6ff", "is_secret": false, "is_verified": false, - "line_number": 671, - "type": "Hex High Entropy String", + "line_number": 2108, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "mcpgateway/db.py": [ { - "hashed_secret": "8cc69a0306a6b8f4c32428f44505f11eaebb3ebf", + "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", "is_secret": false, "is_verified": false, - "line_number": 680, - "type": "Hex High Entropy String", + "line_number": 80, + "type": "Basic Auth Credentials", "verified_result": null - }, + } + ], + "mcpgateway/llm_provider_configs.py": [ { - "hashed_secret": "6538f9a3e2204f918d5719f17a23367f4d2105c7", + "hashed_secret": "25910f981e85ca04baf359199dd0bd4a3ae738b6", "is_secret": false, "is_verified": false, - "line_number": 690, - "type": "Hex High Entropy String", + "line_number": 308, + "type": "AWS Access Key", "verified_result": null }, { - "hashed_secret": "b4259ea178dd7cb0249df5d8feb7d13a2d6fd820", + "hashed_secret": "d70eab08607a4d05faa2d0d6647206599e9abc65", "is_secret": false, "is_verified": false, - "line_number": 699, - "type": "Hex High Entropy String", + "line_number": 316, + "type": "Base64 High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/middleware/request_logging_middleware.py": [ { - "hashed_secret": "fb82e6c6c8d3ed2486ef8249d7e59336b5c86610", + "hashed_secret": "e9fe51f94eadabf54dbf2fbbd57188b9abee436e", "is_secret": false, "is_verified": false, - "line_number": 708, - "type": "Hex High Entropy String", + "line_number": 40, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "ba44f293d24fbbcbe1ef98385355f7fed4f8419d", + "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", "is_secret": false, "is_verified": false, - "line_number": 714, - "type": "Hex High Entropy String", + "line_number": 171, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "47aeac2fa4acd0b3b040c9927a26128bf0c029af", + "hashed_secret": "1073ab6cda4b991cd29f9e83a307f34004ae9327", "is_secret": false, "is_verified": false, - "line_number": 720, - "type": "Hex High Entropy String", + "line_number": 177, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "bfdcf12c00b303dde9a57ba3e08927a60a89c6b8", + "hashed_secret": "d4fe581561f18ee5006254a7e53f53cbed780bc2", "is_secret": false, "is_verified": false, - "line_number": 731, - "type": "Hex High Entropy String", + "line_number": 259, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "mcpgateway/plugins/framework/external/grpc/tls_utils.py": [ { - "hashed_secret": "ef2e592f89cb7fff6bccacece98f099ca10add8f", + "hashed_secret": "623e76c36aa2a886542011e28412cc761d7ceb01", "is_secret": false, "is_verified": false, - "line_number": 741, - "type": "Hex High Entropy String", + "line_number": 132, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "mcpgateway/plugins/framework/hooks/policies.py": [ { - "hashed_secret": "8be0eab839d1133fc403dcc2790b84d4dfff3381", + "hashed_secret": "e812ba8d00b270ef3502bb53ceb31e8c5188f14e", "is_secret": false, "is_verified": false, - "line_number": 751, - "type": "Hex High Entropy String", + "line_number": 98, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "mcpgateway/plugins/framework/validators.py": [ { - "hashed_secret": "def835c5f3d29ef5143e04e29a61cd85a81fe09e", + "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", "is_secret": false, "is_verified": false, - "line_number": 761, - "type": "Hex High Entropy String", + "line_number": 115, + "type": "Basic Auth Credentials", "verified_result": null - }, + } + ], + "mcpgateway/routers/auth.py": [ { - "hashed_secret": "891ddfb5f57f136198961b98f8014e3c7bc9e602", + "hashed_secret": "aa1a82fe15c74459f1261961b07ae924e2b94ab2", "is_secret": false, "is_verified": false, - "line_number": 770, - "type": "Hex High Entropy String", + "line_number": 150, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "mcpgateway/routers/email_auth.py": [ { - "hashed_secret": "009be79e577350e7372a50b6359f46f336fd19b7", + "hashed_secret": "6993a3fd94a012ab50fb6b9e97ec238310f0b177", "is_secret": false, "is_verified": false, - "line_number": 779, - "type": "Hex High Entropy String", + "line_number": 397, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "74ac836ae646fd72c7c162ace0d10d50d81af86b", + "hashed_secret": "52dcc83ec1e54426ad58a64854d1eb8d5f5d9685", "is_secret": false, "is_verified": false, - "line_number": 789, - "type": "Hex High Entropy String", + "line_number": 398, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "852251ba1e8919a04b1d4678fbf9ba2a73e82a19", + "hashed_secret": "a616a64c0fbc30f12287d0f24f3b90dd2e6a206e", "is_secret": false, "is_verified": false, - "line_number": 798, - "type": "Hex High Entropy String", + "line_number": 680, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "mcpgateway/routers/oauth_router.py": [ { - "hashed_secret": "c63addae2503bfe26923f0460158bb2fc674b928", + "hashed_secret": "d3ecb0d890368d7659ee54010045b835dacb8efe", "is_secret": false, "is_verified": false, - "line_number": 809, - "type": "Hex High Entropy String", + "line_number": 610, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "mcpgateway/schemas.py": [ { - "hashed_secret": "9f7a7461ae8d5e27458e899b4e84416483680a51", + "hashed_secret": "c377074d6473f35a91001981355da793dc808ffd", "is_secret": false, "is_verified": false, - "line_number": 820, + "line_number": 4111, "type": "Hex High Entropy String", "verified_result": null }, { - "hashed_secret": "d7eb4b2e2903f99d21a81b63f8dc61caf9653962", + "hashed_secret": "6367c48dd193d56ea7b0baad25b19455e529f5ee", "is_secret": false, "is_verified": false, - "line_number": 826, - "type": "Hex High Entropy String", + "line_number": 5224, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "f6e7445fc17e030fff9f666217b155b72274e314", + "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", "is_secret": false, "is_verified": false, - "line_number": 858, - "type": "Hex High Entropy String", + "line_number": 5388, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "9daac1b266d3875ecff4a4b7932e73d63f51718f", + "hashed_secret": "f42a3fabe1e9bed059d727f47eb752e3aa61b977", "is_secret": false, "is_verified": false, - "line_number": 871, - "type": "Hex High Entropy String", + "line_number": 5445, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "60a673ff967b996194d288e551490d960a18d4e2", + "hashed_secret": "b85788b459aa4d67e1070930dae6d0827756aadb", "is_secret": false, "is_verified": false, - "line_number": 877, - "type": "Hex High Entropy String", + "line_number": 5483, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "783f9b75a939be68c3c05223c34ff5948fb4c52b", + "hashed_secret": "52dcc83ec1e54426ad58a64854d1eb8d5f5d9685", "is_secret": false, "is_verified": false, - "line_number": 883, - "type": "Hex High Entropy String", + "line_number": 5484, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "mcpgateway/scripts/validate_env.py": [ { - "hashed_secret": "58f0106d7117389d48944ab0421c44ca96dd7992", + "hashed_secret": "b0fb9e3dbf6beca57a1e203dddab80910598ec3d", "is_secret": false, "is_verified": false, - "line_number": 895, - "type": "Hex High Entropy String", + "line_number": 112, + "type": "Base64 High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/services/argon2_service.py": [ { - "hashed_secret": "4ca26daad23a4d2006728ec49e27020cfc307869", + "hashed_secret": "f13733f6dd9f1ed3118e2da31428c71eab5ffd99", "is_secret": false, "is_verified": false, - "line_number": 909, - "type": "Hex High Entropy String", + "line_number": 50, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "mcpgateway/services/email_auth_service.py": [ { - "hashed_secret": "bc37fdf97c85de38881099f997b799f0ce27c830", + "hashed_secret": "f42a3fabe1e9bed059d727f47eb752e3aa61b977", "is_secret": false, "is_verified": false, - "line_number": 921, - "type": "Hex High Entropy String", + "line_number": 573, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "mcpgateway/services/mcp_client_chat_service.py": [ { - "hashed_secret": "ba8ac4ac3810916dd75cdf9bb539b045d8f828f4", + "hashed_secret": "11fa7c37d697f30e6aee828b4426a10f83ab2380", "is_secret": false, "is_verified": false, - "line_number": 933, - "type": "Hex High Entropy String", + "line_number": 526, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "dd30e69b06fc9e7153b91d6152b47abee06bc76e", + "hashed_secret": "94b778a67d72bc0e8fa5838e7fa48b37739a9e30", "is_secret": false, "is_verified": false, - "line_number": 939, - "type": "Hex High Entropy String", + "line_number": 624, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "edea2c54c8d390fdfff6535d3da4fd1fdf17b62a", + "hashed_secret": "6d9c68c603e465077bdd49c62347fe54717f83a3", "is_secret": false, "is_verified": false, - "line_number": 949, - "type": "Hex High Entropy String", + "line_number": 719, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "bfb212002ccc72caf73acbfb0a5bd6c1c2acaa42", + "hashed_secret": "c7a8c334eef5d1749fface7d42c66f9ae5e8cf36", "is_secret": false, "is_verified": false, - "line_number": 958, - "type": "Hex High Entropy String", + "line_number": 1157, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "044ff8fe1e6d9bb34b8e097fa053256f451e1d65", + "hashed_secret": "a62f2225bf70bfaccbc7f1ef2a397836717377de", "is_secret": false, "is_verified": false, - "line_number": 969, - "type": "Hex High Entropy String", + "line_number": 1796, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "93658223830f17449a3b81a759279f8db378716a", + "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", "is_secret": false, "is_verified": false, - "line_number": 980, - "type": "Hex High Entropy String", + "line_number": 1809, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "mcpgateway/services/oauth_manager.py": [ { - "hashed_secret": "cd5a59f8105c99afb30595e52a11d73e855f2c9f", + "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", "is_secret": false, "is_verified": false, - "line_number": 993, - "type": "Hex High Entropy String", + "line_number": 105, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "f4fd806c07aaf73c1529b88212a826fadcae43ae", + "hashed_secret": "819ef87051ee2837fefbb462d846b8d282d3b756", "is_secret": false, "is_verified": false, - "line_number": 1004, - "type": "Hex High Entropy String", + "line_number": 108, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "mcpgateway/services/observability_service.py": [ { - "hashed_secret": "dafd46fad0421f3f00574f5c736ec0847f53d212", + "hashed_secret": "0a24796d4c71ce722a92f450f69dc36c60b21de4", "is_secret": false, "is_verified": false, - "line_number": 1016, + "line_number": 165, "type": "Hex High Entropy String", "verified_result": null }, { - "hashed_secret": "8f48b5146d88920187ca1c8d1d52cd0b008f12de", + "hashed_secret": "8750a512f22c55598175aee8790ae2470ec88d16", "is_secret": false, "is_verified": false, - "line_number": 1025, + "line_number": 165, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/services/resource_service.py": [ { - "hashed_secret": "083e28bdae3cd5b3af135e50a5758bd732e076c4", + "hashed_secret": "a10b98d7340036e9c8c301704f623eddd733cc1a", "is_secret": false, "is_verified": false, - "line_number": 1031, + "line_number": 316, "type": "Hex High Entropy String", "verified_result": null }, { - "hashed_secret": "1eeb415c50a8d2b04a705ebf09786adf00d8cf59", + "hashed_secret": "718cbcc5a4207c0d5f38e3a309bdba17cb0074b7", "is_secret": false, "is_verified": false, - "line_number": 1041, + "line_number": 3290, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/services/sso_service.py": [ { - "hashed_secret": "14b703e941e97ca4115fbfc7afc9d19920aa9c6c", + "hashed_secret": "920a25ef686c4f7ca6ad23dd109d3ad653161832", "is_secret": false, "is_verified": false, - "line_number": 1047, - "type": "Hex High Entropy String", + "line_number": 780, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "mcpgateway/services/support_bundle_service.py": [ { - "hashed_secret": "f711a3e8a220ceb3c9b360745772e4ed8659f46c", + "hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8", "is_secret": false, "is_verified": false, - "line_number": 1053, - "type": "Hex High Entropy String", + "line_number": 157, + "type": "Basic Auth Credentials", "verified_result": null - }, + } + ], + "mcpgateway/sri_hashes.json": [ { - "hashed_secret": "ca0b633ac167cec25b04208a0e3caeec68ad6c15", + "hashed_secret": "1fd5e6bd76663b16aee9602fa54dff982f0c0513", "is_secret": false, "is_verified": false, - "line_number": 1063, - "type": "Hex High Entropy String", + "line_number": 2, + "type": "Base64 High Entropy String", "verified_result": null }, { - "hashed_secret": "fc643b8317813d4055f095a72f90dfc83ac98a52", + "hashed_secret": "9412580d859a192c1915cdc58de2e145d249ef6a", "is_secret": false, "is_verified": false, - "line_number": 1076, - "type": "Hex High Entropy String", + "line_number": 3, + "type": "Base64 High Entropy String", "verified_result": null }, { - "hashed_secret": "fed671d6a24626d6cf16f91b98f2b11ba736d095", + "hashed_secret": "d8797430a133d1bc558e4ef4ba7110b28b67abfc", "is_secret": false, "is_verified": false, - "line_number": 1082, - "type": "Hex High Entropy String", + "line_number": 4, + "type": "Base64 High Entropy String", "verified_result": null }, { - "hashed_secret": "33eb20252a41cba23b834a062ead51beccf29376", + "hashed_secret": "6af61c57e8d8bcb6af6de09cfa7fb75c67785a48", "is_secret": false, "is_verified": false, - "line_number": 1093, - "type": "Hex High Entropy String", + "line_number": 5, + "type": "Base64 High Entropy String", "verified_result": null }, { - "hashed_secret": "f57b63e67ed485c5ffe3dec0888657ac4c281ca5", + "hashed_secret": "d58b463fcf8838bc9fec1cd217fc96dd6b2c02e8", "is_secret": false, "is_verified": false, - "line_number": 1099, - "type": "Hex High Entropy String", + "line_number": 6, + "type": "Base64 High Entropy String", "verified_result": null }, { - "hashed_secret": "824096ad190248d40cef63862048a64d18e73093", + "hashed_secret": "b5fc888b800befb7f0afd247b85cf1e00a1c617d", "is_secret": false, "is_verified": false, - "line_number": 1108, - "type": "Hex High Entropy String", + "line_number": 7, + "type": "Base64 High Entropy String", "verified_result": null }, { - "hashed_secret": "67eb96441e8f50509300c66e81b167f6e3f0f21d", + "hashed_secret": "a322631affd826e32a9fe4c1edd155526253a368", "is_secret": false, "is_verified": false, - "line_number": 1119, - "type": "Hex High Entropy String", + "line_number": 8, + "type": "Base64 High Entropy String", "verified_result": null }, { - "hashed_secret": "8e11a5e472d1d4d98e2bdc00b90448cca073dd97", + "hashed_secret": "5936242a1ad0d29897b3d7d8ca1075298653775c", "is_secret": false, "is_verified": false, - "line_number": 1128, - "type": "Hex High Entropy String", + "line_number": 9, + "type": "Base64 High Entropy String", "verified_result": null }, { - "hashed_secret": "a5ea7e9c1ce564f30e9551f8200f29d90aa6ec5e", + "hashed_secret": "f7d3016ba11801d588c442e1a09f00897ff182e3", "is_secret": false, "is_verified": false, - "line_number": 1144, - "type": "Hex High Entropy String", + "line_number": 10, + "type": "Base64 High Entropy String", "verified_result": null }, { - "hashed_secret": "e5720b6d98058e32a6194a43416caa780707a3de", + "hashed_secret": "57004d905b477a36f855d6a639c52b11c8385d1d", "is_secret": false, "is_verified": false, - "line_number": 1155, - "type": "Hex High Entropy String", + "line_number": 11, + "type": "Base64 High Entropy String", "verified_result": null }, { - "hashed_secret": "5a9e3d91c47e5f9099917f07fd7130195338b4ca", + "hashed_secret": "d7abf8d576f5ea125ba6fd308f28d8654f523a8c", "is_secret": false, "is_verified": false, - "line_number": 1166, - "type": "Hex High Entropy String", + "line_number": 12, + "type": "Base64 High Entropy String", "verified_result": null }, { - "hashed_secret": "79a0a8be10240450c72322f433eb92948903500f", + "hashed_secret": "0c82d92406ec9e3e50eedb724fca9a07b131f966", "is_secret": false, "is_verified": false, - "line_number": 1180, - "type": "Hex High Entropy String", + "line_number": 13, + "type": "Base64 High Entropy String", "verified_result": null }, { - "hashed_secret": "2bb2fe1e57f60f9972d8dc8aafecc14ea140b262", + "hashed_secret": "724ed978fa8c0af12c9e23a521acda5f9c22fd74", "is_secret": false, "is_verified": false, - "line_number": 1196, - "type": "Hex High Entropy String", + "line_number": 14, + "type": "Base64 High Entropy String", "verified_result": null }, { - "hashed_secret": "d93f2cd8895569df797eb6a57011aa1489604dee", + "hashed_secret": "f8852355a4d22965e4c3640c9c3c6e4039a83fc2", "is_secret": false, "is_verified": false, - "line_number": 1202, - "type": "Hex High Entropy String", + "line_number": 15, + "type": "Base64 High Entropy String", "verified_result": null }, { - "hashed_secret": "3f85aa33de16e014a62133be00e2fae3fee16a15", + "hashed_secret": "e40ac0dfd74f63f996a157f0c79c9ee0b9fbc951", "is_secret": false, "is_verified": false, - "line_number": 1208, - "type": "Hex High Entropy String", + "line_number": 16, + "type": "Base64 High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/templates/admin.html": [ { - "hashed_secret": "319d3f52ed81b3c7c6a69e0ddb313dba40f94795", + "hashed_secret": "b4e44716dbbf57be3dae2f819d96795a85d06652", "is_secret": false, "is_verified": false, - "line_number": 1220, - "type": "Hex High Entropy String", + "line_number": 12461, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "mcpgateway/templates/change-password-required.html": [ { - "hashed_secret": "99bce2688418d3adf96547726401effde2101407", + "hashed_secret": "6947818ac409551f11fbaa78f0ea6391960aa5b8", "is_secret": false, "is_verified": false, - "line_number": 1231, - "type": "Hex High Entropy String", + "line_number": 582, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "mcpgateway/templates/mcp_registry_partial.html": [ { - "hashed_secret": "611ee95376df586616103d908ee6f0e50de5ce6f", + "hashed_secret": "d4c3d66fd0c38547a3c7a4c6bdc29c36911bc030", "is_secret": false, "is_verified": false, - "line_number": 1241, - "type": "Hex High Entropy String", + "line_number": 667, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "mcpgateway/toolops/toolops_altk_service.py": [ { - "hashed_secret": "ada69b50f3a7e11d7d7e84d61c7cd446583bc38b", + "hashed_secret": "772fabfec4c6f068c046e516ab0b946909697d32", "is_secret": false, "is_verified": false, - "line_number": 1252, + "line_number": 283, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/toolops/utils/db_util.py": [ { - "hashed_secret": "2cec7afe8f0f8f21d28f274299142cbbe949023c", + "hashed_secret": "e2eea31cf61d791c922b110554ce1f0a6ed046e9", "is_secret": false, "is_verified": false, - "line_number": 1270, + "line_number": 178, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "mcpgateway/toolops/utils/llm_util.py": [ { - "hashed_secret": "370f2dab232e01715bb1463fe9a85a8a0e3b41ed", + "hashed_secret": "e6a51c968d41fd1d3437e6b7d9101e527ee1b83f", "is_secret": false, "is_verified": false, - "line_number": 1276, - "type": "Hex High Entropy String", + "line_number": 67, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "04a1a3194c674cd1244ae336e0fe7054fb9bc64f", + "hashed_secret": "9c4006403b5388cd34ccfe3ed1b1d3e53cf35700", "is_secret": false, "is_verified": false, - "line_number": 1282, - "type": "Hex High Entropy String", + "line_number": 81, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "788b34ba07c35e639f6e3e82f608189791e3239f", + "hashed_secret": "d01e71e4c47a4022acd25c74bffedd2641a60c70", "is_secret": false, "is_verified": false, - "line_number": 1293, - "type": "Hex High Entropy String", + "line_number": 96, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "d1837f7e31117d99b0cbffd82e7eabcea851a94c", + "hashed_secret": "39fc8e4accd6a3563c4be159e507195ad3d7274c", "is_secret": false, "is_verified": false, - "line_number": 1299, - "type": "Hex High Entropy String", + "line_number": 106, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "mcpgateway/tools/builder/common.py": [ { - "hashed_secret": "040278ead73fc50f601174bf24cfe3552915a67e", + "hashed_secret": "2a1027e8abaef8567159c09ddca81604fb1ba8c0", "is_secret": false, "is_verified": false, - "line_number": 1305, - "type": "Hex High Entropy String", + "line_number": 428, + "type": "Basic Auth Credentials", "verified_result": null - }, + } + ], + "mcpgateway/tools/builder/templates/compose/docker-compose.yaml.j2": [ { - "hashed_secret": "b85b391225b20c3137322bcc23c7e2c31320a83e", + "hashed_secret": "bd0160c2cf35d950843c88f3be2b9412ed71f485", "is_secret": false, "is_verified": false, - "line_number": 1314, - "type": "Hex High Entropy String", + "line_number": 44, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "604edf3bd2202bebf89e23895a6ef68088f57e42", + "hashed_secret": "4d4acd9b084d13f5fdb23807d857e1c48a1cfd0f", "is_secret": false, "is_verified": false, - "line_number": 1323, - "type": "Hex High Entropy String", + "line_number": 164, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "mcpgateway/translate_header_utils.py": [ { - "hashed_secret": "7167df607c246fe7c20de1a7b7abc394c7026cd0", + "hashed_secret": "e48c47f1431afd3bb030deea266b4309e2228c5b", "is_secret": false, "is_verified": false, - "line_number": 1336, - "type": "Hex High Entropy String", + "line_number": 190, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "f0f817643483baa005cf31ad44d972f65508675e", + "hashed_secret": "a62f2225bf70bfaccbc7f1ef2a397836717377de", "is_secret": false, "is_verified": false, - "line_number": 1346, - "type": "Hex High Entropy String", + "line_number": 262, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "64c9975a7ab1b5bcd2439c334f2370a65d4aae82", + "hashed_secret": "ee977806d7286510da8b9a7492ba58e2484c0ecc", "is_secret": false, "is_verified": false, - "line_number": 1359, - "type": "Hex High Entropy String", + "line_number": 317, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "mcpgateway/utils/create_jwt_token.py": [ { - "hashed_secret": "d3858567b9054a3163ab6dfd94a79713a72f7cec", + "hashed_secret": "2fcd9b91b955db1627d18720cb7395ef2893a497", "is_secret": false, "is_verified": false, - "line_number": 1368, - "type": "Hex High Entropy String", + "line_number": 36, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "mcpgateway/utils/generate_keys.py": [ { - "hashed_secret": "042d13670968f7c52be4bc75a67035bdc4128e45", + "hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9", "is_secret": false, "is_verified": false, - "line_number": 1378, - "type": "Hex High Entropy String", + "line_number": 89, + "type": "Private Key", "verified_result": null - }, + } + ], + "mcpgateway/utils/services_auth.py": [ { - "hashed_secret": "35f86be9fc56fed00caa9d04d07259deee6e5713", + "hashed_secret": "a2fe5a0cdcbf2e84dbf868a11c5f2ca79f2d7b8d", "is_secret": false, "is_verified": false, - "line_number": 1390, - "type": "Hex High Entropy String", + "line_number": 218, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "mcpgateway/utils/sso_bootstrap.py": [ { - "hashed_secret": "4388f4a1071af57b8f44733a4b590114701c8263", + "hashed_secret": "920a25ef686c4f7ca6ad23dd109d3ad653161832", "is_secret": false, "is_verified": false, - "line_number": 1402, - "type": "Hex High Entropy String", + "line_number": 43, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "c635956e53a2624afd87d3ac8a41701e377f00e7", + "hashed_secret": "b44bf05d644c15c4d84f78771de011e3cce924c0", "is_secret": false, "is_verified": false, - "line_number": 1415, - "type": "Hex High Entropy String", + "line_number": 60, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "5747d8af9684661644b7df2fe00e51dc16f6843d", + "hashed_secret": "999a3419d9959d3c39b11dcc67d79c7888b4b765", "is_secret": false, "is_verified": false, - "line_number": 1426, - "type": "Hex High Entropy String", + "line_number": 72, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "a5f5903bd227b9d811f037da91086231b40422a7", + "hashed_secret": "bacac952b5cb942687d38a9eda6531d570f88b22", "is_secret": false, "is_verified": false, - "line_number": 1437, - "type": "Hex High Entropy String", + "line_number": 85, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "34cfa36d78842de5ac91d80bd7a8996d903abee8", + "hashed_secret": "cd1ecfcd67c8b85800a483d77e550d36727e8925", "is_secret": false, "is_verified": false, - "line_number": 1443, - "type": "Hex High Entropy String", + "line_number": 99, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "mcpgateway/utils/url_auth.py": [ { - "hashed_secret": "6be5b6b2b339ebb0359e498fb3752087d91f5ea2", + "hashed_secret": "6367c48dd193d56ea7b0baad25b19455e529f5ee", "is_secret": false, "is_verified": false, - "line_number": 1452, - "type": "Hex High Entropy String", + "line_number": 68, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "f9de3f6e836fa1b0a50f4e0e06154a422c0ed705", + "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", "is_secret": false, "is_verified": false, - "line_number": 1461, - "type": "Hex High Entropy String", + "line_number": 130, + "type": "Basic Auth Credentials", "verified_result": null }, { - "hashed_secret": "0253824dccde843d47fa65b99bb29912e5ec3a0a", + "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", "is_secret": false, "is_verified": false, - "line_number": 1470, - "type": "Hex High Entropy String", + "line_number": 136, + "type": "Basic Auth Credentials", "verified_result": null }, { - "hashed_secret": "d8a8d970a479e89ac4678dabd4216dfa8b22e040", + "hashed_secret": "b02dff0ec9d24823e77c27c281a852247896b86d", "is_secret": false, "is_verified": false, - "line_number": 1479, - "type": "Hex High Entropy String", + "line_number": 138, + "type": "Basic Auth Credentials", "verified_result": null }, { - "hashed_secret": "d4e3e4ca451b86ca4ea86a0b2db6ec3ff7c01c22", + "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", "is_secret": false, "is_verified": false, - "line_number": 1490, - "type": "Hex High Entropy String", + "line_number": 203, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "mcpgateway/utils/verify_credentials.py": [ { - "hashed_secret": "84469e279d411898bc4109bd5c351e99cec1a050", + "hashed_secret": "a4b48a81cdab1e1a5dd37907d6c85ca1c61ddc7c", "is_secret": false, "is_verified": false, - "line_number": 1506, - "type": "Hex High Entropy String", + "line_number": 564, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "mcpgateway/version.py": [ { - "hashed_secret": "8f9c265c3866a77dcb45fd46fa0b83dc78acf82d", + "hashed_secret": "9addbf544119efa4a64223b649750a510f0d463f", "is_secret": false, "is_verified": false, - "line_number": 1521, - "type": "Hex High Entropy String", + "line_number": 518, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "4eee70986dc22664f0597495a6d0dd22fc5422c4", + "hashed_secret": "9addbf544119efa4a64223b649750a510f0d463f", "is_secret": false, "is_verified": false, - "line_number": 1540, - "type": "Hex High Entropy String", + "line_number": 580, + "type": "Basic Auth Credentials", "verified_result": null - }, + } + ], + "plugin_templates/external/{{cookiecutter.plugin_slug}}/.env.template": [ { - "hashed_secret": "3b98f886da0f14b493bd4e915f301e538d33776e", + "hashed_secret": "623e76c36aa2a886542011e28412cc761d7ceb01", "is_secret": false, "is_verified": false, - "line_number": 1558, - "type": "Hex High Entropy String", + "line_number": 118, + "type": "Secret Keyword", + "verified_result": null + } + ], + "plugins/content_moderation/README.md": [ + { + "hashed_secret": "c586890f1a2e6a43c2611ec983782d53141333de", + "is_secret": false, + "is_verified": false, + "line_number": 168, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "958891bf3ce63f0001e6975b282f6d26461ca4bf", + "hashed_secret": "1d3b1295c282649864a65fe3bd1631bcf7499ef5", "is_secret": false, "is_verified": false, - "line_number": 1567, - "type": "Hex High Entropy String", + "line_number": 174, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "94014c04a964970c30d309870f97ea5d8ea5bfd8", + "hashed_secret": "83542070ee8238c5ad6484b7824016bd2d2fb32e", "is_secret": false, "is_verified": false, - "line_number": 1578, - "type": "Hex High Entropy String", + "line_number": 186, + "type": "Secret Keyword", "verified_result": null } ], - "mcp-servers/rust/filesystem-server/Cargo.lock": [ + "plugins/content_moderation/TESTING.md": [ { - "hashed_secret": "e0f930ce4dc6ee91bd9c13f93bdd411a875847ad", + "hashed_secret": "11fa7c37d697f30e6aee828b4426a10f83ab2380", "is_secret": false, "is_verified": false, - "line_number": 9, - "type": "Hex High Entropy String", + "line_number": 204, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "0c2b06b30faf95e40c75aaaefa957d835c1df392", + "hashed_secret": "1d3b1295c282649864a65fe3bd1631bcf7499ef5", "is_secret": false, "is_verified": false, - "line_number": 18, - "type": "Hex High Entropy String", + "line_number": 240, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "950ea3a71e9f5d30d3723b3f7595d1524bd0de20", + "hashed_secret": "3bee216ebc256d692260fc3adc765050508fef5e", "is_secret": false, "is_verified": false, - "line_number": 27, - "type": "Hex High Entropy String", + "line_number": 428, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "plugins/encoded_exfil_detection/README.md": [ { - "hashed_secret": "a0b32b6f12e419acbd7b54d25cfcb35ea7d88b39", - "is_secret": false, + "hashed_secret": "f7f877c24a4ebef314009a2e79314797bae91bb6", "is_verified": false, - "line_number": 42, - "type": "Hex High Entropy String", - "verified_result": null + "line_number": 149, + "type": "Secret Keyword", + "verified_result": null, + "is_secret": false }, { - "hashed_secret": "4d8ca7a1c773e7850208f4b7e18b740a9d526e28", - "is_secret": false, + "hashed_secret": "5f2fe4f03b2314fa3a217f1e1f0ab2e3f5b1b49f", "is_verified": false, - "line_number": 48, - "type": "Hex High Entropy String", - "verified_result": null - }, + "line_number": 178, + "type": "Secret Keyword", + "verified_result": null, + "is_secret": false + } + ], + "plugins/examples/custom_auth_example/README.md": [ { - "hashed_secret": "e4d87006218e91f030a57eb9b12decf51068e90b", + "hashed_secret": "423529858c0ba74fbbe141bce50b20f3c0d3b9f6", "is_secret": false, "is_verified": false, - "line_number": 57, - "type": "Hex High Entropy String", + "line_number": 70, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "f505b7d95ee94121d5d84074bb61cbfc8115242c", + "hashed_secret": "31b38ab9a06eec4e1a194a5e23c83fbc6643c20a", "is_secret": false, "is_verified": false, - "line_number": 66, - "type": "Hex High Entropy String", + "line_number": 104, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "053152e6d6ec523a570bc615219d6c829a0d455b", + "hashed_secret": "e94104ce07e03bb7892c943c21235d8f91793c73", "is_secret": false, "is_verified": false, - "line_number": 77, - "type": "Hex High Entropy String", + "line_number": 196, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "94da59c445b7bfedb03dfc1ee4ade601cee2c067", + "hashed_secret": "2bbebb59c0430ebfe54c81f73e3bb99c9e704341", "is_secret": false, "is_verified": false, - "line_number": 83, - "type": "Hex High Entropy String", + "line_number": 295, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "07781af8811774ce3bf3152bbfa1c98d7ea03c61", + "hashed_secret": "ed65c049bb2f78ee4f703b2158ba9cc6ea31fb7e", "is_secret": false, "is_verified": false, - "line_number": 94, - "type": "Hex High Entropy String", + "line_number": 295, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "plugins/examples/custom_auth_example/custom_auth.py": [ { - "hashed_secret": "ece49906f7a72ed3cfb012e5e2e1209e1b226172", + "hashed_secret": "79acc7201378a3d075db4c3b716187b65d4369ba", "is_secret": false, "is_verified": false, - "line_number": 100, - "type": "Hex High Entropy String", + "line_number": 260, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "plugins/examples/simple_token_auth/simple_token_auth.py": [ { - "hashed_secret": "388b28008bbc41d55a4ab30e40375d58ef630445", + "hashed_secret": "8a9e9e42f8e154a255c1455267654ab4ec13528b", "is_secret": false, "is_verified": false, - "line_number": 106, - "type": "Hex High Entropy String", + "line_number": 157, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "181759bd3798be44125fc8c50e62ba054d318783", + "hashed_secret": "3d75cbdb604907851e109e4ced7161c178648532", "is_secret": false, "is_verified": false, - "line_number": 139, - "type": "Hex High Entropy String", + "line_number": 166, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "plugins/external/cedar/resources/runtime/config.yaml": [ { - "hashed_secret": "aa442844f85bcb213caecfd88e01d69e5e0c4d07", + "hashed_secret": "ddb2c93ebcf6098f6ccb08807ed66e02934248a5", "is_secret": false, "is_verified": false, - "line_number": 158, - "type": "Hex High Entropy String", + "line_number": 8, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "plugins/external/llmguard/README.md": [ { - "hashed_secret": "b25a21f1a0e2fc65dc90424e07d7210587e8b296", + "hashed_secret": "50c8af90ac5a0b327517a411650008199a358c96", "is_secret": false, "is_verified": false, - "line_number": 187, + "line_number": 308, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "plugins/external/llmguard/resources/plugins/config.yaml": [ { - "hashed_secret": "457b24d26a9cd360a12b56f87c6f1daf3d30a895", + "hashed_secret": "50c8af90ac5a0b327517a411650008199a358c96", "is_secret": false, "is_verified": false, - "line_number": 193, + "line_number": 14, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "plugins/external/llmguard/resources/runtime/config.yaml": [ { - "hashed_secret": "79ddcb7f40bc791b947777312e940ffe52c9d9f2", + "hashed_secret": "ddb2c93ebcf6098f6ccb08807ed66e02934248a5", "is_secret": false, "is_verified": false, - "line_number": 199, - "type": "Hex High Entropy String", + "line_number": 8, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "plugins/external/opa/README.md": [ { - "hashed_secret": "b4808fb54c15b4bfe5268dd5ef3669d35302dac8", + "hashed_secret": "a5d80863d8ca51c72e6cb7799ac9657eaddb3b1d", "is_secret": false, "is_verified": false, - "line_number": 209, - "type": "Hex High Entropy String", + "line_number": 235, + "type": "Base64 High Entropy String", "verified_result": null - }, + } + ], + "plugins/external/opa/resources/runtime/config.yaml": [ { - "hashed_secret": "20af2a76b19cc7a1a93269f1e5f876f58bd266cc", + "hashed_secret": "ddb2c93ebcf6098f6ccb08807ed66e02934248a5", "is_secret": false, "is_verified": false, - "line_number": 215, - "type": "Hex High Entropy String", + "line_number": 8, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "plugins/pii_filter/README.md": [ { - "hashed_secret": "37b95db3d7bca7d5da0f62332b6968e1454c85bb", + "hashed_secret": "25910f981e85ca04baf359199dd0bd4a3ae738b6", "is_secret": false, "is_verified": false, - "line_number": 221, - "type": "Hex High Entropy String", + "line_number": 344, + "type": "AWS Access Key", "verified_result": null - }, + } + ], + "plugins/resource_filter/README.md": [ { - "hashed_secret": "a5f1914a4edf604fca63fe9c60cb62601b7c701c", + "hashed_secret": "048fe8f760ed90d8058cacc2da41ed7a7eb76669", "is_secret": false, "is_verified": false, - "line_number": 227, - "type": "Hex High Entropy String", + "line_number": 153, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "eac7344fb138e2bc70ec74dd15a52fe0c253887a", + "hashed_secret": "48574e7efc2af452e5187ab0f9381560f17577be", "is_secret": false, "is_verified": false, - "line_number": 237, - "type": "Hex High Entropy String", + "line_number": 156, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "plugins/vault/README.md": [ { - "hashed_secret": "a01b71d602bc5018e96db1a555a02018f474f151", + "hashed_secret": "b60d121b438a380c343d5ec3c2037564b82ffef3", "is_secret": false, "is_verified": false, - "line_number": 243, - "type": "Hex High Entropy String", + "line_number": 181, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "ba9e3cd70200d1008c1cd6e43fc0f2265452d3d6", + "hashed_secret": "83722c5a3bcef30aa1c6014bedd0ddd5b4107514", "is_secret": false, "is_verified": false, - "line_number": 257, - "type": "Hex High Entropy String", + "line_number": 194, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "plugins/webhook_notification/TESTING.md": [ { - "hashed_secret": "a79fc0f9c28c100ce8b3f3a1fe5cbaee6864eed6", + "hashed_secret": "3acfb2c2b433c0ea7ff107e33df91b18e52f960f", "is_secret": false, "is_verified": false, - "line_number": 267, - "type": "Hex High Entropy String", + "line_number": 61, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "plugins_rust/encoded_exfil_detection/benches/encoded_exfil_detection.rs": [ { - "hashed_secret": "bc197f5ee9458fce96164b6cd1c28520136bac52", + "hashed_secret": "9e53fffcf6d106f85fff53416e2ae687e1346aea", "is_secret": false, "is_verified": false, - "line_number": 279, + "line_number": 27, "type": "Hex High Entropy String", "verified_result": null }, { - "hashed_secret": "710066c385eadd0091f067e0a8e2573ac53d5f4b", + "hashed_secret": "67844048772af8b97258a5f1fe00b0043216f739", "is_secret": false, "is_verified": false, - "line_number": 291, - "type": "Hex High Entropy String", + "line_number": 49, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "20ba5750c60eb4d6c0385d1c99ceddd591d40cb1", + "hashed_secret": "648b3cbf65960d678821b2ae53740d890e1be100", "is_secret": false, "is_verified": false, - "line_number": 297, - "type": "Hex High Entropy String", + "line_number": 128, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "plugins_rust/encoded_exfil_detection/src/lib.rs": [ { - "hashed_secret": "256ba2edaff5c73ef22a272ac76fb83fa73b86ba", + "hashed_secret": "1d278d3c888d1a2fa7eed622bfc02927ce4049af", "is_secret": false, "is_verified": false, - "line_number": 303, - "type": "Hex High Entropy String", + "line_number": 573, + "type": "Base64 High Entropy String", "verified_result": null }, { - "hashed_secret": "8f5fced06242dc5e430978c695ce15b86635597a", + "hashed_secret": "d7da4707f9793b357da965eb04f06ceae7d7bfa6", "is_secret": false, "is_verified": false, - "line_number": 313, - "type": "Hex High Entropy String", + "line_number": 574, + "type": "Base64 High Entropy String", "verified_result": null }, { - "hashed_secret": "aea69a0db4b6fb9c1801ecd9266c98e51a5b6515", + "hashed_secret": "0962e90797cdf7ff77c8a0a9477a7ca4f493465f", "is_secret": false, "is_verified": false, - "line_number": 323, - "type": "Hex High Entropy String", + "line_number": 892, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "68a15e87a6b9806b9214862840726ef1acc2e5dd", + "hashed_secret": "ca3f3092640b07c051f03e52690fda1bcee3f60d", "is_secret": false, "is_verified": false, - "line_number": 329, - "type": "Hex High Entropy String", + "line_number": 913, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "37296779892fa772bc497cc53fa15fb2ced95b84", - "is_secret": false, + "hashed_secret": "532fdeccb155dce5b528ba039b9a5d201b817e3b", "is_verified": false, - "line_number": 339, - "type": "Hex High Entropy String", - "verified_result": null + "line_number": 932, + "type": "Secret Keyword", + "verified_result": null, + "is_secret": false }, { - "hashed_secret": "076f7448f7767df7a0af32c25c88174167b2b870", - "is_secret": false, + "hashed_secret": "cf743b3a58a4d0f91c1d7f5825c0b1b5f7758174", "is_verified": false, - "line_number": 348, - "type": "Hex High Entropy String", - "verified_result": null + "line_number": 969, + "type": "Base64 High Entropy String", + "verified_result": null, + "is_secret": false }, { - "hashed_secret": "9cdec82ff07c4062846684c9ba65e4dc199923d2", + "hashed_secret": "ff1a7ebc241b8eefe9e75e13f20cc22c365ab626", + "is_verified": false, + "line_number": 969, + "type": "Secret Keyword", + "verified_result": null, + "is_secret": false + } + ], + "plugins_rust/secrets_detection/benches/secrets_detection.rs": [ + { + "hashed_secret": "86de8c52637ec530fe39b0a8471da9b8764d5242", "is_secret": false, "is_verified": false, - "line_number": 354, - "type": "Hex High Entropy String", + "line_number": 53, + "type": "AWS Access Key", "verified_result": null }, { - "hashed_secret": "9264404b12f829d6dfbfdac1825b97e54fcbb535", + "hashed_secret": "5b8c02feb3811310ff452e9a812b9f98873f36bf", "is_secret": false, "is_verified": false, - "line_number": 364, - "type": "Hex High Entropy String", + "line_number": 69, + "type": "SoftLayer Credentials", "verified_result": null }, { - "hashed_secret": "df7d483ec7a8e3b39542662290f8d52a71439b7a", + "hashed_secret": "0fa6996ddd42e0f9db3f1c04d06453026d56da0f", "is_secret": false, "is_verified": false, - "line_number": 377, - "type": "Hex High Entropy String", + "line_number": 73, + "type": "SoftLayer Credentials", "verified_result": null }, { - "hashed_secret": "11b0d546f276abc72c2c2b0afa721f73b9f02097", + "hashed_secret": "de56e523c4cf688e56a7df48cf01e2a21db5b5a4", "is_secret": false, "is_verified": false, - "line_number": 388, - "type": "Hex High Entropy String", + "line_number": 77, + "type": "AWS Access Key", "verified_result": null - }, + } + ], + "plugins_rust/secrets_detection/compare_performance.py": [ { - "hashed_secret": "64a88e08b9fc9184294e66cf5676b86ba6251482", + "hashed_secret": "86de8c52637ec530fe39b0a8471da9b8764d5242", "is_secret": false, "is_verified": false, - "line_number": 397, - "type": "Hex High Entropy String", + "line_number": 67, + "type": "AWS Access Key", "verified_result": null }, { - "hashed_secret": "8b80e3dbf214e8b5f42deaf56c671624fb8916a9", + "hashed_secret": "5b8c02feb3811310ff452e9a812b9f98873f36bf", "is_secret": false, "is_verified": false, - "line_number": 403, - "type": "Hex High Entropy String", + "line_number": 71, + "type": "SoftLayer Credentials", "verified_result": null }, { - "hashed_secret": "1652117d4ce1279d3c4dc2c3dd53cfea2d615645", + "hashed_secret": "0fa6996ddd42e0f9db3f1c04d06453026d56da0f", "is_secret": false, "is_verified": false, - "line_number": 414, - "type": "Hex High Entropy String", + "line_number": 72, + "type": "SoftLayer Credentials", "verified_result": null - }, + } + ], + "plugins_rust/secrets_detection/examples/heavy_workload.rs": [ { - "hashed_secret": "7b74a642aa1e82a1f46a117ed71c022366e398e9", + "hashed_secret": "86de8c52637ec530fe39b0a8471da9b8764d5242", "is_secret": false, "is_verified": false, - "line_number": 420, - "type": "Hex High Entropy String", + "line_number": 34, + "type": "AWS Access Key", "verified_result": null }, { - "hashed_secret": "e3423649a06049806d8883193ffa13f6488898ba", + "hashed_secret": "5b8c02feb3811310ff452e9a812b9f98873f36bf", "is_secret": false, "is_verified": false, - "line_number": 429, - "type": "Hex High Entropy String", + "line_number": 38, + "type": "SoftLayer Credentials", "verified_result": null }, { - "hashed_secret": "2e5f4bd84fb086fa302ed7423a10d19721492f95", + "hashed_secret": "0fa6996ddd42e0f9db3f1c04d06453026d56da0f", "is_secret": false, "is_verified": false, - "line_number": 435, - "type": "Hex High Entropy String", + "line_number": 39, + "type": "SoftLayer Credentials", "verified_result": null }, { - "hashed_secret": "39e1dc7e8bd1f918717fde66f30bc3884780a612", + "hashed_secret": "de56e523c4cf688e56a7df48cf01e2a21db5b5a4", "is_secret": false, "is_verified": false, - "line_number": 446, - "type": "Hex High Entropy String", + "line_number": 40, + "type": "AWS Access Key", "verified_result": null }, { - "hashed_secret": "1139b24a74bbaa575b3e47243e1c0c0b2a892073", + "hashed_secret": "be4fc4886bd949b369d5e092eb87494f12e57e5b", "is_secret": false, "is_verified": false, - "line_number": 456, - "type": "Hex High Entropy String", + "line_number": 42, + "type": "Private Key", "verified_result": null - }, + } + ], + "plugins_rust/secrets_detection/src/patterns.rs": [ { - "hashed_secret": "5b3e7af3505b6a244c716a014043c6a367b2339f", + "hashed_secret": "86de8c52637ec530fe39b0a8471da9b8764d5242", "is_secret": false, "is_verified": false, - "line_number": 474, - "type": "Hex High Entropy String", + "line_number": 94, + "type": "AWS Access Key", "verified_result": null }, { - "hashed_secret": "2e2d13eb72c2d2c46e0c94dd2259343b0933dda9", + "hashed_secret": "de56e523c4cf688e56a7df48cf01e2a21db5b5a4", "is_secret": false, "is_verified": false, - "line_number": 485, - "type": "Hex High Entropy String", + "line_number": 98, + "type": "AWS Access Key", "verified_result": null }, { - "hashed_secret": "4d2c6fb73af42ea62f965535a82e83df5411d419", + "hashed_secret": "a5f223264a7296f00627a4c5c00a9957c67f5479", "is_secret": false, "is_verified": false, - "line_number": 521, - "type": "Hex High Entropy String", + "line_number": 238, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "fc40fb75e9edfd1c082708dbcc1384d9e68f90f7", + "hashed_secret": "be4fc4886bd949b369d5e092eb87494f12e57e5b", "is_secret": false, "is_verified": false, - "line_number": 527, - "type": "Hex High Entropy String", + "line_number": 282, + "type": "Private Key", "verified_result": null }, { - "hashed_secret": "a05ef788debbaeb03dbdc3e5db4ee44747b18eb7", + "hashed_secret": "daefe0b4345a654580dcad25c7c11ff4c944a8c0", "is_secret": false, "is_verified": false, - "line_number": 533, - "type": "Hex High Entropy String", + "line_number": 286, + "type": "Private Key", "verified_result": null }, { - "hashed_secret": "1e57f3e0354398a25902ba8a10b12a84283243cc", + "hashed_secret": "f0778f3e140a61d5bbbed5430773e52af2f5fba4", "is_secret": false, "is_verified": false, - "line_number": 542, - "type": "Hex High Entropy String", + "line_number": 290, + "type": "Private Key", "verified_result": null }, { - "hashed_secret": "5dd5a3b90a4aec184d6390d782a283adf53b87f9", + "hashed_secret": "27c6929aef41ae2bcadac15ca6abcaff72cda9cd", "is_secret": false, "is_verified": false, - "line_number": 548, - "type": "Hex High Entropy String", + "line_number": 294, + "type": "Private Key", "verified_result": null }, { - "hashed_secret": "8d05fb42532fecd995f8eff2190f412088d6178f", + "hashed_secret": "75f3be87351a63f1c2f677632838f9e2be066891", "is_secret": false, "is_verified": false, - "line_number": 557, + "line_number": 336, "type": "Hex High Entropy String", "verified_result": null }, { - "hashed_secret": "89cf74f6e03a27de9d661324442e0b372ad9eefb", + "hashed_secret": "a19310f994b3a8974f03cddd1de4b862266947e8", "is_secret": false, "is_verified": false, - "line_number": 572, + "line_number": 340, "type": "Hex High Entropy String", "verified_result": null }, { - "hashed_secret": "e72195ef7791709c24e8be4349842896a88bab07", + "hashed_secret": "b1775a785f09a6ebaf2dc33d6eaeb98974d9cdb8", "is_secret": false, "is_verified": false, - "line_number": 582, + "line_number": 344, "type": "Hex High Entropy String", "verified_result": null }, { - "hashed_secret": "f2c63a981fbb2e2b39e4c05d8ec787e3befa3b07", + "hashed_secret": "7326d685c1328a4b748e7c239ba227c8a7afc988", "is_secret": false, "is_verified": false, - "line_number": 588, + "line_number": 350, "type": "Hex High Entropy String", "verified_result": null }, { - "hashed_secret": "f5cdeb5a0b086c5a674eb8ad81bf17fd63bd0a01", + "hashed_secret": "e0eca9acea92cdf77e594f0d3367116b9adffeec", "is_secret": false, "is_verified": false, - "line_number": 599, - "type": "Hex High Entropy String", + "line_number": 354, + "type": "Base64 High Entropy String", "verified_result": null }, { - "hashed_secret": "2ebad4ca4792566abd1ba0d665249302119cb457", + "hashed_secret": "e4d0f04f03d9183f843c9d764592ba5bcd17f356", "is_secret": false, "is_verified": false, - "line_number": 605, - "type": "Hex High Entropy String", + "line_number": 369, + "type": "Base64 High Entropy String", "verified_result": null }, { - "hashed_secret": "7f0ff8004a236b8bba117372cb42ca731446c4b6", + "hashed_secret": "b256e49b893f8b2eeb2384801910c70ed17a2191", "is_secret": false, "is_verified": false, - "line_number": 616, - "type": "Hex High Entropy String", + "line_number": 411, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "plugins_rust/secrets_detection/src/scanner.rs": [ { - "hashed_secret": "c4bf225e39c6d664754ca87cb8751f8a2a093134", + "hashed_secret": "50b68587c1c40248753c5e9190dcc3dd73aa142a", "is_secret": false, "is_verified": false, - "line_number": 622, - "type": "Hex High Entropy String", + "line_number": 243, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "8ee82c36fc0af17f1fdeb940d2bca6424bf2e453", + "hashed_secret": "4e8dd640c77be10809ea27a4a6e668d9cba82014", "is_secret": false, "is_verified": false, - "line_number": 628, - "type": "Hex High Entropy String", + "line_number": 248, + "type": "SoftLayer Credentials", "verified_result": null }, { - "hashed_secret": "fb98da5b813e3ae31d0e430526334ae9f010a546", + "hashed_secret": "721b09413c9b91bf3278f966b62c7a03ae2ef9a0", "is_secret": false, "is_verified": false, - "line_number": 645, - "type": "Hex High Entropy String", + "line_number": 248, + "type": "Base64 High Entropy String", "verified_result": null }, { - "hashed_secret": "a7bb22eb6421d498f26d952fe0635a1d04a27bcd", + "hashed_secret": "be4fc4886bd949b369d5e092eb87494f12e57e5b", "is_secret": false, "is_verified": false, - "line_number": 657, - "type": "Hex High Entropy String", + "line_number": 292, + "type": "Private Key", "verified_result": null }, { - "hashed_secret": "c6bfb6db84c8b6af05fdd750164b7d70f039b3b6", + "hashed_secret": "de56e523c4cf688e56a7df48cf01e2a21db5b5a4", "is_secret": false, "is_verified": false, - "line_number": 670, - "type": "Hex High Entropy String", + "line_number": 726, + "type": "AWS Access Key", "verified_result": null }, { - "hashed_secret": "399da540bd166b0c7bb3a2815bb400eb11e5638d", + "hashed_secret": "2e3810897e838a5edcfd57b5fb117927fdc922da", "is_secret": false, "is_verified": false, - "line_number": 683, - "type": "Hex High Entropy String", + "line_number": 740, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "f3d3c26ed24a648081ced9182bb1b19f7d78e68d", + "hashed_secret": "7f1d81c1d9cf850d6a1dbd32d5f110b6732fe60e", "is_secret": false, "is_verified": false, - "line_number": 692, - "type": "Hex High Entropy String", + "line_number": 740, + "type": "Base64 High Entropy String", "verified_result": null }, { - "hashed_secret": "1d591d1374352e720051c9d3f7b1e2c5557d5fd6", + "hashed_secret": "86de8c52637ec530fe39b0a8471da9b8764d5242", "is_secret": false, "is_verified": false, - "line_number": 698, - "type": "Hex High Entropy String", + "line_number": 799, + "type": "AWS Access Key", "verified_result": null - }, + } + ], + "podman-compose-sonarqube.yaml": [ { - "hashed_secret": "0fa72c3d33bbc7feb5618105d7b5997001614612", + "hashed_secret": "345e9ea7c857e75dedd9edb24c232e1cab297c19", "is_secret": false, "is_verified": false, - "line_number": 704, - "type": "Hex High Entropy String", + "line_number": 28, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "run-granian.sh": [ { - "hashed_secret": "c9fc785a64064e9f54662a39349af4f8ca9fc333", + "hashed_secret": "64f5490a2808803712ef99d9dfb8bc3ea5a15077", "is_secret": false, "is_verified": false, - "line_number": 714, - "type": "Hex High Entropy String", + "line_number": 291, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "ce3dad8a04c408de2c43ad7c3ed460e21897bc5d", + "hashed_secret": "cff0d14e4337fa8bdb68dfa906f04b0df6fad72f", "is_secret": false, "is_verified": false, - "line_number": 724, - "type": "Hex High Entropy String", + "line_number": 406, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "run-gunicorn.sh": [ { - "hashed_secret": "92f04de23131564ceddfbbd126b98292b0934577", + "hashed_secret": "64f5490a2808803712ef99d9dfb8bc3ea5a15077", "is_secret": false, "is_verified": false, - "line_number": 737, - "type": "Hex High Entropy String", + "line_number": 296, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "run.sh": [ { - "hashed_secret": "3976cf14dbb91cafcaaddad7d5b88cf44aba7b58", + "hashed_secret": "fa9beb99e4029ad5a6615399e7bbae21356086b3", "is_secret": false, "is_verified": false, - "line_number": 743, - "type": "Hex High Entropy String", + "line_number": 115, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "1f969023518451bb7f23d59ff7c96c0a6a259877", + "hashed_secret": "7b4455a56fbf1d198e45e04c437488514645a82c", "is_secret": false, "is_verified": false, - "line_number": 749, - "type": "Hex High Entropy String", + "line_number": 117, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "scripts/ci/run_upgrade_validation.sh": [ { - "hashed_secret": "64476db6085c2f677304cd3398408043caab5651", + "hashed_secret": "1d193899f802762003a56b1cbcfeb55f2b04d61b", "is_secret": false, "is_verified": false, - "line_number": 771, - "type": "Hex High Entropy String", + "line_number": 448, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "b46c956f080e29b6f05c5538b6f134e2417d839c", + "hashed_secret": "dcdf24a1d4440d5ebe319bf804f96c81e08350f9", "is_secret": false, "is_verified": false, - "line_number": 787, - "type": "Hex High Entropy String", + "line_number": 455, + "type": "Basic Auth Credentials", "verified_result": null - }, + } + ], + "scripts/demo_a2a_agent_auth.py": [ { - "hashed_secret": "ace4c15ef5d5b8c769f9d24cf4bf1de85fecb2c7", + "hashed_secret": "e415dfafffe0c1859396cee8659521eaeb789ced", "is_secret": false, "is_verified": false, - "line_number": 810, - "type": "Hex High Entropy String", + "line_number": 45, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "scripts/lib/common.sh": [ { - "hashed_secret": "9bfd048ce5ceb106ba035489cea34b77d79814e4", + "hashed_secret": "cff0d14e4337fa8bdb68dfa906f04b0df6fad72f", "is_secret": false, "is_verified": false, - "line_number": 825, - "type": "Hex High Entropy String", + "line_number": 176, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "scripts/test-sso-flow.sh": [ { - "hashed_secret": "86b403165d79a78a2c4a595fe14902c5f4f47361", + "hashed_secret": "174c2b8209d705f3db2cd2f6f84b79e6e27ca793", "is_secret": false, "is_verified": false, - "line_number": 834, - "type": "Hex High Entropy String", + "line_number": 82, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "6c94d59722bce3906d347ed600e37e119a7837df", + "hashed_secret": "d86a48c00ab62d2631c5cd5735471e7b5e34b7dc", "is_secret": false, "is_verified": false, - "line_number": 847, - "type": "Hex High Entropy String", + "line_number": 84, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "scripts/test_email_auth_api.py": [ { - "hashed_secret": "23db86ff034d7e9f799f0226a1f1a7baae4c7de1", + "hashed_secret": "55ae024ac61278627948dc9375b31bbbbfd7b442", "is_secret": false, "is_verified": false, - "line_number": 860, - "type": "Hex High Entropy String", + "line_number": 304, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "49b46f344ecff2a2a1820536df0334ca68050940", + "hashed_secret": "364d84ed2a75ef4c9a3cba031109db88e504511b", "is_secret": false, "is_verified": false, - "line_number": 874, - "type": "Hex High Entropy String", + "line_number": 329, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "50d2139960f135299c21e3cdaa6b6f076b1e2080", + "hashed_secret": "0378e32875a5f7f922d2cce11b0741096d61c5f2", "is_secret": false, "is_verified": false, - "line_number": 880, - "type": "Hex High Entropy String", + "line_number": 483, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "08f998bf3af34432739a9ab2cddd2bb85654a4b2", + "hashed_secret": "8b5f2dff4edd3be66dd04ef8f9b6da4f6a4d0463", "is_secret": false, "is_verified": false, - "line_number": 894, - "type": "Hex High Entropy String", + "line_number": 506, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "be61a53441932e4f82987dcaf2ef89fbcfaac0a5", + "hashed_secret": "1c5d7338a53cbdce0a6d26c4572e9725eb94ffc0", "is_secret": false, "is_verified": false, - "line_number": 900, - "type": "Hex High Entropy String", + "line_number": 519, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "7ed5883c89e869acd5ac1fb9e12730f4a42b29c4", + "hashed_secret": "673563640ec0c172e8d6f1316ae097269e3986c4", "is_secret": false, "is_verified": false, - "line_number": 915, - "type": "Hex High Entropy String", + "line_number": 527, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "c6818e944c28f358430e50279f8552ba70d43b1c", + "hashed_secret": "0269ac00d06201bd7bcf234fed607b713469d2d0", "is_secret": false, "is_verified": false, - "line_number": 921, - "type": "Hex High Entropy String", + "line_number": 551, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "3062e8c5ec47afaf7ba2ce1532e8e56c0af6ea8f", + "hashed_secret": "42d7bc4801cb72f2559bd9f6c9af9a2707ca321e", "is_secret": false, "is_verified": false, - "line_number": 927, - "type": "Hex High Entropy String", + "line_number": 742, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "scripts/test_mcp_client.py": [ { - "hashed_secret": "29aae00ffb8818a8acfa28442eb304f3862cbaf5", + "hashed_secret": "1ee61ed8e67eecf5f1e97c4786b6728b478b8fae", "is_secret": false, "is_verified": false, - "line_number": 938, + "line_number": 30, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "scripts/test_mcp_token_scoping.py": [ { - "hashed_secret": "3f43ff08f493a219d47746a5e566a00929107d92", + "hashed_secret": "ae28b10c8c7ef67658ef1de7b29ee23556a32eef", "is_secret": false, "is_verified": false, - "line_number": 948, + "line_number": 325, "type": "Hex High Entropy String", "verified_result": null }, { - "hashed_secret": "4faeea7fc4d0ed985b094fe85a90aab7fc3d7ae8", + "hashed_secret": "2df14e4719f299249cd9a97cf68cc87232a27cbb", "is_secret": false, "is_verified": false, - "line_number": 964, + "line_number": 348, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "tests/conftest.py": [ { - "hashed_secret": "61098053a09b0d7751e1a1c50a3c6208309c5b0c", + "hashed_secret": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", "is_secret": false, "is_verified": false, - "line_number": 976, - "type": "Hex High Entropy String", + "line_number": 115, + "type": "Basic Auth Credentials", "verified_result": null }, { - "hashed_secret": "7f471e64dc9e4d9e288dd5f95604e359310dd19d", + "hashed_secret": "206c80413b9a96c1312cc346b7d2517b84463edd", "is_secret": false, "is_verified": false, - "line_number": 985, - "type": "Hex High Entropy String", + "line_number": 164, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/e2e/mcp_test_helpers.py": [ { - "hashed_secret": "7a3f11072e339bc675d446b98fb026bdd111d679", + "hashed_secret": "07b7b454e840ca79e1582edcf973b051fc56e079", "is_secret": false, "is_verified": false, - "line_number": 991, - "type": "Hex High Entropy String", + "line_number": 39, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/e2e/test_admin_apis.py": [ { - "hashed_secret": "8ff217a235ef34ef9bbdc8de3ba9d64919c5f3ad", + "hashed_secret": "ba9cdae9b74942b8ac45ec20dfc3803a07fe6de9", "is_secret": false, "is_verified": false, - "line_number": 1001, - "type": "Hex High Entropy String", + "line_number": 58, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/e2e/test_admin_mcp_pool_metrics.py": [ { - "hashed_secret": "2bbb23c710243f6c2dd0da44f0bce1512d40aeb0", + "hashed_secret": "a1f09a2e61748dd65f724a3e680c1895ecf4c2cd", "is_secret": false, "is_verified": false, - "line_number": 1007, - "type": "Hex High Entropy String", + "line_number": 61, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/e2e/test_main_apis.py": [ { - "hashed_secret": "93408d219072bd897710a546d04acbdf8f518992", + "hashed_secret": "ba9cdae9b74942b8ac45ec20dfc3803a07fe6de9", "is_secret": false, "is_verified": false, - "line_number": 1013, - "type": "Hex High Entropy String", + "line_number": 80, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "190d967672500a817f10cf8c5f8ae7f1f73c006a", + "hashed_secret": "4dfad2d5130a5bcaf2716c495de92da13f4389e0", "is_secret": false, "is_verified": false, - "line_number": 1023, - "type": "Hex High Entropy String", + "line_number": 758, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/e2e/test_mcp_rbac_transport.py": [ { - "hashed_secret": "8977d964704890333a861cc91fb4738b73ca2c65", + "hashed_secret": "6e2e8a100b17f5c9083b5f8c6f2ed58ae6286e5b", "is_secret": false, "is_verified": false, - "line_number": 1029, - "type": "Hex High Entropy String", + "line_number": 892, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/e2e/test_oauth_protected_resource.py": [ { - "hashed_secret": "29cc4d113d2b819fd175fde15139a9047b4714ae", + "hashed_secret": "ba9cdae9b74942b8ac45ec20dfc3803a07fe6de9", "is_secret": false, "is_verified": false, - "line_number": 1035, - "type": "Hex High Entropy String", + "line_number": 58, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/e2e/test_translate_dynamic_env_e2e.py": [ { - "hashed_secret": "0e41bc284bfc2877d6684a4ecc1ac5095172bb24", + "hashed_secret": "528ceffb33609fca3a5646ec64a032d4362d301b", "is_secret": false, "is_verified": false, - "line_number": 1041, - "type": "Hex High Entropy String", + "line_number": 201, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "4d3dfbf42f79749cb066b45b4baa929fb891809a", + "hashed_secret": "d4f1a495029e6aec0aeb30aa3ce002dab645f08c", "is_secret": false, "is_verified": false, - "line_number": 1047, - "type": "Hex High Entropy String", + "line_number": 290, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "36b97c530b1f4c96e9033800c80908a6cfa4b761", + "hashed_secret": "fb6c2cbb5141e95639121909fe1b5a7012d1041a", "is_secret": false, "is_verified": false, - "line_number": 1053, - "type": "Hex High Entropy String", + "line_number": 338, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "2d70a27f9bdfdb0ebea3fa79f7962fa957bd11fa", + "hashed_secret": "8590ea4c4485851eeb808692bf07b80640a3619e", "is_secret": false, "is_verified": false, - "line_number": 1062, - "type": "Hex High Entropy String", + "line_number": 554, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/integration/test_concurrency_row_locking.py": [ { - "hashed_secret": "b7db53b4da996a7c3f03e516ecfb7b6663ad3a67", + "hashed_secret": "03aca59f05abb8e7b8ceb737167b6cd48dfa0a3f", "is_secret": false, "is_verified": false, - "line_number": 1068, - "type": "Hex High Entropy String", + "line_number": 62, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/integration/test_dcr_flow_integration.py": [ { - "hashed_secret": "551358ac7f3ecd5113292ebdd3a797bc72f72f08", + "hashed_secret": "352b2a4120b46b2e0221e753a1272cd34a6aa0da", "is_secret": false, "is_verified": false, - "line_number": 1077, - "type": "Hex High Entropy String", + "line_number": 144, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/integration/test_integration.py": [ { - "hashed_secret": "52174d432f72b603ab3aeb65e046d8842f64eba8", + "hashed_secret": "718cbcc5a4207c0d5f38e3a309bdba17cb0074b7", "is_secret": false, "is_verified": false, - "line_number": 1083, + "line_number": 218, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "tests/integration/test_translate_dynamic_env.py": [ { - "hashed_secret": "6b855f1f95b14579ef94fd09bab1c34327526c1e", + "hashed_secret": "528ceffb33609fca3a5646ec64a032d4362d301b", "is_secret": false, "is_verified": false, - "line_number": 1089, - "type": "Hex High Entropy String", + "line_number": 211, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/js/admin-ui.test.js": [ { - "hashed_secret": "22c495789b158bf96a04f9bd11015ad54eace5c6", + "hashed_secret": "1ded3053d0363079a4e681a3b700435d6d880290", "is_secret": false, "is_verified": false, - "line_number": 1095, - "type": "Hex High Entropy String", + "line_number": 157, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "1c467bf7b5d8839eb1e74702a5f267741ec101c2", + "hashed_secret": "0e5ce87ec4f3ba937574c210b09d984dbc1f1ece", "is_secret": false, "is_verified": false, - "line_number": 1106, - "type": "Hex High Entropy String", + "line_number": 172, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "dc2e94f24b05f113bf345bb2afbcb6e26645cd5c", + "hashed_secret": "9dbbba966fd8f1a21e6f0d38ffa4afc69cca66b6", "is_secret": false, "is_verified": false, - "line_number": 1123, - "type": "Hex High Entropy String", + "line_number": 213, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/load/README.md": [ { - "hashed_secret": "eeefac5557cd6eaf258414064c8b29f8afc34c65", + "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", "is_secret": false, "is_verified": false, - "line_number": 1132, - "type": "Hex High Entropy String", + "line_number": 80, + "type": "Basic Auth Credentials", "verified_result": null - }, + } + ], + "tests/loadtest/locustfile.py": [ { - "hashed_secret": "9f07d77e5781c78d5ccbce0d869c20816ce3211d", + "hashed_secret": "d033e22ae348aeb5660fc2140aec35850c4da997", "is_secret": false, "is_verified": false, - "line_number": 1146, - "type": "Hex High Entropy String", + "line_number": 3784, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "3eb645bfb98952d218d44f82f285b49ff2e34dde", + "hashed_secret": "d73bccb432c5c7b1b166cfa603b3b99af63fd580", "is_secret": false, "is_verified": false, - "line_number": 1156, - "type": "Hex High Entropy String", + "line_number": 6627, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "0a549d859d0064d99bd64aa5226d0588e42e1f32", + "hashed_secret": "20d22126242b5c7e253d84dd6ff0ed7a6d2662a2", "is_secret": false, "is_verified": false, - "line_number": 1165, - "type": "Hex High Entropy String", + "line_number": 6983, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "12061f2ebc56b7c0d1167ed4ead1f4570bf89831", + "hashed_secret": "fa9beb99e4029ad5a6615399e7bbae21356086b3", "is_secret": false, "is_verified": false, - "line_number": 1171, - "type": "Hex High Entropy String", + "line_number": 7007, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/loadtest/locustfile_baseline.py": [ { - "hashed_secret": "311b714970e6d0063373ae0f8669e447214d8626", + "hashed_secret": "2df14e4719f299249cd9a97cf68cc87232a27cbb", "is_secret": false, "is_verified": false, - "line_number": 1180, + "line_number": 96, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "tests/loadtest/locustfile_echo_delay.py": [ { - "hashed_secret": "74dec2a35b9068a13cd960b157b962611bf31365", + "hashed_secret": "293324f6824bb3a6db5c4dc42a60ddd4a9851c99", "is_secret": false, "is_verified": false, - "line_number": 1191, + "line_number": 103, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "tests/loadtest/locustfile_mcp_isolation.py": [ { - "hashed_secret": "ebbea2a8b6a37ded68465f81f315d4a23c27a8e8", + "hashed_secret": "07b7b454e840ca79e1582edcf973b051fc56e079", "is_secret": false, "is_verified": false, - "line_number": 1202, - "type": "Hex High Entropy String", + "line_number": 34, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/manual/README.md": [ { - "hashed_secret": "3558448508502aa5a344f39c803ec2665ee9e81b", + "hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3", "is_secret": false, "is_verified": false, - "line_number": 1211, - "type": "Hex High Entropy String", + "line_number": 214, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "a71d7ae63f77e488a44fc92784434919791f0c3f", + "hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3", "is_secret": false, "is_verified": false, - "line_number": 1217, - "type": "Hex High Entropy String", + "line_number": 219, + "type": "Basic Auth Credentials", "verified_result": null - }, + } + ], + "tests/manual/concurrency/conc_01_gateways_results.md": [ { - "hashed_secret": "e591abf3379f4a9791ed9fe0134efe5eeb70d9ee", + "hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3", "is_secret": false, "is_verified": false, - "line_number": 1223, - "type": "Hex High Entropy String", + "line_number": 42, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "069b2d0ee6a81123a6f841920f52bab96e24cd15", + "hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3", "is_secret": false, "is_verified": false, - "line_number": 1238, - "type": "Hex High Entropy String", + "line_number": 126, + "type": "Basic Auth Credentials", "verified_result": null - }, + } + ], + "tests/manual/concurrency/conc_02_gateways_results.md": [ { - "hashed_secret": "8d059a6ae441b561e2f1c076d396fae696e5a25a", + "hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3", "is_secret": false, "is_verified": false, - "line_number": 1249, - "type": "Hex High Entropy String", + "line_number": 43, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "32daac6899f5795631ba20f4716dc2b264fc32f2", + "hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3", "is_secret": false, "is_verified": false, - "line_number": 1255, - "type": "Hex High Entropy String", + "line_number": 58, + "type": "Basic Auth Credentials", "verified_result": null - }, + } + ], + "tests/manual/concurrency/run_conc_01_gateways.sh": [ { - "hashed_secret": "57a3c2bd3253c61856df4bf13198109b0d51486d", + "hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3", "is_secret": false, "is_verified": false, - "line_number": 1267, - "type": "Hex High Entropy String", + "line_number": 9, + "type": "Basic Auth Credentials", "verified_result": null - }, + } + ], + "tests/manual/concurrency/run_conc_02_gateways.sh": [ { - "hashed_secret": "802fb7ee4a23f1061a4a099e6518b448f5dea8d3", + "hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3", "is_secret": false, "is_verified": false, - "line_number": 1277, - "type": "Hex High Entropy String", + "line_number": 9, + "type": "Basic Auth Credentials", "verified_result": null - }, + } + ], + "tests/manual/testcases/api_a2a.yaml": [ { - "hashed_secret": "a4cf867673699f5dc3d18038b5a87c2a7bf4dfd3", + "hashed_secret": "69dba6dfc0be932d10cdbfaae770c300cd7e0e3c", "is_secret": false, "is_verified": false, - "line_number": 1290, - "type": "Hex High Entropy String", + "line_number": 43, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "a02ff67f8e504e59af8eff72afb26fda27037a42", + "hashed_secret": "fd9a45bc0b07706d849cf85021ecf9123fa83d82", "is_secret": false, "is_verified": false, - "line_number": 1296, - "type": "Hex High Entropy String", + "line_number": 54, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/manual/testcases/api_authentication.yaml": [ { - "hashed_secret": "d978e33d84309bf97aa5af7e47486be164c0787e", + "hashed_secret": "5d14de7c2c81aaa796123494202ba7d1d27b207b", "is_secret": false, "is_verified": false, - "line_number": 1302, - "type": "Hex High Entropy String", + "line_number": 31, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "f2ceefa876785b2d7376d2af71e760c71ec0109e", + "hashed_secret": "fa9beb99e4029ad5a6615399e7bbae21356086b3", "is_secret": false, "is_verified": false, - "line_number": 1308, - "type": "Hex High Entropy String", + "line_number": 47, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "ea76a6819e6c7b642a58ff2639e061e2303fc28d", + "hashed_secret": "0d8343a9d687097938d7d9e630c5e37991949108", "is_secret": false, "is_verified": false, - "line_number": 1314, - "type": "Hex High Entropy String", + "line_number": 110, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/manual/testcases/api_federation.yaml": [ { - "hashed_secret": "20b3b413826d61a82664d755aaaa4a180123d161", + "hashed_secret": "fa9beb99e4029ad5a6615399e7bbae21356086b3", "is_secret": false, "is_verified": false, - "line_number": 1320, - "type": "Hex High Entropy String", + "line_number": 39, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/manual/testcases/database_tests.yaml": [ { - "hashed_secret": "56397d9a520742406189d71ac27362811f4eb3f4", + "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", "is_secret": false, "is_verified": false, - "line_number": 1329, - "type": "Hex High Entropy String", + "line_number": 79, + "type": "Basic Auth Credentials", "verified_result": null - }, + } + ], + "tests/manual/testcases/migration_tests.yaml": [ { - "hashed_secret": "978bcf06dc12b74b1579b446ae082265db1358f6", + "hashed_secret": "5f8b73c6cddcd6dd3a2eb5747a25be2e71b6d202", "is_secret": false, "is_verified": false, - "line_number": 1335, - "type": "Hex High Entropy String", + "line_number": 118, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "d4167cd9e3def427dd3a89e8f434dd0799b3f1d1", + "hashed_secret": "6082663664461e9cbe453fe5325a381f10dcddda", "is_secret": false, "is_verified": false, - "line_number": 1344, - "type": "Hex High Entropy String", + "line_number": 149, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/manual/testcases/security_tests.yaml": [ { - "hashed_secret": "8cc69a0306a6b8f4c32428f44505f11eaebb3ebf", + "hashed_secret": "656ae8968017cd346560397f967a2c2dbfb70f6f", "is_secret": false, "is_verified": false, - "line_number": 1354, - "type": "Hex High Entropy String", + "line_number": 122, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/manual/testcases/setup_instructions.yaml": [ { - "hashed_secret": "6538f9a3e2204f918d5719f17a23367f4d2105c7", + "hashed_secret": "46e6a38b6d25f56087891d1152d4d082b5767b3e", "is_secret": false, "is_verified": false, - "line_number": 1364, - "type": "Hex High Entropy String", + "line_number": 84, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/migration/conftest.py": [ { - "hashed_secret": "b4259ea178dd7cb0249df5d8feb7d13a2d6fd820", + "hashed_secret": "1b255e8e77fb3fafbf1fe2013347f762fbca9cd6", "is_secret": false, "is_verified": false, - "line_number": 1373, - "type": "Hex High Entropy String", + "line_number": 273, + "type": "Basic Auth Credentials", "verified_result": null - }, + } + ], + "tests/migration/docker-compose.test.yml": [ { - "hashed_secret": "fb82e6c6c8d3ed2486ef8249d7e59336b5c86610", + "hashed_secret": "1b255e8e77fb3fafbf1fe2013347f762fbca9cd6", "is_secret": false, "is_verified": false, - "line_number": 1382, - "type": "Hex High Entropy String", + "line_number": 18, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "ba44f293d24fbbcbe1ef98385355f7fed4f8419d", + "hashed_secret": "1b255e8e77fb3fafbf1fe2013347f762fbca9cd6", "is_secret": false, "is_verified": false, - "line_number": 1388, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "bfdcf12c00b303dde9a57ba3e08927a60a89c6b8", - "is_secret": false, - "is_verified": false, - "line_number": 1394, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8be0eab839d1133fc403dcc2790b84d4dfff3381", - "is_secret": false, - "is_verified": false, - "line_number": 1404, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "891ddfb5f57f136198961b98f8014e3c7bc9e602", - "is_secret": false, - "is_verified": false, - "line_number": 1414, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "19c50b4f711d39a10ac1890b58e609934e3e7ff6", - "is_secret": false, - "is_verified": false, - "line_number": 1423, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "74ac836ae646fd72c7c162ace0d10d50d81af86b", - "is_secret": false, - "is_verified": false, - "line_number": 1432, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "852251ba1e8919a04b1d4678fbf9ba2a73e82a19", - "is_secret": false, - "is_verified": false, - "line_number": 1441, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "0675b70f52618de044d1d1703b6c8849b407c83a", - "is_secret": false, - "is_verified": false, - "line_number": 1452, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "c63addae2503bfe26923f0460158bb2fc674b928", - "is_secret": false, - "is_verified": false, - "line_number": 1464, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9f7a7461ae8d5e27458e899b4e84416483680a51", - "is_secret": false, - "is_verified": false, - "line_number": 1475, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "5b35b10ce4ffd9e769dc5edeb91e3717ca35a110", - "is_secret": false, - "is_verified": false, - "line_number": 1481, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f66222a8ae1b071937f17107a8a7ff76f9e4fcd0", - "is_secret": false, - "is_verified": false, - "line_number": 1516, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "356f1e2390be5a96f7e81a7d5cc78eae496d3d6a", - "is_secret": false, - "is_verified": false, - "line_number": 1525, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "5fc581d05017589594c7337c5773c29468817fb5", - "is_secret": false, - "is_verified": false, - "line_number": 1557, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f808ceeac2f8a46610fb15ef6ad978cbaa37a11d", - "is_secret": false, - "is_verified": false, - "line_number": 1570, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "1ac59a9207d43c177051b356ba21af36e0c5e1c3", - "is_secret": false, - "is_verified": false, - "line_number": 1585, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b64689f9c223de81042e74dbcfbe5a73a2c1544c", - "is_secret": false, - "is_verified": false, - "line_number": 1598, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9daac1b266d3875ecff4a4b7932e73d63f51718f", - "is_secret": false, - "is_verified": false, - "line_number": 1607, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "60a673ff967b996194d288e551490d960a18d4e2", - "is_secret": false, - "is_verified": false, - "line_number": 1613, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d4b5e511e6b010068fe8fc17a580b24af40fca83", - "is_secret": false, - "is_verified": false, - "line_number": 1619, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8a1b53508b16a129f35039f353671356a8281e8b", - "is_secret": false, - "is_verified": false, - "line_number": 1628, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "58f0106d7117389d48944ab0421c44ca96dd7992", - "is_secret": false, - "is_verified": false, - "line_number": 1637, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "bc37fdf97c85de38881099f997b799f0ce27c830", - "is_secret": false, - "is_verified": false, - "line_number": 1651, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "190341c4e045ee5d256b052fa97f59b1d066169f", - "is_secret": false, - "is_verified": false, - "line_number": 1663, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "3632f3823bb444d1697daae3d3c00f8c1472fbac", - "is_secret": false, - "is_verified": false, - "line_number": 1669, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a3f550383fa80c876ecec52c42b21b375aec47a3", - "is_secret": false, - "is_verified": false, - "line_number": 1682, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ba8ac4ac3810916dd75cdf9bb539b045d8f828f4", - "is_secret": false, - "is_verified": false, - "line_number": 1692, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "dd30e69b06fc9e7153b91d6152b47abee06bc76e", - "is_secret": false, - "is_verified": false, - "line_number": 1698, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "edea2c54c8d390fdfff6535d3da4fd1fdf17b62a", - "is_secret": false, - "is_verified": false, - "line_number": 1708, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "bfb212002ccc72caf73acbfb0a5bd6c1c2acaa42", - "is_secret": false, - "is_verified": false, - "line_number": 1717, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "044ff8fe1e6d9bb34b8e097fa053256f451e1d65", - "is_secret": false, - "is_verified": false, - "line_number": 1728, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "93658223830f17449a3b81a759279f8db378716a", - "is_secret": false, - "is_verified": false, - "line_number": 1739, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "cd5a59f8105c99afb30595e52a11d73e855f2c9f", - "is_secret": false, - "is_verified": false, - "line_number": 1752, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f4fd806c07aaf73c1529b88212a826fadcae43ae", - "is_secret": false, - "is_verified": false, - "line_number": 1763, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "dafd46fad0421f3f00574f5c736ec0847f53d212", - "is_secret": false, - "is_verified": false, - "line_number": 1775, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8f48b5146d88920187ca1c8d1d52cd0b008f12de", - "is_secret": false, - "is_verified": false, - "line_number": 1784, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "083e28bdae3cd5b3af135e50a5758bd732e076c4", - "is_secret": false, - "is_verified": false, - "line_number": 1790, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "630ac66c09261f06d21e712acac6287e3d573cc9", - "is_secret": false, - "is_verified": false, - "line_number": 1800, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "1eeb415c50a8d2b04a705ebf09786adf00d8cf59", - "is_secret": false, - "is_verified": false, - "line_number": 1806, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "14b703e941e97ca4115fbfc7afc9d19920aa9c6c", - "is_secret": false, - "is_verified": false, - "line_number": 1812, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f711a3e8a220ceb3c9b360745772e4ed8659f46c", - "is_secret": false, - "is_verified": false, - "line_number": 1818, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ca0b633ac167cec25b04208a0e3caeec68ad6c15", - "is_secret": false, - "is_verified": false, - "line_number": 1828, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "7d64118ef4ee08c6e431da22c00cc6db06fc8495", - "is_secret": false, - "is_verified": false, - "line_number": 1841, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "fc643b8317813d4055f095a72f90dfc83ac98a52", - "is_secret": false, - "is_verified": false, - "line_number": 1847, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "fed671d6a24626d6cf16f91b98f2b11ba736d095", - "is_secret": false, - "is_verified": false, - "line_number": 1853, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "33eb20252a41cba23b834a062ead51beccf29376", - "is_secret": false, - "is_verified": false, - "line_number": 1864, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "24c6c50199abdbdf01f5bfc41c06d323bb57ac6b", - "is_secret": false, - "is_verified": false, - "line_number": 1873, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "611820b81f8ac5cb3dca1a7fb85e67110bc551c5", - "is_secret": false, - "is_verified": false, - "line_number": 1884, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f57b63e67ed485c5ffe3dec0888657ac4c281ca5", - "is_secret": false, - "is_verified": false, - "line_number": 1897, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "824096ad190248d40cef63862048a64d18e73093", - "is_secret": false, - "is_verified": false, - "line_number": 1906, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "67eb96441e8f50509300c66e81b167f6e3f0f21d", - "is_secret": false, - "is_verified": false, - "line_number": 1917, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "09057af5054a5ad648e142f3d8f20650c1961474", - "is_secret": false, - "is_verified": false, - "line_number": 1926, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b62ab1173e770de947a50fe2b3ae83805000809b", - "is_secret": false, - "is_verified": false, - "line_number": 1941, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "de47997a6ccac36e55eafe4f8112bbe269455d65", - "is_secret": false, - "is_verified": false, - "line_number": 1947, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "86efcec63d7f5e5c3cf34e1acfd29fc257f3c382", - "is_secret": false, - "is_verified": false, - "line_number": 1957, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8e11a5e472d1d4d98e2bdc00b90448cca073dd97", - "is_secret": false, - "is_verified": false, - "line_number": 1967, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a5ea7e9c1ce564f30e9551f8200f29d90aa6ec5e", - "is_secret": false, - "is_verified": false, - "line_number": 1984, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "7794fee4eaeafc705f8eeba3b4d69cba7869dab0", - "is_secret": false, - "is_verified": false, - "line_number": 1995, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e5720b6d98058e32a6194a43416caa780707a3de", - "is_secret": false, - "is_verified": false, - "line_number": 2005, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "dcb719d0fd8853d6851ce72bfe162430bd5541c1", - "is_secret": false, - "is_verified": false, - "line_number": 2016, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "5a9e3d91c47e5f9099917f07fd7130195338b4ca", - "is_secret": false, - "is_verified": false, - "line_number": 2027, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "79a0a8be10240450c72322f433eb92948903500f", - "is_secret": false, - "is_verified": false, - "line_number": 2040, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "102dfadad7f84fbd0da59ec7a4360651e8018702", - "is_secret": false, - "is_verified": false, - "line_number": 2056, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2bb2fe1e57f60f9972d8dc8aafecc14ea140b262", - "is_secret": false, - "is_verified": false, - "line_number": 2074, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d93f2cd8895569df797eb6a57011aa1489604dee", - "is_secret": false, - "is_verified": false, - "line_number": 2080, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "3f85aa33de16e014a62133be00e2fae3fee16a15", - "is_secret": false, - "is_verified": false, - "line_number": 2086, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "319d3f52ed81b3c7c6a69e0ddb313dba40f94795", - "is_secret": false, - "is_verified": false, - "line_number": 2098, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "99bce2688418d3adf96547726401effde2101407", - "is_secret": false, - "is_verified": false, - "line_number": 2109, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "611ee95376df586616103d908ee6f0e50de5ce6f", - "is_secret": false, - "is_verified": false, - "line_number": 2119, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8792035d236f541da7fcdacf04647f5050fd4f37", - "is_secret": false, - "is_verified": false, - "line_number": 2130, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2bba312da1074a195e616dd95b0d9970dcbf623b", - "is_secret": false, - "is_verified": false, - "line_number": 2148, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "3723256ef4a259c5ffb74f1064cd3af866f10e67", - "is_secret": false, - "is_verified": false, - "line_number": 2154, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "356ed61809a90d30af6045b6733d7bd893616efd", - "is_secret": false, - "is_verified": false, - "line_number": 2160, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "6ff92b40723a4d4d939cd2a59bb13f9f0291a78f", - "is_secret": false, - "is_verified": false, - "line_number": 2173, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2cec7afe8f0f8f21d28f274299142cbbe949023c", - "is_secret": false, - "is_verified": false, - "line_number": 2184, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "370f2dab232e01715bb1463fe9a85a8a0e3b41ed", - "is_secret": false, - "is_verified": false, - "line_number": 2190, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d8f1d314d800de9172c39e761f105816ae4af3e8", - "is_secret": false, - "is_verified": false, - "line_number": 2196, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a0ccf26d3e5e73d1fea4cbd7e882f8a4269b51e6", - "is_secret": false, - "is_verified": false, - "line_number": 2208, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "83719d2d8284b30dee2823349c2d7f600e237d8c", - "is_secret": false, - "is_verified": false, - "line_number": 2214, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "04a1a3194c674cd1244ae336e0fe7054fb9bc64f", - "is_secret": false, - "is_verified": false, - "line_number": 2220, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "788b34ba07c35e639f6e3e82f608189791e3239f", - "is_secret": false, - "is_verified": false, - "line_number": 2231, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a2de2067076eb603a022b1b629bba6167194fcaa", - "is_secret": false, - "is_verified": false, - "line_number": 2237, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "cbd62b2fef0566cbd4adcae3c005b9fb1d23170d", - "is_secret": false, - "is_verified": false, - "line_number": 2243, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "5ab5e8c5a013b4ea054a281a403fd0b7ab4917bc", - "is_secret": false, - "is_verified": false, - "line_number": 2249, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "831b1f0f0af506c6b1b5c2de459b9b4b4f3ac633", - "is_secret": false, - "is_verified": false, - "line_number": 2259, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d1837f7e31117d99b0cbffd82e7eabcea851a94c", - "is_secret": false, - "is_verified": false, - "line_number": 2268, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "040278ead73fc50f601174bf24cfe3552915a67e", - "is_secret": false, - "is_verified": false, - "line_number": 2274, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b85b391225b20c3137322bcc23c7e2c31320a83e", - "is_secret": false, - "is_verified": false, - "line_number": 2283, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "604edf3bd2202bebf89e23895a6ef68088f57e42", - "is_secret": false, - "is_verified": false, - "line_number": 2292, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a98f930ed9e869e7534be3e4b8ca121b2f46b3cc", - "is_secret": false, - "is_verified": false, - "line_number": 2305, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "7167df607c246fe7c20de1a7b7abc394c7026cd0", - "is_secret": false, - "is_verified": false, - "line_number": 2319, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f0f817643483baa005cf31ad44d972f65508675e", - "is_secret": false, - "is_verified": false, - "line_number": 2329, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "64c9975a7ab1b5bcd2439c334f2370a65d4aae82", - "is_secret": false, - "is_verified": false, - "line_number": 2342, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d3858567b9054a3163ab6dfd94a79713a72f7cec", - "is_secret": false, - "is_verified": false, - "line_number": 2351, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "042d13670968f7c52be4bc75a67035bdc4128e45", - "is_secret": false, - "is_verified": false, - "line_number": 2361, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "35f86be9fc56fed00caa9d04d07259deee6e5713", - "is_secret": false, - "is_verified": false, - "line_number": 2373, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "7493d3c7ad45735197216f5195a3a4b2f0dc05f8", - "is_secret": false, - "is_verified": false, - "line_number": 2385, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2dc6e8f934b74951c38b2c67b869de588648a255", - "is_secret": false, - "is_verified": false, - "line_number": 2395, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4388f4a1071af57b8f44733a4b590114701c8263", - "is_secret": false, - "is_verified": false, - "line_number": 2404, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "c635956e53a2624afd87d3ac8a41701e377f00e7", - "is_secret": false, - "is_verified": false, - "line_number": 2417, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "5747d8af9684661644b7df2fe00e51dc16f6843d", - "is_secret": false, - "is_verified": false, - "line_number": 2428, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a5f5903bd227b9d811f037da91086231b40422a7", - "is_secret": false, - "is_verified": false, - "line_number": 2439, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "34cfa36d78842de5ac91d80bd7a8996d903abee8", - "is_secret": false, - "is_verified": false, - "line_number": 2445, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "6be5b6b2b339ebb0359e498fb3752087d91f5ea2", - "is_secret": false, - "is_verified": false, - "line_number": 2454, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f9de3f6e836fa1b0a50f4e0e06154a422c0ed705", - "is_secret": false, - "is_verified": false, - "line_number": 2463, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "0253824dccde843d47fa65b99bb29912e5ec3a0a", - "is_secret": false, - "is_verified": false, - "line_number": 2472, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d8a8d970a479e89ac4678dabd4216dfa8b22e040", - "is_secret": false, - "is_verified": false, - "line_number": 2481, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d4e3e4ca451b86ca4ea86a0b2db6ec3ff7c01c22", - "is_secret": false, - "is_verified": false, - "line_number": 2492, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "84469e279d411898bc4109bd5c351e99cec1a050", - "is_secret": false, - "is_verified": false, - "line_number": 2508, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8f9c265c3866a77dcb45fd46fa0b83dc78acf82d", - "is_secret": false, - "is_verified": false, - "line_number": 2523, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4eee70986dc22664f0597495a6d0dd22fc5422c4", - "is_secret": false, - "is_verified": false, - "line_number": 2542, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "54f3ab20cd8669d07e74b7b8767b7c2d2be98e84", - "is_secret": false, - "is_verified": false, - "line_number": 2560, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "dfd0762d8aa9fbdd90ebad7aa8fee494eb5849a5", - "is_secret": false, - "is_verified": false, - "line_number": 2566, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ed1ba03fabc77d3aa81f08b87029efbd7d9a1340", - "is_secret": false, - "is_verified": false, - "line_number": 2572, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "3a49d16dc29026d169224f8bc14e6828a62128c2", - "is_secret": false, - "is_verified": false, - "line_number": 2583, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f2031cef7184ff2655c2795f8f53c0451d4ea648", - "is_secret": false, - "is_verified": false, - "line_number": 2595, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "fb3ffd7b8e0ef97a3b404c2db1b91868d28b0298", - "is_secret": false, - "is_verified": false, - "line_number": 2604, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a58d51dc68f673a049f4d82bdf01e61cf73b93b2", - "is_secret": false, - "is_verified": false, - "line_number": 2615, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "86abb8da6326f0620dc9f0951186d370cad910b9", - "is_secret": false, - "is_verified": false, - "line_number": 2624, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "3b7aac6c5358eda96f36b1d1a7df21dd571dec15", - "is_secret": false, - "is_verified": false, - "line_number": 2636, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "fa35aa28d81cf737ac8af26aae31b0b195eef3af", - "is_secret": false, - "is_verified": false, - "line_number": 2642, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "6274c331e3520248957d3a2497e2edab2c3aaee0", - "is_secret": false, - "is_verified": false, - "line_number": 2653, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "27ab5e55f6ba7afa0e99492446d43050c7b719fa", - "is_secret": false, - "is_verified": false, - "line_number": 2664, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "94014c04a964970c30d309870f97ea5d8ea5bfd8", - "is_secret": false, - "is_verified": false, - "line_number": 2675, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/admin.py": [ - { - "hashed_secret": "fa9beb99e4029ad5a6615399e7bbae21356086b3", - "is_secret": false, - "is_verified": false, - "line_number": 4256, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "559b05f1b2863e725b76e216ac3dadecbf92e244", - "is_secret": false, - "is_verified": false, - "line_number": 4864, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "a8af4759392d4f7496d613174f33afe2074a4b8d", - "is_secret": false, - "is_verified": false, - "line_number": 4866, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "85b60d811d16ff56b3654587d4487f713bfa33b7", - "is_secret": false, - "is_verified": false, - "line_number": 14931, - "type": "Secret Keyword", - "verified_result": null - } - ], - "mcpgateway/alembic.ini": [ - { - "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", - "is_secret": false, - "is_verified": false, - "line_number": 87, - "type": "Basic Auth Credentials", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/04cda6733305_add_admin_types_to_email_users.py": [ - { - "hashed_secret": "c052f9f7b5b94886b423384e4938ed8388cd77b4", - "is_secret": false, - "is_verified": false, - "line_number": 28, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/0f81d4a5efe0_new_table_email_team_member_history_for_.py": [ - { - "hashed_secret": "aeaf4ea9e7c81ad714da7db03735fd1d053414c7", - "is_secret": false, - "is_verified": false, - "line_number": 20, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/14ac971cee42_add_user_context_to_oauth_tokens.py": [ - { - "hashed_secret": "8e70fb96660fbce85c4be348188c03c3d98a5e95", - "is_secret": false, - "is_verified": false, - "line_number": 19, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/191a2def08d7_resource_rename_template_to_uri_template.py": [ - { - "hashed_secret": "36b0556065adc2fdd8a15b7ffac028a043c34595", - "is_secret": false, - "is_verified": false, - "line_number": 17, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "0c1c30eba610778ef5e68a58883a052733a22bd4", - "is_secret": false, - "is_verified": false, - "line_number": 18, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/1fc1795f6983_merge_a2a_and_custom_name_changes.py": [ - { - "hashed_secret": "b6d082a6cc1261d009b34cddd19dca4828f1cb7d", - "is_secret": false, - "is_verified": false, - "line_number": 18, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/34492f99a0c4_add_comprehensive_metadata_to_all_.py": [ - { - "hashed_secret": "9743f9059c52643b7cc917663462e853fb36f69d", - "is_secret": false, - "is_verified": false, - "line_number": 23, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/356a2d4eed6f_uuid_change_for_prompt_and_resources.py": [ - { - "hashed_secret": "c81482140a7bf22e957d89f358479e90adef80dc", - "is_secret": false, - "is_verified": false, - "line_number": 20, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "c561aac847538846232699cfaed7fc7fabd1d134", - "is_secret": false, - "is_verified": false, - "line_number": 21, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/3b17fdc40a8d_add_passthrough_headers_to_gateways_and_.py": [ - { - "hashed_secret": "044c1c4f5cc1f89969f46018b2bacbe8918aaf1a", - "is_secret": false, - "is_verified": false, - "line_number": 22, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/3c89a45f32e5_add_grpc_services_table.py": [ - { - "hashed_secret": "83af00f2e79329f549c1438dec118e0cba4ad111", - "is_secret": false, - "is_verified": false, - "line_number": 18, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/43c07ed25a24_add_oauth_fields_to_servers.py": [ - { - "hashed_secret": "48ffbad96aa9c2b33f9486f5a3c2108198acb518", - "is_secret": false, - "is_verified": false, - "line_number": 21, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/4e6273136e56_normalize_registered_oauth_client_issuer.py": [ - { - "hashed_secret": "2d40811ded1703d949aca3c40d77175a5c568c05", - "is_secret": false, - "is_verified": false, - "line_number": 24, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/5f3c681b05e1_add_index_for_team_name.py": [ - { - "hashed_secret": "e6ec1410341b496819aeb85882509889c33017af", - "is_secret": false, - "is_verified": false, - "line_number": 20, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/61ee11c482d6_add_code_verifier_to_oauth_states_for_.py": [ - { - "hashed_secret": "aeaf4ea9e7c81ad714da7db03735fd1d053414c7", - "is_secret": false, - "is_verified": false, - "line_number": 19, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/64acf94cb7f2_cleanup_inactive_duplicate_user_roles.py": [ - { - "hashed_secret": "dc7c15edd98c7c7ce4cd107fb4ccb17329510002", - "is_secret": false, - "is_verified": false, - "line_number": 24, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "52294c20155b397fbf140baa0cfb6d9400c82c25", - "is_secret": false, - "is_verified": false, - "line_number": 25, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/733159a4fa74_add_display_name_to_tools.py": [ - { - "hashed_secret": "b6d082a6cc1261d009b34cddd19dca4828f1cb7d", - "is_secret": false, - "is_verified": false, - "line_number": 23, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/77243f5bfce5_add_tool_id_to_a2a_agents.py": [ - { - "hashed_secret": "21562fcd9707470b4fa7cec05458e86caeb25fda", - "is_secret": false, - "is_verified": false, - "line_number": 18, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/8a16a77260f0_adding_original_description_column_to_.py": [ - { - "hashed_secret": "10b36c8089b5791d82f0700828fd23a9c782101f", - "is_secret": false, - "is_verified": false, - "line_number": 19, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/8a2934be50c0_rest_pass_api_fld_tools.py": [ - { - "hashed_secret": "c84583fef8c1325676bd098d09cfc64f185792f7", - "is_secret": false, - "is_verified": false, - "line_number": 18, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/9e028ecf59c4_tag_records_changes_list_str_to_list_.py": [ - { - "hashed_secret": "c561aac847538846232699cfaed7fc7fabd1d134", - "is_secret": false, - "is_verified": false, - "line_number": 19, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/9f5d93ced2b3_add_llm_permissions_to_default_roles.py": [ - { - "hashed_secret": "99927e177522cd7b4b713ac9fe99968d2f71b829", - "is_secret": false, - "is_verified": false, - "line_number": 26, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/a23a08d61eb0_add_observability_tables.py": [ - { - "hashed_secret": "9d88d7eece452a76b2955ecfbb251f23cf7b7905", - "is_secret": false, - "is_verified": false, - "line_number": 18, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/a3c38b6c2437_fix_a2a_agents_auth_value.py": [ - { - "hashed_secret": "52294c20155b397fbf140baa0cfb6d9400c82c25", - "is_secret": false, - "is_verified": false, - "line_number": 31, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9b094435d625c9385a1bb083604f87fa7c225da0", - "is_secret": false, - "is_verified": false, - "line_number": 32, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/a4f1c7d8e9b0_add_app_user_email_to_oauth_states.py": [ - { - "hashed_secret": "5ebf53ef06815a59e9fd0378d3a19dff8b0ac28a", - "is_secret": false, - "is_verified": false, - "line_number": 17, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "99927e177522cd7b4b713ac9fe99968d2f71b829", - "is_secret": false, - "is_verified": false, - "line_number": 18, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/a7f3c9e1b2d4_add_title_to_tools_resources_prompts.py": [ - { - "hashed_secret": "2e6b7b27cad43ed0908be745231a98936aad5293", - "is_secret": false, - "is_verified": false, - "line_number": 17, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/a8f3b2c1d4e5_add_gateway_refresh_fields.py": [ - { - "hashed_secret": "19c9ebc4037f02acb15d950b258d51e9eb249966", - "is_secret": false, - "is_verified": false, - "line_number": 19, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "21562fcd9707470b4fa7cec05458e86caeb25fda", - "is_secret": false, - "is_verified": false, - "line_number": 20, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/aac21d6f9522_merge_ca_cert_and_observability_heads.py": [ - { - "hashed_secret": "74cf584eb4fffdfa472c808000adb71841786f3e", - "is_secret": false, - "is_verified": false, - "line_number": 14, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/abf8ac3b6008_add_admin_overview_and_servers_use_to_.py": [ - { - "hashed_secret": "dc7c15edd98c7c7ce4cd107fb4ccb17329510002", - "is_secret": false, - "is_verified": false, - "line_number": 32, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/add_oauth_tokens_table.py": [ - { - "hashed_secret": "3333a8285ac54459f46f2ef561184275586dd1a7", - "is_secret": false, - "is_verified": false, - "line_number": 23, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/b2d9c6e4f1a7_add_explicit_team_and_token_permissions.py": [ - { - "hashed_secret": "c4770f062e9bfa8ec054475f98315913da1d16fd", - "is_secret": false, - "is_verified": false, - "line_number": 25, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "5ebf53ef06815a59e9fd0378d3a19dff8b0ac28a", - "is_secret": false, - "is_verified": false, - "line_number": 26, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/b9e496e91e71_merge_gateway_refresh_and_sso_provider_.py": [ - { - "hashed_secret": "19c9ebc4037f02acb15d950b258d51e9eb249966", - "is_secret": false, - "is_verified": false, - "line_number": 14, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/ba202ac1665f_migrate_user_roles_to_configurable_.py": [ - { - "hashed_secret": "10b36c8089b5791d82f0700828fd23a9c782101f", - "is_secret": false, - "is_verified": false, - "line_number": 39, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/c96c11c111b4_create_index_on_user_name.py": [ - { - "hashed_secret": "21562fcd9707470b4fa7cec05458e86caeb25fda", - "is_secret": false, - "is_verified": false, - "line_number": 29, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/cfc3d6aa0fb2_consolidated_multiuser_team_rbac_.py": [ - { - "hashed_secret": "f216ad54219955ae0c4c306010eee4c60b0ec6e1", - "is_secret": false, - "is_verified": false, - "line_number": 30, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/d9e0f1a2b3c4_change_token_uniqueness_to_per_team.py": [ - { - "hashed_secret": "45fc9ab5f41816c6979df8bc85ffdc160a0e696a", - "is_secret": false, - "is_verified": false, - "line_number": 29, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "c4770f062e9bfa8ec054475f98315913da1d16fd", - "is_secret": false, - "is_verified": false, - "line_number": 30, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/e182847d89e6_unique_constraints_changes_for_gateways_.py": [ - { - "hashed_secret": "8e70fb96660fbce85c4be348188c03c3d98a5e95", - "is_secret": false, - "is_verified": false, - "line_number": 22, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f216ad54219955ae0c4c306010eee4c60b0ec6e1", - "is_secret": false, - "is_verified": false, - "line_number": 23, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/e1f2a3b4c5d6_add_grant_source_to_user_roles.py": [ - { - "hashed_secret": "9b094435d625c9385a1bb083604f87fa7c225da0", - "is_secret": false, - "is_verified": false, - "line_number": 22, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "45fc9ab5f41816c6979df8bc85ffdc160a0e696a", - "is_secret": false, - "is_verified": false, - "line_number": 23, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/e5a59c16e041_unique_const_changes_for_prompt_and_.py": [ - { - "hashed_secret": "81d89c741bbf0978e92cb9852870242cc88c7db2", - "is_secret": false, - "is_verified": false, - "line_number": 18, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "c84583fef8c1325676bd098d09cfc64f185792f7", - "is_secret": false, - "is_verified": false, - "line_number": 19, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/eb17fd368f9d_merge_passthrough_headers_and_tags_.py": [ - { - "hashed_secret": "9743f9059c52643b7cc917663462e853fb36f69d", - "is_secret": false, - "is_verified": false, - "line_number": 18, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "044c1c4f5cc1f89969f46018b2bacbe8918aaf1a", - "is_secret": false, - "is_verified": false, - "line_number": 19, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/ee288b094280_add_auth_query_params_to_gateways.py": [ - { - "hashed_secret": "48ffbad96aa9c2b33f9486f5a3c2108198acb518", - "is_secret": false, - "is_verified": false, - "line_number": 21, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/f1a2b3c4d5e6_add_auth_query_params_to_a2a_agents.py": [ - { - "hashed_secret": "2d40811ded1703d949aca3c40d77175a5c568c05", - "is_secret": false, - "is_verified": false, - "line_number": 20, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/f3a3a3d901b8_remove_gateway_url_unique_constraint.py": [ - { - "hashed_secret": "0c1c30eba610778ef5e68a58883a052733a22bd4", - "is_secret": false, - "is_verified": false, - "line_number": 24, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "74cf584eb4fffdfa472c808000adb71841786f3e", - "is_secret": false, - "is_verified": false, - "line_number": 25, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/f8c9d3e2a1b4_add_oauth_config_to_gateways.py": [ - { - "hashed_secret": "3333a8285ac54459f46f2ef561184275586dd1a7", - "is_secret": false, - "is_verified": false, - "line_number": 22, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/g1a2b3c4d5e6_add_pagination_indexes.py": [ - { - "hashed_secret": "81d89c741bbf0978e92cb9852870242cc88c7db2", - "is_secret": false, - "is_verified": false, - "line_number": 19, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/h2b3c4d5e6f7_add_oauth_config_to_a2a_agents.py": [ - { - "hashed_secret": "83af00f2e79329f549c1438dec118e0cba4ad111", - "is_secret": false, - "is_verified": false, - "line_number": 23, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/i3c4d5e6f7g8_add_observability_performance_indexes.py": [ - { - "hashed_secret": "9d88d7eece452a76b2955ecfbb251f23cf7b7905", - "is_secret": false, - "is_verified": false, - "line_number": 18, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/k5e6f7g8h9i0_add_structured_logging_tables.py": [ - { - "hashed_secret": "c81482140a7bf22e957d89f358479e90adef80dc", - "is_secret": false, - "is_verified": false, - "line_number": 16, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/u5f6g7h8i9j0_add_provider_metadata_to_sso_providers.py": [ - { - "hashed_secret": "e6ec1410341b496819aeb85882509889c33017af", - "is_secret": false, - "is_verified": false, - "line_number": 28, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/v1a2b3c4d5e6_assign_default_viewer_role_to_existing_users.py": [ - { - "hashed_secret": "c052f9f7b5b94886b423384e4938ed8388cd77b4", - "is_secret": false, - "is_verified": false, - "line_number": 39, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/alembic/versions/z1a2b3c4d5e6_add_password_change_required.py": [ - { - "hashed_secret": "36b0556065adc2fdd8a15b7ffac028a043c34595", - "is_secret": false, - "is_verified": false, - "line_number": 16, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/cache/session_registry.py": [ - { - "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", - "is_secret": false, - "is_verified": false, - "line_number": 152, - "type": "Basic Auth Credentials", - "verified_result": null - } - ], - "mcpgateway/common/validators.py": [ - { - "hashed_secret": "c377074d6473f35a91001981355da793dc808ffd", - "is_secret": false, - "is_verified": false, - "line_number": 700, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", - "is_secret": false, - "is_verified": false, - "line_number": 1102, - "type": "Basic Auth Credentials", - "verified_result": null - } - ], - "mcpgateway/config.py": [ - { - "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", - "is_secret": false, - "is_verified": false, - "line_number": 221, - "type": "Basic Auth Credentials", - "verified_result": null - }, - { - "hashed_secret": "ff37a98a9963d347e9749a5c1b3936a4a245a6ff", - "is_secret": false, - "is_verified": false, - "line_number": 2108, - "type": "Secret Keyword", - "verified_result": null - } - ], - "mcpgateway/db.py": [ - { - "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", - "is_secret": false, - "is_verified": false, - "line_number": 80, - "type": "Basic Auth Credentials", - "verified_result": null - } - ], - "mcpgateway/llm_provider_configs.py": [ - { - "hashed_secret": "25910f981e85ca04baf359199dd0bd4a3ae738b6", - "is_secret": false, - "is_verified": false, - "line_number": 308, - "type": "AWS Access Key", - "verified_result": null - }, - { - "hashed_secret": "d70eab08607a4d05faa2d0d6647206599e9abc65", - "is_secret": false, - "is_verified": false, - "line_number": 316, - "type": "Base64 High Entropy String", - "verified_result": null - } - ], - "mcpgateway/middleware/request_logging_middleware.py": [ - { - "hashed_secret": "e9fe51f94eadabf54dbf2fbbd57188b9abee436e", - "is_secret": false, - "is_verified": false, - "line_number": 40, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", - "is_secret": false, - "is_verified": false, - "line_number": 171, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "1073ab6cda4b991cd29f9e83a307f34004ae9327", - "is_secret": false, - "is_verified": false, - "line_number": 177, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "d4fe581561f18ee5006254a7e53f53cbed780bc2", - "is_secret": false, - "is_verified": false, - "line_number": 259, - "type": "Secret Keyword", - "verified_result": null - } - ], - "mcpgateway/plugins/framework/external/grpc/tls_utils.py": [ - { - "hashed_secret": "623e76c36aa2a886542011e28412cc761d7ceb01", - "is_secret": false, - "is_verified": false, - "line_number": 132, - "type": "Secret Keyword", - "verified_result": null - } - ], - "mcpgateway/plugins/framework/hooks/policies.py": [ - { - "hashed_secret": "e812ba8d00b270ef3502bb53ceb31e8c5188f14e", - "is_secret": false, - "is_verified": false, - "line_number": 98, - "type": "Secret Keyword", - "verified_result": null - } - ], - "mcpgateway/plugins/framework/validators.py": [ - { - "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", - "is_secret": false, - "is_verified": false, - "line_number": 115, - "type": "Basic Auth Credentials", - "verified_result": null - } - ], - "mcpgateway/routers/auth.py": [ - { - "hashed_secret": "aa1a82fe15c74459f1261961b07ae924e2b94ab2", - "is_secret": false, - "is_verified": false, - "line_number": 150, - "type": "Secret Keyword", - "verified_result": null - } - ], - "mcpgateway/routers/email_auth.py": [ - { - "hashed_secret": "6993a3fd94a012ab50fb6b9e97ec238310f0b177", - "is_secret": false, - "is_verified": false, - "line_number": 397, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "52dcc83ec1e54426ad58a64854d1eb8d5f5d9685", - "is_secret": false, - "is_verified": false, - "line_number": 398, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "a616a64c0fbc30f12287d0f24f3b90dd2e6a206e", - "is_secret": false, - "is_verified": false, - "line_number": 680, - "type": "Secret Keyword", - "verified_result": null - } - ], - "mcpgateway/routers/oauth_router.py": [ - { - "hashed_secret": "d3ecb0d890368d7659ee54010045b835dacb8efe", - "is_secret": false, - "is_verified": false, - "line_number": 610, - "type": "Secret Keyword", - "verified_result": null - } - ], - "mcpgateway/schemas.py": [ - { - "hashed_secret": "c377074d6473f35a91001981355da793dc808ffd", - "is_secret": false, - "is_verified": false, - "line_number": 4111, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "6367c48dd193d56ea7b0baad25b19455e529f5ee", - "is_secret": false, - "is_verified": false, - "line_number": 5224, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", - "is_secret": false, - "is_verified": false, - "line_number": 5388, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "f42a3fabe1e9bed059d727f47eb752e3aa61b977", - "is_secret": false, - "is_verified": false, - "line_number": 5445, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "b85788b459aa4d67e1070930dae6d0827756aadb", - "is_secret": false, - "is_verified": false, - "line_number": 5483, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "52dcc83ec1e54426ad58a64854d1eb8d5f5d9685", - "is_secret": false, - "is_verified": false, - "line_number": 5484, - "type": "Secret Keyword", - "verified_result": null - } - ], - "mcpgateway/scripts/validate_env.py": [ - { - "hashed_secret": "b0fb9e3dbf6beca57a1e203dddab80910598ec3d", - "is_secret": false, - "is_verified": false, - "line_number": 112, - "type": "Base64 High Entropy String", - "verified_result": null - } - ], - "mcpgateway/services/argon2_service.py": [ - { - "hashed_secret": "f13733f6dd9f1ed3118e2da31428c71eab5ffd99", - "is_secret": false, - "is_verified": false, - "line_number": 50, - "type": "Secret Keyword", - "verified_result": null - } - ], - "mcpgateway/services/email_auth_service.py": [ - { - "hashed_secret": "f42a3fabe1e9bed059d727f47eb752e3aa61b977", - "is_secret": false, - "is_verified": false, - "line_number": 573, - "type": "Secret Keyword", - "verified_result": null - } - ], - "mcpgateway/services/mcp_client_chat_service.py": [ - { - "hashed_secret": "11fa7c37d697f30e6aee828b4426a10f83ab2380", - "is_secret": false, - "is_verified": false, - "line_number": 526, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "94b778a67d72bc0e8fa5838e7fa48b37739a9e30", - "is_secret": false, - "is_verified": false, - "line_number": 624, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "6d9c68c603e465077bdd49c62347fe54717f83a3", - "is_secret": false, - "is_verified": false, - "line_number": 719, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "c7a8c334eef5d1749fface7d42c66f9ae5e8cf36", - "is_secret": false, - "is_verified": false, - "line_number": 1157, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "a62f2225bf70bfaccbc7f1ef2a397836717377de", - "is_secret": false, - "is_verified": false, - "line_number": 1796, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", - "is_secret": false, - "is_verified": false, - "line_number": 1809, - "type": "Secret Keyword", - "verified_result": null - } - ], - "mcpgateway/services/oauth_manager.py": [ - { - "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", - "is_secret": false, - "is_verified": false, - "line_number": 105, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "819ef87051ee2837fefbb462d846b8d282d3b756", - "is_secret": false, - "is_verified": false, - "line_number": 108, - "type": "Secret Keyword", - "verified_result": null - } - ], - "mcpgateway/services/observability_service.py": [ - { - "hashed_secret": "0a24796d4c71ce722a92f450f69dc36c60b21de4", - "is_secret": false, - "is_verified": false, - "line_number": 165, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8750a512f22c55598175aee8790ae2470ec88d16", - "is_secret": false, - "is_verified": false, - "line_number": 165, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/services/resource_service.py": [ - { - "hashed_secret": "a10b98d7340036e9c8c301704f623eddd733cc1a", - "is_secret": false, - "is_verified": false, - "line_number": 316, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "718cbcc5a4207c0d5f38e3a309bdba17cb0074b7", - "is_secret": false, - "is_verified": false, - "line_number": 3290, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/services/sso_service.py": [ - { - "hashed_secret": "920a25ef686c4f7ca6ad23dd109d3ad653161832", - "is_secret": false, - "is_verified": false, - "line_number": 780, - "type": "Secret Keyword", - "verified_result": null - } - ], - "mcpgateway/services/support_bundle_service.py": [ - { - "hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8", - "is_secret": false, - "is_verified": false, - "line_number": 157, - "type": "Basic Auth Credentials", - "verified_result": null - } - ], - "mcpgateway/sri_hashes.json": [ - { - "hashed_secret": "1fd5e6bd76663b16aee9602fa54dff982f0c0513", - "is_secret": false, - "is_verified": false, - "line_number": 2, - "type": "Base64 High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9412580d859a192c1915cdc58de2e145d249ef6a", - "is_secret": false, - "is_verified": false, - "line_number": 3, - "type": "Base64 High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d8797430a133d1bc558e4ef4ba7110b28b67abfc", - "is_secret": false, - "is_verified": false, - "line_number": 4, - "type": "Base64 High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "6af61c57e8d8bcb6af6de09cfa7fb75c67785a48", - "is_secret": false, - "is_verified": false, - "line_number": 5, - "type": "Base64 High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d58b463fcf8838bc9fec1cd217fc96dd6b2c02e8", - "is_secret": false, - "is_verified": false, - "line_number": 6, - "type": "Base64 High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b5fc888b800befb7f0afd247b85cf1e00a1c617d", - "is_secret": false, - "is_verified": false, - "line_number": 7, - "type": "Base64 High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a322631affd826e32a9fe4c1edd155526253a368", - "is_secret": false, - "is_verified": false, - "line_number": 8, - "type": "Base64 High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "5936242a1ad0d29897b3d7d8ca1075298653775c", - "is_secret": false, - "is_verified": false, - "line_number": 9, - "type": "Base64 High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f7d3016ba11801d588c442e1a09f00897ff182e3", - "is_secret": false, - "is_verified": false, - "line_number": 10, - "type": "Base64 High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "57004d905b477a36f855d6a639c52b11c8385d1d", - "is_secret": false, - "is_verified": false, - "line_number": 11, - "type": "Base64 High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d7abf8d576f5ea125ba6fd308f28d8654f523a8c", - "is_secret": false, - "is_verified": false, - "line_number": 12, - "type": "Base64 High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "0c82d92406ec9e3e50eedb724fca9a07b131f966", - "is_secret": false, - "is_verified": false, - "line_number": 13, - "type": "Base64 High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "724ed978fa8c0af12c9e23a521acda5f9c22fd74", - "is_secret": false, - "is_verified": false, - "line_number": 14, - "type": "Base64 High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f8852355a4d22965e4c3640c9c3c6e4039a83fc2", - "is_secret": false, - "is_verified": false, - "line_number": 15, - "type": "Base64 High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e40ac0dfd74f63f996a157f0c79c9ee0b9fbc951", - "is_secret": false, - "is_verified": false, - "line_number": 16, - "type": "Base64 High Entropy String", - "verified_result": null - } - ], - "mcpgateway/templates/admin.html": [ - { - "hashed_secret": "b4e44716dbbf57be3dae2f819d96795a85d06652", - "is_secret": false, - "is_verified": false, - "line_number": 12461, - "type": "Secret Keyword", - "verified_result": null - } - ], - "mcpgateway/templates/change-password-required.html": [ - { - "hashed_secret": "6947818ac409551f11fbaa78f0ea6391960aa5b8", - "is_secret": false, - "is_verified": false, - "line_number": 582, - "type": "Secret Keyword", - "verified_result": null - } - ], - "mcpgateway/templates/mcp_registry_partial.html": [ - { - "hashed_secret": "d4c3d66fd0c38547a3c7a4c6bdc29c36911bc030", - "is_secret": false, - "is_verified": false, - "line_number": 667, - "type": "Secret Keyword", - "verified_result": null - } - ], - "mcpgateway/toolops/toolops_altk_service.py": [ - { - "hashed_secret": "772fabfec4c6f068c046e516ab0b946909697d32", - "is_secret": false, - "is_verified": false, - "line_number": 283, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/toolops/utils/db_util.py": [ - { - "hashed_secret": "e2eea31cf61d791c922b110554ce1f0a6ed046e9", - "is_secret": false, - "is_verified": false, - "line_number": 178, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "mcpgateway/toolops/utils/llm_util.py": [ - { - "hashed_secret": "e6a51c968d41fd1d3437e6b7d9101e527ee1b83f", - "is_secret": false, - "is_verified": false, - "line_number": 67, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "9c4006403b5388cd34ccfe3ed1b1d3e53cf35700", - "is_secret": false, - "is_verified": false, - "line_number": 81, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "d01e71e4c47a4022acd25c74bffedd2641a60c70", - "is_secret": false, - "is_verified": false, - "line_number": 96, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "39fc8e4accd6a3563c4be159e507195ad3d7274c", - "is_secret": false, - "is_verified": false, - "line_number": 106, - "type": "Secret Keyword", - "verified_result": null - } - ], - "mcpgateway/tools/builder/common.py": [ - { - "hashed_secret": "2a1027e8abaef8567159c09ddca81604fb1ba8c0", - "is_secret": false, - "is_verified": false, - "line_number": 428, - "type": "Basic Auth Credentials", - "verified_result": null - } - ], - "mcpgateway/tools/builder/templates/compose/docker-compose.yaml.j2": [ - { - "hashed_secret": "bd0160c2cf35d950843c88f3be2b9412ed71f485", - "is_secret": false, - "is_verified": false, - "line_number": 44, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "4d4acd9b084d13f5fdb23807d857e1c48a1cfd0f", - "is_secret": false, - "is_verified": false, - "line_number": 164, - "type": "Secret Keyword", - "verified_result": null - } - ], - "mcpgateway/translate_header_utils.py": [ - { - "hashed_secret": "e48c47f1431afd3bb030deea266b4309e2228c5b", - "is_secret": false, - "is_verified": false, - "line_number": 190, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "a62f2225bf70bfaccbc7f1ef2a397836717377de", - "is_secret": false, - "is_verified": false, - "line_number": 262, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "ee977806d7286510da8b9a7492ba58e2484c0ecc", - "is_secret": false, - "is_verified": false, - "line_number": 317, - "type": "Secret Keyword", - "verified_result": null - } - ], - "mcpgateway/utils/create_jwt_token.py": [ - { - "hashed_secret": "2fcd9b91b955db1627d18720cb7395ef2893a497", - "is_secret": false, - "is_verified": false, - "line_number": 36, - "type": "Secret Keyword", - "verified_result": null - } - ], - "mcpgateway/utils/generate_keys.py": [ - { - "hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9", - "is_secret": false, - "is_verified": false, - "line_number": 89, - "type": "Private Key", - "verified_result": null - } - ], - "mcpgateway/utils/services_auth.py": [ - { - "hashed_secret": "a2fe5a0cdcbf2e84dbf868a11c5f2ca79f2d7b8d", - "is_secret": false, - "is_verified": false, - "line_number": 218, - "type": "Secret Keyword", - "verified_result": null - } - ], - "mcpgateway/utils/sso_bootstrap.py": [ - { - "hashed_secret": "920a25ef686c4f7ca6ad23dd109d3ad653161832", - "is_secret": false, - "is_verified": false, - "line_number": 43, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "b44bf05d644c15c4d84f78771de011e3cce924c0", - "is_secret": false, - "is_verified": false, - "line_number": 60, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "999a3419d9959d3c39b11dcc67d79c7888b4b765", - "is_secret": false, - "is_verified": false, - "line_number": 72, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "bacac952b5cb942687d38a9eda6531d570f88b22", - "is_secret": false, - "is_verified": false, - "line_number": 85, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "cd1ecfcd67c8b85800a483d77e550d36727e8925", - "is_secret": false, - "is_verified": false, - "line_number": 99, - "type": "Secret Keyword", - "verified_result": null - } - ], - "mcpgateway/utils/url_auth.py": [ - { - "hashed_secret": "6367c48dd193d56ea7b0baad25b19455e529f5ee", - "is_secret": false, - "is_verified": false, - "line_number": 68, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", - "is_secret": false, - "is_verified": false, - "line_number": 130, - "type": "Basic Auth Credentials", - "verified_result": null - }, - { - "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", - "is_secret": false, - "is_verified": false, - "line_number": 136, - "type": "Basic Auth Credentials", - "verified_result": null - }, - { - "hashed_secret": "b02dff0ec9d24823e77c27c281a852247896b86d", - "is_secret": false, - "is_verified": false, - "line_number": 138, - "type": "Basic Auth Credentials", - "verified_result": null - }, - { - "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", - "is_secret": false, - "is_verified": false, - "line_number": 203, - "type": "Secret Keyword", - "verified_result": null - } - ], - "mcpgateway/utils/verify_credentials.py": [ - { - "hashed_secret": "a4b48a81cdab1e1a5dd37907d6c85ca1c61ddc7c", - "is_secret": false, - "is_verified": false, - "line_number": 564, - "type": "Secret Keyword", - "verified_result": null - } - ], - "mcpgateway/version.py": [ - { - "hashed_secret": "9addbf544119efa4a64223b649750a510f0d463f", - "is_secret": false, - "is_verified": false, - "line_number": 518, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "9addbf544119efa4a64223b649750a510f0d463f", - "is_secret": false, - "is_verified": false, - "line_number": 580, - "type": "Basic Auth Credentials", - "verified_result": null - } - ], - "plugin_templates/external/{{cookiecutter.plugin_slug}}/.env.template": [ - { - "hashed_secret": "623e76c36aa2a886542011e28412cc761d7ceb01", - "is_secret": false, - "is_verified": false, - "line_number": 118, - "type": "Secret Keyword", - "verified_result": null - } - ], - "plugins/content_moderation/README.md": [ - { - "hashed_secret": "c586890f1a2e6a43c2611ec983782d53141333de", - "is_secret": false, - "is_verified": false, - "line_number": 168, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "1d3b1295c282649864a65fe3bd1631bcf7499ef5", - "is_secret": false, - "is_verified": false, - "line_number": 174, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "83542070ee8238c5ad6484b7824016bd2d2fb32e", - "is_secret": false, - "is_verified": false, - "line_number": 186, - "type": "Secret Keyword", - "verified_result": null - } - ], - "plugins/content_moderation/TESTING.md": [ - { - "hashed_secret": "11fa7c37d697f30e6aee828b4426a10f83ab2380", - "is_secret": false, - "is_verified": false, - "line_number": 204, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "1d3b1295c282649864a65fe3bd1631bcf7499ef5", - "is_secret": false, - "is_verified": false, - "line_number": 240, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "3bee216ebc256d692260fc3adc765050508fef5e", - "is_secret": false, - "is_verified": false, - "line_number": 428, - "type": "Secret Keyword", - "verified_result": null - } - ], - "plugins/encoded_exfil_detection/README.md": [ - { - "hashed_secret": "f7f877c24a4ebef314009a2e79314797bae91bb6", - "is_secret": false, - "is_verified": false, - "line_number": 149, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "5f2fe4f03b2314fa3a217f1e1f0ab2e3f5b1b49f", - "is_secret": false, - "is_verified": false, - "line_number": 178, - "type": "Secret Keyword", - "verified_result": null - } - ], - "plugins/examples/custom_auth_example/README.md": [ - { - "hashed_secret": "423529858c0ba74fbbe141bce50b20f3c0d3b9f6", - "is_secret": false, - "is_verified": false, - "line_number": 70, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "31b38ab9a06eec4e1a194a5e23c83fbc6643c20a", - "is_secret": false, - "is_verified": false, - "line_number": 104, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "e94104ce07e03bb7892c943c21235d8f91793c73", - "is_secret": false, - "is_verified": false, - "line_number": 196, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "2bbebb59c0430ebfe54c81f73e3bb99c9e704341", - "is_secret": false, - "is_verified": false, - "line_number": 295, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "ed65c049bb2f78ee4f703b2158ba9cc6ea31fb7e", - "is_secret": false, - "is_verified": false, - "line_number": 295, - "type": "Secret Keyword", - "verified_result": null - } - ], - "plugins/examples/custom_auth_example/custom_auth.py": [ - { - "hashed_secret": "79acc7201378a3d075db4c3b716187b65d4369ba", - "is_secret": false, - "is_verified": false, - "line_number": 260, - "type": "Secret Keyword", - "verified_result": null - } - ], - "plugins/examples/simple_token_auth/simple_token_auth.py": [ - { - "hashed_secret": "8a9e9e42f8e154a255c1455267654ab4ec13528b", - "is_secret": false, - "is_verified": false, - "line_number": 157, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "3d75cbdb604907851e109e4ced7161c178648532", - "is_secret": false, - "is_verified": false, - "line_number": 166, - "type": "Secret Keyword", - "verified_result": null - } - ], - "plugins/external/cedar/resources/runtime/config.yaml": [ - { - "hashed_secret": "ddb2c93ebcf6098f6ccb08807ed66e02934248a5", - "is_secret": false, - "is_verified": false, - "line_number": 8, - "type": "Secret Keyword", - "verified_result": null - } - ], - "plugins/external/llmguard/README.md": [ - { - "hashed_secret": "50c8af90ac5a0b327517a411650008199a358c96", - "is_secret": false, - "is_verified": false, - "line_number": 308, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "plugins/external/llmguard/resources/plugins/config.yaml": [ - { - "hashed_secret": "50c8af90ac5a0b327517a411650008199a358c96", - "is_secret": false, - "is_verified": false, - "line_number": 14, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "plugins/external/llmguard/resources/runtime/config.yaml": [ - { - "hashed_secret": "ddb2c93ebcf6098f6ccb08807ed66e02934248a5", - "is_secret": false, - "is_verified": false, - "line_number": 8, - "type": "Secret Keyword", - "verified_result": null - } - ], - "plugins/external/opa/README.md": [ - { - "hashed_secret": "a5d80863d8ca51c72e6cb7799ac9657eaddb3b1d", - "is_secret": false, - "is_verified": false, - "line_number": 235, - "type": "Base64 High Entropy String", - "verified_result": null - } - ], - "plugins/external/opa/resources/runtime/config.yaml": [ - { - "hashed_secret": "ddb2c93ebcf6098f6ccb08807ed66e02934248a5", - "is_secret": false, - "is_verified": false, - "line_number": 8, - "type": "Secret Keyword", - "verified_result": null - } - ], - "plugins/pii_filter/README.md": [ - { - "hashed_secret": "25910f981e85ca04baf359199dd0bd4a3ae738b6", - "is_secret": false, - "is_verified": false, - "line_number": 344, - "type": "AWS Access Key", - "verified_result": null - } - ], - "plugins/resource_filter/README.md": [ - { - "hashed_secret": "048fe8f760ed90d8058cacc2da41ed7a7eb76669", - "is_secret": false, - "is_verified": false, - "line_number": 153, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "48574e7efc2af452e5187ab0f9381560f17577be", - "is_secret": false, - "is_verified": false, - "line_number": 156, - "type": "Secret Keyword", - "verified_result": null - } - ], - "plugins/vault/README.md": [ - { - "hashed_secret": "b60d121b438a380c343d5ec3c2037564b82ffef3", - "is_secret": false, - "is_verified": false, - "line_number": 181, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "83722c5a3bcef30aa1c6014bedd0ddd5b4107514", - "is_secret": false, - "is_verified": false, - "line_number": 194, - "type": "Secret Keyword", - "verified_result": null - } - ], - "plugins/webhook_notification/TESTING.md": [ - { - "hashed_secret": "3acfb2c2b433c0ea7ff107e33df91b18e52f960f", - "is_secret": false, - "is_verified": false, - "line_number": 61, - "type": "Secret Keyword", - "verified_result": null - } - ], - "plugins_rust/encoded_exfil_detection/Cargo.lock": [ - { - "hashed_secret": "e0f930ce4dc6ee91bd9c13f93bdd411a875847ad", - "is_secret": false, - "is_verified": false, - "line_number": 9, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e4eb0d22814fd1612e292742063af598468d65ea", - "is_secret": false, - "is_verified": false, - "line_number": 18, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "0c2b06b30faf95e40c75aaaefa957d835c1df392", - "is_secret": false, - "is_verified": false, - "line_number": 27, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b660bfa30c205b9b6687cd7c0fec21dd718dd211", - "is_secret": false, - "is_verified": false, - "line_number": 36, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a0b32b6f12e419acbd7b54d25cfcb35ea7d88b39", - "is_secret": false, - "is_verified": false, - "line_number": 42, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f31c8be2a83d8786ff5ea96f532898a2d296dd95", - "is_secret": false, - "is_verified": false, - "line_number": 48, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ece49906f7a72ed3cfb012e5e2e1209e1b226172", - "is_secret": false, - "is_verified": false, - "line_number": 54, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b25a21f1a0e2fc65dc90424e07d7210587e8b296", - "is_secret": false, - "is_verified": false, - "line_number": 60, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "024de6406eb14fab83f73c7aba00b007b6369930", - "is_secret": false, - "is_verified": false, - "line_number": 66, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "6b72376f39b229e9e6b4be8fe9c459fe555929ea", - "is_secret": false, - "is_verified": false, - "line_number": 72, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a5f1914a4edf604fca63fe9c60cb62601b7c701c", - "is_secret": false, - "is_verified": false, - "line_number": 78, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "eac7344fb138e2bc70ec74dd15a52fe0c253887a", - "is_secret": false, - "is_verified": false, - "line_number": 88, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4292dceca17415f83418a3909507b7c29737d284", - "is_secret": false, - "is_verified": false, - "line_number": 94, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ac8b8c7acbe0e66015ad3a9344c3cead1643ae19", - "is_secret": false, - "is_verified": false, - "line_number": 107, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2a5dd330cc6af22e5e259877e4a6342ed6395f2f", - "is_secret": false, - "is_verified": false, - "line_number": 118, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "84a7dcddd87e289faa3d086dc9a6ac50292f8ee2", - "is_secret": false, - "is_verified": false, - "line_number": 124, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b0571ccc0a2237bc0fc35d60e94a9503d51b36dd", - "is_secret": false, - "is_verified": false, - "line_number": 134, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "bc6e2ea457c17caef176812bc1d770066c3b8028", - "is_secret": false, - "is_verified": false, - "line_number": 143, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "710066c385eadd0091f067e0a8e2573ac53d5f4b", - "is_secret": false, - "is_verified": false, - "line_number": 153, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "aea69a0db4b6fb9c1801ecd9266c98e51a5b6515", - "is_secret": false, - "is_verified": false, - "line_number": 159, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "43cc136a251b358a7a64968300d250877dbf86bd", - "is_secret": false, - "is_verified": false, - "line_number": 165, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9feb0877f4bbf6117f628b1362db082168af749c", - "is_secret": false, - "is_verified": false, - "line_number": 190, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "68a15e87a6b9806b9214862840726ef1acc2e5dd", - "is_secret": false, - "is_verified": false, - "line_number": 200, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "37296779892fa772bc497cc53fa15fb2ced95b84", - "is_secret": false, - "is_verified": false, - "line_number": 210, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "076f7448f7767df7a0af32c25c88174167b2b870", - "is_secret": false, - "is_verified": false, - "line_number": 219, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "562e55d49724eaac4cff1b62be4137d8f5292a72", - "is_secret": false, - "is_verified": false, - "line_number": 225, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8cb759d32a886afc6561de73e3102dcdc2cba219", - "is_secret": false, - "is_verified": false, - "line_number": 231, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4af1b31ab033275a37cc32187a52e4c46cf762db", - "is_secret": false, - "is_verified": false, - "line_number": 240, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e3423649a06049806d8883193ffa13f6488898ba", - "is_secret": false, - "is_verified": false, - "line_number": 258, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4d2c6fb73af42ea62f965535a82e83df5411d419", - "is_secret": false, - "is_verified": false, - "line_number": 264, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "762fc498e829264bd84320c53f9bd6c8eb445b8e", - "is_secret": false, - "is_verified": false, - "line_number": 270, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "fb738e4a202642582d5a12f41a884e26001c4fa6", - "is_secret": false, - "is_verified": false, - "line_number": 279, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8c73d56ba41f0eeb296413affaa812431de0bd07", - "is_secret": false, - "is_verified": false, - "line_number": 290, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f3d3c26ed24a648081ced9182bb1b19f7d78e68d", - "is_secret": false, - "is_verified": false, - "line_number": 301, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "1d591d1374352e720051c9d3f7b1e2c5557d5fd6", - "is_secret": false, - "is_verified": false, - "line_number": 307, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ace4c15ef5d5b8c769f9d24cf4bf1de85fecb2c7", - "is_secret": false, - "is_verified": false, - "line_number": 313, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9bfd048ce5ceb106ba035489cea34b77d79814e4", - "is_secret": false, - "is_verified": false, - "line_number": 328, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4faeea7fc4d0ed985b094fe85a90aab7fc3d7ae8", - "is_secret": false, - "is_verified": false, - "line_number": 337, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2518cc4854c30b1401b7bef0dc542237d5acce4c", - "is_secret": false, - "is_verified": false, - "line_number": 347, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b2104d534fcbba7dd524595af7e52e29da1eca68", - "is_secret": false, - "is_verified": false, - "line_number": 356, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "aadc0453d2d3fdc05093cc42f8950f5445782e13", - "is_secret": false, - "is_verified": false, - "line_number": 368, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d2e033fe193c9a2aa937c12c6b496692a3bfd0f3", - "is_secret": false, - "is_verified": false, - "line_number": 377, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4c35f1105fef7dd05206e5c580220cae7fb0d31b", - "is_secret": false, - "is_verified": false, - "line_number": 386, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2bbb23c710243f6c2dd0da44f0bce1512d40aeb0", - "is_secret": false, - "is_verified": false, - "line_number": 395, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f07469cb2b99055bd011f347860438b0957c4b5b", - "is_secret": false, - "is_verified": false, - "line_number": 401, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "0ff83af8f867301e963f7ad167917125f83e8b40", - "is_secret": false, - "is_verified": false, - "line_number": 411, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "1a5c1aafe91ad50de9b57d58c892732234dd2618", - "is_secret": false, - "is_verified": false, - "line_number": 417, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2d70a27f9bdfdb0ebea3fa79f7962fa957bd11fa", - "is_secret": false, - "is_verified": false, - "line_number": 423, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a32cf693a7717063dad28f208cc038d14a3e847c", - "is_secret": false, - "is_verified": false, - "line_number": 429, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "db0c5903c40f2dcf6cc3179aa01c0026d18b3061", - "is_secret": false, - "is_verified": false, - "line_number": 435, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "52174d432f72b603ab3aeb65e046d8842f64eba8", - "is_secret": false, - "is_verified": false, - "line_number": 445, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e936de0f0fb5cb3b98a0a1fd5f06d47d714a948b", - "is_secret": false, - "is_verified": false, - "line_number": 451, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9f07d77e5781c78d5ccbce0d869c20816ce3211d", - "is_secret": false, - "is_verified": false, - "line_number": 466, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "3eb645bfb98952d218d44f82f285b49ff2e34dde", - "is_secret": false, - "is_verified": false, - "line_number": 476, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "0a549d859d0064d99bd64aa5226d0588e42e1f32", - "is_secret": false, - "is_verified": false, - "line_number": 485, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "12061f2ebc56b7c0d1167ed4ead1f4570bf89831", - "is_secret": false, - "is_verified": false, - "line_number": 491, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ebbea2a8b6a37ded68465f81f315d4a23c27a8e8", - "is_secret": false, - "is_verified": false, - "line_number": 500, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "1da2731a9796fcb269eb9b244d1e98e23618af6e", - "is_secret": false, - "is_verified": false, - "line_number": 509, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "3558448508502aa5a344f39c803ec2665ee9e81b", - "is_secret": false, - "is_verified": false, - "line_number": 525, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "51d39c0f11bd886e295a5a60dbec0332b6900c4c", - "is_secret": false, - "is_verified": false, - "line_number": 531, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d13f6ae109bf8e970b5d7d6a5c160a09f0642448", - "is_secret": false, - "is_verified": false, - "line_number": 537, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f6f33d498bbbe78966c6d4cc9e02eff57dccd290", - "is_secret": false, - "is_verified": false, - "line_number": 546, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9fbf26ca6c2385c79c99e7cfc059be4768610da9", - "is_secret": false, - "is_verified": false, - "line_number": 556, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "1db3bfc3973b7b1848de6d62826146ea6ba8b79b", - "is_secret": false, - "is_verified": false, - "line_number": 565, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a83eec6a7f047ef3b54c6471da4fee13861fbc74", - "is_secret": false, - "is_verified": false, - "line_number": 575, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "09bd80f5c95312a3d6aef8957b831a6679e729ac", - "is_secret": false, - "is_verified": false, - "line_number": 585, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "7e21427077e475ce5749e288815b8c8e9fda21c8", - "is_secret": false, - "is_verified": false, - "line_number": 594, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "072ff15e3bdf3b38a4b02d9e3e0ef4d937e18e15", - "is_secret": false, - "is_verified": false, - "line_number": 607, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b7d9cbf27e5b10a62ae9848be4b6fbbbe93491e1", - "is_secret": false, - "is_verified": false, - "line_number": 613, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "32cad93d535d24a043ec493dcb579e8744a33d5b", - "is_secret": false, - "is_verified": false, - "line_number": 622, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "125cc8a1a532f3c912919ef250bb42eee5a94e07", - "is_secret": false, - "is_verified": false, - "line_number": 628, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "56397d9a520742406189d71ac27362811f4eb3f4", - "is_secret": false, - "is_verified": false, - "line_number": 637, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "978bcf06dc12b74b1579b446ae082265db1358f6", - "is_secret": false, - "is_verified": false, - "line_number": 643, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "6538f9a3e2204f918d5719f17a23367f4d2105c7", - "is_secret": false, - "is_verified": false, - "line_number": 652, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "50feffdb4b1d1ce1f1432a2628ae3cbe1dca5849", - "is_secret": false, - "is_verified": false, - "line_number": 661, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "397e6f1ae7f26850afeb84f8fc86d442a23a5d39", - "is_secret": false, - "is_verified": false, - "line_number": 675, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "13095d536a4a53c8d1e3461be1259680963bb20c", - "is_secret": false, - "is_verified": false, - "line_number": 684, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "5477445d0fbadc7dffdaed724591de709ef47edf", - "is_secret": false, - "is_verified": false, - "line_number": 694, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "db5edc57fbb1b32f713e854ade035e9e48c39a09", - "is_secret": false, - "is_verified": false, - "line_number": 706, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e84cc493fd67427b615b414feca76a450c7840c3", - "is_secret": false, - "is_verified": false, - "line_number": 719, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "60dcabffca2924f2c9777b16acdcdd113632d1dd", - "is_secret": false, - "is_verified": false, - "line_number": 745, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9a2cdf6585922eea3346605d503aa1370139e781", - "is_secret": false, - "is_verified": false, - "line_number": 759, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "47aeac2fa4acd0b3b040c9927a26128bf0c029af", - "is_secret": false, - "is_verified": false, - "line_number": 768, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ef2e592f89cb7fff6bccacece98f099ca10add8f", - "is_secret": false, - "is_verified": false, - "line_number": 779, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "def835c5f3d29ef5143e04e29a61cd85a81fe09e", - "is_secret": false, - "is_verified": false, - "line_number": 789, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a12225190f4c63e09c71dac208d1979b10e19e55", - "is_secret": false, - "is_verified": false, - "line_number": 798, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "c6db2ba51720f54d6b7336f1eb4d8032d96c76ed", - "is_secret": false, - "is_verified": false, - "line_number": 804, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d8f23b8e5e11045f82a31210713d308de8c1a83e", - "is_secret": false, - "is_verified": false, - "line_number": 814, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "0675b70f52618de044d1d1703b6c8849b407c83a", - "is_secret": false, - "is_verified": false, - "line_number": 824, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "c63addae2503bfe26923f0460158bb2fc674b928", - "is_secret": false, - "is_verified": false, - "line_number": 836, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "0c6e3a84ec63c609153315d01f022e862468c93d", - "is_secret": false, - "is_verified": false, - "line_number": 847, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "7595306f5da32ddff19f11930cfd7e021bca6a62", - "is_secret": false, - "is_verified": false, - "line_number": 853, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d2aacc8c22ee3e26d7a6f5d24bf528c87202c5f0", - "is_secret": false, - "is_verified": false, - "line_number": 859, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "bbe494c4c6d883d257069617aadba814a0b7af42", - "is_secret": false, - "is_verified": false, - "line_number": 865, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e045cb9f89b83706ea83457e157baa07f4da23d1", - "is_secret": false, - "is_verified": false, - "line_number": 877, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8b334846415d5595c15bebc8efe4ccd59c4a6668", - "is_secret": false, - "is_verified": false, - "line_number": 901, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e5eb894a4ae8142b70b7384d9ab7648abd1cc138", - "is_secret": false, - "is_verified": false, - "line_number": 912, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9daac1b266d3875ecff4a4b7932e73d63f51718f", - "is_secret": false, - "is_verified": false, - "line_number": 922, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d4b5e511e6b010068fe8fc17a580b24af40fca83", - "is_secret": false, - "is_verified": false, - "line_number": 928, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "dd30e69b06fc9e7153b91d6152b47abee06bc76e", - "is_secret": false, - "is_verified": false, - "line_number": 937, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "edea2c54c8d390fdfff6535d3da4fd1fdf17b62a", - "is_secret": false, - "is_verified": false, - "line_number": 947, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "bfb212002ccc72caf73acbfb0a5bd6c1c2acaa42", - "is_secret": false, - "is_verified": false, - "line_number": 956, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "93658223830f17449a3b81a759279f8db378716a", - "is_secret": false, - "is_verified": false, - "line_number": 967, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "6d252f2c2f14196d2164788e909d288b535a7730", - "is_secret": false, - "is_verified": false, - "line_number": 980, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8f48b5146d88920187ca1c8d1d52cd0b008f12de", - "is_secret": false, - "is_verified": false, - "line_number": 989, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9098c70eda3617323c613ed583e0a586a40bd2aa", - "is_secret": false, - "is_verified": false, - "line_number": 995, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "3cf7abf6e2a5256f811356be0f4b4f6911091224", - "is_secret": false, - "is_verified": false, - "line_number": 1001, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4da8447e2ac1a122131506f348248adb6384a3cf", - "is_secret": false, - "is_verified": false, - "line_number": 1007, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d713c28bb4df4f085fe5021c9369a8f72c7bfaba", - "is_secret": false, - "is_verified": false, - "line_number": 1018, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "09057af5054a5ad648e142f3d8f20650c1961474", - "is_secret": false, - "is_verified": false, - "line_number": 1024, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b62ab1173e770de947a50fe2b3ae83805000809b", - "is_secret": false, - "is_verified": false, - "line_number": 1037, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "7cf071642e1fbafa4f4139211d77123b9d5186f5", - "is_secret": false, - "is_verified": false, - "line_number": 1043, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2409e70c2d1016521b66c38545b57d9d558bd817", - "is_secret": false, - "is_verified": false, - "line_number": 1052, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "bd9c7b2b6c7454d73e7adde21fb63bd376791445", - "is_secret": false, - "is_verified": false, - "line_number": 1062, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "548024f4442052f17fa80873dcdb06c8be34b7aa", - "is_secret": false, - "is_verified": false, - "line_number": 1077, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e9932a2f581ce1c412466aba20ce5938f4ce4b6f", - "is_secret": false, - "is_verified": false, - "line_number": 1086, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "544a059b6156aa5094965cbf44cacd5232cdc84f", - "is_secret": false, - "is_verified": false, - "line_number": 1095, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "93696deb086eae01f6ade3bad646ad47f2569baf", - "is_secret": false, - "is_verified": false, - "line_number": 1101, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "89d71d034c88d9fca865976bdee1e29923b47431", - "is_secret": false, - "is_verified": false, - "line_number": 1110, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "89012c955a5b955d1bde634f9d5c1b6676a9ba5b", - "is_secret": false, - "is_verified": false, - "line_number": 1116, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f2a4445a1ffb12565fcf36d3b7782c9c76220f68", - "is_secret": false, - "is_verified": false, - "line_number": 1122, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8b26271bc5d1417e978b9b8eeea1363781d72a6b", - "is_secret": false, - "is_verified": false, - "line_number": 1133, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "56374b43ea51a40841a831e9c8c603f5463c18e6", - "is_secret": false, - "is_verified": false, - "line_number": 1144, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2cec7afe8f0f8f21d28f274299142cbbe949023c", - "is_secret": false, - "is_verified": false, - "line_number": 1153, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4d520ee751c4e698d412228eb52c7be60d9cc467", - "is_secret": false, - "is_verified": false, - "line_number": 1159, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "da6d928ee5aec0d6f03233e69f560c629e94627c", - "is_secret": false, - "is_verified": false, - "line_number": 1165, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d90f1e7c44d076236dd0aefdf9d808e98e7099e0", - "is_secret": false, - "is_verified": false, - "line_number": 1175, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "5ab5e8c5a013b4ea054a281a403fd0b7ab4917bc", - "is_secret": false, - "is_verified": false, - "line_number": 1187, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d1837f7e31117d99b0cbffd82e7eabcea851a94c", - "is_secret": false, - "is_verified": false, - "line_number": 1197, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "808905688514585ccff789bd66dcc97ca8205bf4", - "is_secret": false, - "is_verified": false, - "line_number": 1203, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "06be809fff59940d36f06f8ebc746d2db102f91f", - "is_secret": false, - "is_verified": false, - "line_number": 1216, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "aa82ea49bab0380d31a6bd815081bcc00fb627b6", - "is_secret": false, - "is_verified": false, - "line_number": 1226, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "465af5b50e950d5cd94c411a1c48a87ae8c1742e", - "is_secret": false, - "is_verified": false, - "line_number": 1239, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a87970b12f487a7b4b2dde69c4553fc759412a7c", - "is_secret": false, - "is_verified": false, - "line_number": 1248, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "c210cc626902bf389b3d925d7e92a8d4664cb3ff", - "is_secret": false, - "is_verified": false, - "line_number": 1258, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "32e3bd24faa462bf1a7d222314a8db909821dfcb", - "is_secret": false, - "is_verified": false, - "line_number": 1268, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2dc6e8f934b74951c38b2c67b869de588648a255", - "is_secret": false, - "is_verified": false, - "line_number": 1274, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "fb6c73d1cf78d39e7b71760b572559736d27ad42", - "is_secret": false, - "is_verified": false, - "line_number": 1283, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4388f4a1071af57b8f44733a4b590114701c8263", - "is_secret": false, - "is_verified": false, - "line_number": 1289, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "c635956e53a2624afd87d3ac8a41701e377f00e7", - "is_secret": false, - "is_verified": false, - "line_number": 1302, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "5747d8af9684661644b7df2fe00e51dc16f6843d", - "is_secret": false, - "is_verified": false, - "line_number": 1313, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a5f5903bd227b9d811f037da91086231b40422a7", - "is_secret": false, - "is_verified": false, - "line_number": 1324, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "34cfa36d78842de5ac91d80bd7a8996d903abee8", - "is_secret": false, - "is_verified": false, - "line_number": 1330, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "6be5b6b2b339ebb0359e498fb3752087d91f5ea2", - "is_secret": false, - "is_verified": false, - "line_number": 1339, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f9de3f6e836fa1b0a50f4e0e06154a422c0ed705", - "is_secret": false, - "is_verified": false, - "line_number": 1348, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "41d9bb6a8fee6ba11182b2f7973302854a4d2247", - "is_secret": false, - "is_verified": false, - "line_number": 1357, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "958222524529407d497c8c5bbbe8281b7c381e3c", - "is_secret": false, - "is_verified": false, - "line_number": 1363, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f1ff85189f1c0a5f11f0c0b11f33d9852075f0d4", - "is_secret": false, - "is_verified": false, - "line_number": 1372, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "94014c04a964970c30d309870f97ea5d8ea5bfd8", - "is_secret": false, - "is_verified": false, - "line_number": 1383, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "plugins_rust/encoded_exfil_detection/benches/encoded_exfil_detection.rs": [ - { - "hashed_secret": "9e53fffcf6d106f85fff53416e2ae687e1346aea", - "is_secret": false, - "is_verified": false, - "line_number": 27, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "67844048772af8b97258a5f1fe00b0043216f739", - "is_secret": false, - "is_verified": false, - "line_number": 49, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "648b3cbf65960d678821b2ae53740d890e1be100", - "is_secret": false, - "is_verified": false, - "line_number": 128, - "type": "Secret Keyword", - "verified_result": null - } - ], - "plugins_rust/encoded_exfil_detection/src/lib.rs": [ - { - "hashed_secret": "1d278d3c888d1a2fa7eed622bfc02927ce4049af", - "is_secret": false, - "is_verified": false, - "line_number": 573, - "type": "Base64 High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d7da4707f9793b357da965eb04f06ceae7d7bfa6", - "is_secret": false, - "is_verified": false, - "line_number": 574, - "type": "Base64 High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "0962e90797cdf7ff77c8a0a9477a7ca4f493465f", - "is_secret": false, - "is_verified": false, - "line_number": 892, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "ca3f3092640b07c051f03e52690fda1bcee3f60d", - "is_secret": false, - "is_verified": false, - "line_number": 913, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "532fdeccb155dce5b528ba039b9a5d201b817e3b", - "is_secret": false, - "is_verified": false, - "line_number": 932, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "cf743b3a58a4d0f91c1d7f5825c0b1b5f7758174", - "is_secret": false, - "is_verified": false, - "line_number": 969, - "type": "Base64 High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ff1a7ebc241b8eefe9e75e13f20cc22c365ab626", - "is_secret": false, - "is_verified": false, - "line_number": 969, - "type": "Secret Keyword", - "verified_result": null - } - ], - "plugins_rust/pii_filter/Cargo.lock": [ - { - "hashed_secret": "e0f930ce4dc6ee91bd9c13f93bdd411a875847ad", - "is_secret": false, - "is_verified": false, - "line_number": 9, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e4eb0d22814fd1612e292742063af598468d65ea", - "is_secret": false, - "is_verified": false, - "line_number": 18, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "0c2b06b30faf95e40c75aaaefa957d835c1df392", - "is_secret": false, - "is_verified": false, - "line_number": 27, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b660bfa30c205b9b6687cd7c0fec21dd718dd211", - "is_secret": false, - "is_verified": false, - "line_number": 36, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a0b32b6f12e419acbd7b54d25cfcb35ea7d88b39", - "is_secret": false, - "is_verified": false, - "line_number": 42, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f31c8be2a83d8786ff5ea96f532898a2d296dd95", - "is_secret": false, - "is_verified": false, - "line_number": 48, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ece49906f7a72ed3cfb012e5e2e1209e1b226172", - "is_secret": false, - "is_verified": false, - "line_number": 54, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "31584e466c43e3e97cd45c38e554e2273016a1aa", - "is_secret": false, - "is_verified": false, - "line_number": 60, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "7b7f80d71c414ee28e24ec7699ce3db9bf4a698b", - "is_secret": false, - "is_verified": false, - "line_number": 69, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "6b72376f39b229e9e6b4be8fe9c459fe555929ea", - "is_secret": false, - "is_verified": false, - "line_number": 75, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a5f1914a4edf604fca63fe9c60cb62601b7c701c", - "is_secret": false, - "is_verified": false, - "line_number": 81, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "eac7344fb138e2bc70ec74dd15a52fe0c253887a", - "is_secret": false, - "is_verified": false, - "line_number": 91, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4292dceca17415f83418a3909507b7c29737d284", - "is_secret": false, - "is_verified": false, - "line_number": 97, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ac8b8c7acbe0e66015ad3a9344c3cead1643ae19", - "is_secret": false, - "is_verified": false, - "line_number": 110, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2a5dd330cc6af22e5e259877e4a6342ed6395f2f", - "is_secret": false, - "is_verified": false, - "line_number": 121, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "84a7dcddd87e289faa3d086dc9a6ac50292f8ee2", - "is_secret": false, - "is_verified": false, - "line_number": 127, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b8f5c1c55ac08a64fa7e5a59c160f7f8218c85b5", - "is_secret": false, - "is_verified": false, - "line_number": 137, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "0ac839a751934212ae0f2b7625a79fecfbf86878", - "is_secret": false, - "is_verified": false, - "line_number": 146, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "710066c385eadd0091f067e0a8e2573ac53d5f4b", - "is_secret": false, - "is_verified": false, - "line_number": 156, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "aea69a0db4b6fb9c1801ecd9266c98e51a5b6515", - "is_secret": false, - "is_verified": false, - "line_number": 162, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e863b03571e186ffa7385bf8fdef7786a21a0a1a", - "is_secret": false, - "is_verified": false, - "line_number": 168, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "43cc136a251b358a7a64968300d250877dbf86bd", - "is_secret": false, - "is_verified": false, - "line_number": 177, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9feb0877f4bbf6117f628b1362db082168af749c", - "is_secret": false, - "is_verified": false, - "line_number": 202, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "68a15e87a6b9806b9214862840726ef1acc2e5dd", - "is_secret": false, - "is_verified": false, - "line_number": 212, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "37296779892fa772bc497cc53fa15fb2ced95b84", - "is_secret": false, - "is_verified": false, - "line_number": 222, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "076f7448f7767df7a0af32c25c88174167b2b870", - "is_secret": false, - "is_verified": false, - "line_number": 231, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "562e55d49724eaac4cff1b62be4137d8f5292a72", - "is_secret": false, - "is_verified": false, - "line_number": 237, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "088b03c54e39344f385c72c0d648cb5b98a63a02", - "is_secret": false, - "is_verified": false, - "line_number": 243, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8cb759d32a886afc6561de73e3102dcdc2cba219", - "is_secret": false, - "is_verified": false, - "line_number": 253, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "cc99ccaf83a5c3347fd6560d1fb362b09a92ae83", - "is_secret": false, - "is_verified": false, - "line_number": 262, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4af1b31ab033275a37cc32187a52e4c46cf762db", - "is_secret": false, - "is_verified": false, - "line_number": 272, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e3423649a06049806d8883193ffa13f6488898ba", - "is_secret": false, - "is_verified": false, - "line_number": 278, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4d2c6fb73af42ea62f965535a82e83df5411d419", - "is_secret": false, - "is_verified": false, - "line_number": 284, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ef842ccf084ffcac3b8030a3ac4c93c4be04860b", - "is_secret": false, - "is_verified": false, - "line_number": 290, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "762fc498e829264bd84320c53f9bd6c8eb445b8e", - "is_secret": false, - "is_verified": false, - "line_number": 300, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "fb738e4a202642582d5a12f41a884e26001c4fa6", - "is_secret": false, - "is_verified": false, - "line_number": 309, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "fb98da5b813e3ae31d0e430526334ae9f010a546", - "is_secret": false, - "is_verified": false, - "line_number": 320, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8c73d56ba41f0eeb296413affaa812431de0bd07", - "is_secret": false, - "is_verified": false, - "line_number": 332, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f3d3c26ed24a648081ced9182bb1b19f7d78e68d", - "is_secret": false, - "is_verified": false, - "line_number": 343, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "1d591d1374352e720051c9d3f7b1e2c5557d5fd6", - "is_secret": false, - "is_verified": false, - "line_number": 349, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ace4c15ef5d5b8c769f9d24cf4bf1de85fecb2c7", - "is_secret": false, - "is_verified": false, - "line_number": 355, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9bfd048ce5ceb106ba035489cea34b77d79814e4", - "is_secret": false, - "is_verified": false, - "line_number": 370, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4faeea7fc4d0ed985b094fe85a90aab7fc3d7ae8", - "is_secret": false, - "is_verified": false, - "line_number": 379, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2518cc4854c30b1401b7bef0dc542237d5acce4c", - "is_secret": false, - "is_verified": false, - "line_number": 389, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b2104d534fcbba7dd524595af7e52e29da1eca68", - "is_secret": false, - "is_verified": false, - "line_number": 398, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "aadc0453d2d3fdc05093cc42f8950f5445782e13", - "is_secret": false, - "is_verified": false, - "line_number": 410, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d2e033fe193c9a2aa937c12c6b496692a3bfd0f3", - "is_secret": false, - "is_verified": false, - "line_number": 419, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4c35f1105fef7dd05206e5c580220cae7fb0d31b", - "is_secret": false, - "is_verified": false, - "line_number": 428, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2bbb23c710243f6c2dd0da44f0bce1512d40aeb0", - "is_secret": false, - "is_verified": false, - "line_number": 437, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f07469cb2b99055bd011f347860438b0957c4b5b", - "is_secret": false, - "is_verified": false, - "line_number": 443, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "0ff83af8f867301e963f7ad167917125f83e8b40", - "is_secret": false, - "is_verified": false, - "line_number": 453, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "1a5c1aafe91ad50de9b57d58c892732234dd2618", - "is_secret": false, - "is_verified": false, - "line_number": 459, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2d70a27f9bdfdb0ebea3fa79f7962fa957bd11fa", - "is_secret": false, - "is_verified": false, - "line_number": 465, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a32cf693a7717063dad28f208cc038d14a3e847c", - "is_secret": false, - "is_verified": false, - "line_number": 471, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "db0c5903c40f2dcf6cc3179aa01c0026d18b3061", - "is_secret": false, - "is_verified": false, - "line_number": 477, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "52174d432f72b603ab3aeb65e046d8842f64eba8", - "is_secret": false, - "is_verified": false, - "line_number": 487, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e936de0f0fb5cb3b98a0a1fd5f06d47d714a948b", - "is_secret": false, - "is_verified": false, - "line_number": 493, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9f07d77e5781c78d5ccbce0d869c20816ce3211d", - "is_secret": false, - "is_verified": false, - "line_number": 508, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "3eb645bfb98952d218d44f82f285b49ff2e34dde", - "is_secret": false, - "is_verified": false, - "line_number": 518, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "0a549d859d0064d99bd64aa5226d0588e42e1f32", - "is_secret": false, - "is_verified": false, - "line_number": 527, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "12061f2ebc56b7c0d1167ed4ead1f4570bf89831", - "is_secret": false, - "is_verified": false, - "line_number": 533, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ebbea2a8b6a37ded68465f81f315d4a23c27a8e8", - "is_secret": false, - "is_verified": false, - "line_number": 542, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "1da2731a9796fcb269eb9b244d1e98e23618af6e", - "is_secret": false, - "is_verified": false, - "line_number": 551, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "3558448508502aa5a344f39c803ec2665ee9e81b", - "is_secret": false, - "is_verified": false, - "line_number": 567, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "51d39c0f11bd886e295a5a60dbec0332b6900c4c", - "is_secret": false, - "is_verified": false, - "line_number": 573, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d13f6ae109bf8e970b5d7d6a5c160a09f0642448", - "is_secret": false, - "is_verified": false, - "line_number": 579, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f6f33d498bbbe78966c6d4cc9e02eff57dccd290", - "is_secret": false, - "is_verified": false, - "line_number": 588, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9fbf26ca6c2385c79c99e7cfc059be4768610da9", - "is_secret": false, - "is_verified": false, - "line_number": 598, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "1db3bfc3973b7b1848de6d62826146ea6ba8b79b", - "is_secret": false, - "is_verified": false, - "line_number": 607, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a83eec6a7f047ef3b54c6471da4fee13861fbc74", - "is_secret": false, - "is_verified": false, - "line_number": 617, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "09bd80f5c95312a3d6aef8957b831a6679e729ac", - "is_secret": false, - "is_verified": false, - "line_number": 627, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "7e21427077e475ce5749e288815b8c8e9fda21c8", - "is_secret": false, - "is_verified": false, - "line_number": 652, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "072ff15e3bdf3b38a4b02d9e3e0ef4d937e18e15", - "is_secret": false, - "is_verified": false, - "line_number": 665, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b7d9cbf27e5b10a62ae9848be4b6fbbbe93491e1", - "is_secret": false, - "is_verified": false, - "line_number": 671, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "32cad93d535d24a043ec493dcb579e8744a33d5b", - "is_secret": false, - "is_verified": false, - "line_number": 680, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "125cc8a1a532f3c912919ef250bb42eee5a94e07", - "is_secret": false, - "is_verified": false, - "line_number": 686, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "56397d9a520742406189d71ac27362811f4eb3f4", - "is_secret": false, - "is_verified": false, - "line_number": 695, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "978bcf06dc12b74b1579b446ae082265db1358f6", - "is_secret": false, - "is_verified": false, - "line_number": 701, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "6538f9a3e2204f918d5719f17a23367f4d2105c7", - "is_secret": false, - "is_verified": false, - "line_number": 710, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "50feffdb4b1d1ce1f1432a2628ae3cbe1dca5849", - "is_secret": false, - "is_verified": false, - "line_number": 719, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "397e6f1ae7f26850afeb84f8fc86d442a23a5d39", - "is_secret": false, - "is_verified": false, - "line_number": 733, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "13095d536a4a53c8d1e3461be1259680963bb20c", - "is_secret": false, - "is_verified": false, - "line_number": 742, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "5477445d0fbadc7dffdaed724591de709ef47edf", - "is_secret": false, - "is_verified": false, - "line_number": 752, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "db5edc57fbb1b32f713e854ade035e9e48c39a09", - "is_secret": false, - "is_verified": false, - "line_number": 764, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e84cc493fd67427b615b414feca76a450c7840c3", - "is_secret": false, - "is_verified": false, - "line_number": 777, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "60dcabffca2924f2c9777b16acdcdd113632d1dd", - "is_secret": false, - "is_verified": false, - "line_number": 803, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9a2cdf6585922eea3346605d503aa1370139e781", - "is_secret": false, - "is_verified": false, - "line_number": 817, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "fb82e6c6c8d3ed2486ef8249d7e59336b5c86610", - "is_secret": false, - "is_verified": false, - "line_number": 826, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "47aeac2fa4acd0b3b040c9927a26128bf0c029af", - "is_secret": false, - "is_verified": false, - "line_number": 832, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ef2e592f89cb7fff6bccacece98f099ca10add8f", - "is_secret": false, - "is_verified": false, - "line_number": 843, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "def835c5f3d29ef5143e04e29a61cd85a81fe09e", - "is_secret": false, - "is_verified": false, - "line_number": 853, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a12225190f4c63e09c71dac208d1979b10e19e55", - "is_secret": false, - "is_verified": false, - "line_number": 862, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "c6db2ba51720f54d6b7336f1eb4d8032d96c76ed", - "is_secret": false, - "is_verified": false, - "line_number": 868, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d8f23b8e5e11045f82a31210713d308de8c1a83e", - "is_secret": false, - "is_verified": false, - "line_number": 878, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "0675b70f52618de044d1d1703b6c8849b407c83a", - "is_secret": false, - "is_verified": false, - "line_number": 888, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "c63addae2503bfe26923f0460158bb2fc674b928", - "is_secret": false, - "is_verified": false, - "line_number": 900, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "0c6e3a84ec63c609153315d01f022e862468c93d", - "is_secret": false, - "is_verified": false, - "line_number": 911, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "7595306f5da32ddff19f11930cfd7e021bca6a62", - "is_secret": false, - "is_verified": false, - "line_number": 917, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d2aacc8c22ee3e26d7a6f5d24bf528c87202c5f0", - "is_secret": false, - "is_verified": false, - "line_number": 923, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "bbe494c4c6d883d257069617aadba814a0b7af42", - "is_secret": false, - "is_verified": false, - "line_number": 929, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e045cb9f89b83706ea83457e157baa07f4da23d1", - "is_secret": false, - "is_verified": false, - "line_number": 941, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8b334846415d5595c15bebc8efe4ccd59c4a6668", - "is_secret": false, - "is_verified": false, - "line_number": 965, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e5eb894a4ae8142b70b7384d9ab7648abd1cc138", - "is_secret": false, - "is_verified": false, - "line_number": 976, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9daac1b266d3875ecff4a4b7932e73d63f51718f", - "is_secret": false, - "is_verified": false, - "line_number": 986, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d4b5e511e6b010068fe8fc17a580b24af40fca83", - "is_secret": false, - "is_verified": false, - "line_number": 992, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "dd30e69b06fc9e7153b91d6152b47abee06bc76e", - "is_secret": false, - "is_verified": false, - "line_number": 1001, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "edea2c54c8d390fdfff6535d3da4fd1fdf17b62a", - "is_secret": false, - "is_verified": false, - "line_number": 1011, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "bfb212002ccc72caf73acbfb0a5bd6c1c2acaa42", - "is_secret": false, - "is_verified": false, - "line_number": 1020, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "93658223830f17449a3b81a759279f8db378716a", - "is_secret": false, - "is_verified": false, - "line_number": 1031, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "6d252f2c2f14196d2164788e909d288b535a7730", - "is_secret": false, - "is_verified": false, - "line_number": 1044, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "56e65ae2b96ac3a748f6ce0ec54a459188c6ba87", - "is_secret": false, - "is_verified": false, - "line_number": 1053, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8f48b5146d88920187ca1c8d1d52cd0b008f12de", - "is_secret": false, - "is_verified": false, - "line_number": 1064, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9098c70eda3617323c613ed583e0a586a40bd2aa", - "is_secret": false, - "is_verified": false, - "line_number": 1070, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "3cf7abf6e2a5256f811356be0f4b4f6911091224", - "is_secret": false, - "is_verified": false, - "line_number": 1076, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4da8447e2ac1a122131506f348248adb6384a3cf", - "is_secret": false, - "is_verified": false, - "line_number": 1082, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "7ee5f550a5ea337809d0dc0330f8d5b0debcb764", - "is_secret": false, - "is_verified": false, - "line_number": 1093, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f57b63e67ed485c5ffe3dec0888657ac4c281ca5", - "is_secret": false, - "is_verified": false, - "line_number": 1099, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "824096ad190248d40cef63862048a64d18e73093", - "is_secret": false, - "is_verified": false, - "line_number": 1108, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "09057af5054a5ad648e142f3d8f20650c1961474", - "is_secret": false, - "is_verified": false, - "line_number": 1119, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b62ab1173e770de947a50fe2b3ae83805000809b", - "is_secret": false, - "is_verified": false, - "line_number": 1132, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "7cf071642e1fbafa4f4139211d77123b9d5186f5", - "is_secret": false, - "is_verified": false, - "line_number": 1138, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2409e70c2d1016521b66c38545b57d9d558bd817", - "is_secret": false, - "is_verified": false, - "line_number": 1147, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "bd9c7b2b6c7454d73e7adde21fb63bd376791445", - "is_secret": false, - "is_verified": false, - "line_number": 1157, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "548024f4442052f17fa80873dcdb06c8be34b7aa", - "is_secret": false, - "is_verified": false, - "line_number": 1172, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e9932a2f581ce1c412466aba20ce5938f4ce4b6f", - "is_secret": false, - "is_verified": false, - "line_number": 1181, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "544a059b6156aa5094965cbf44cacd5232cdc84f", - "is_secret": false, - "is_verified": false, - "line_number": 1190, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "c6f4db06cf473f26b17a37c45c8da55362dc8477", - "is_secret": false, - "is_verified": false, - "line_number": 1196, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "93696deb086eae01f6ade3bad646ad47f2569baf", - "is_secret": false, - "is_verified": false, - "line_number": 1202, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "89d71d034c88d9fca865976bdee1e29923b47431", - "is_secret": false, - "is_verified": false, - "line_number": 1211, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "89012c955a5b955d1bde634f9d5c1b6676a9ba5b", - "is_secret": false, - "is_verified": false, - "line_number": 1217, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f2a4445a1ffb12565fcf36d3b7782c9c76220f68", - "is_secret": false, - "is_verified": false, - "line_number": 1223, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8b26271bc5d1417e978b9b8eeea1363781d72a6b", - "is_secret": false, - "is_verified": false, - "line_number": 1234, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "56374b43ea51a40841a831e9c8c603f5463c18e6", - "is_secret": false, - "is_verified": false, - "line_number": 1245, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "25b36c9171ce850542b5d6177529684358941924", - "is_secret": false, - "is_verified": false, - "line_number": 1254, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4d520ee751c4e698d412228eb52c7be60d9cc467", - "is_secret": false, - "is_verified": false, - "line_number": 1260, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "da6d928ee5aec0d6f03233e69f560c629e94627c", - "is_secret": false, - "is_verified": false, - "line_number": 1266, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d90f1e7c44d076236dd0aefdf9d808e98e7099e0", - "is_secret": false, - "is_verified": false, - "line_number": 1276, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "247410e3854ab062955b1178ea2c11c5d0fbab4f", - "is_secret": false, - "is_verified": false, - "line_number": 1288, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "cbd62b2fef0566cbd4adcae3c005b9fb1d23170d", - "is_secret": false, - "is_verified": false, - "line_number": 1299, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "5ab5e8c5a013b4ea054a281a403fd0b7ab4917bc", - "is_secret": false, - "is_verified": false, - "line_number": 1305, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d1837f7e31117d99b0cbffd82e7eabcea851a94c", - "is_secret": false, - "is_verified": false, - "line_number": 1315, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "040278ead73fc50f601174bf24cfe3552915a67e", - "is_secret": false, - "is_verified": false, - "line_number": 1321, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "808905688514585ccff789bd66dcc97ca8205bf4", - "is_secret": false, - "is_verified": false, - "line_number": 1330, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "06be809fff59940d36f06f8ebc746d2db102f91f", - "is_secret": false, - "is_verified": false, - "line_number": 1343, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "aa82ea49bab0380d31a6bd815081bcc00fb627b6", - "is_secret": false, - "is_verified": false, - "line_number": 1353, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "465af5b50e950d5cd94c411a1c48a87ae8c1742e", - "is_secret": false, - "is_verified": false, - "line_number": 1366, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a87970b12f487a7b4b2dde69c4553fc759412a7c", - "is_secret": false, - "is_verified": false, - "line_number": 1375, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "c210cc626902bf389b3d925d7e92a8d4664cb3ff", - "is_secret": false, - "is_verified": false, - "line_number": 1385, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "32e3bd24faa462bf1a7d222314a8db909821dfcb", - "is_secret": false, - "is_verified": false, - "line_number": 1395, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2dc6e8f934b74951c38b2c67b869de588648a255", - "is_secret": false, - "is_verified": false, - "line_number": 1401, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "fb6c73d1cf78d39e7b71760b572559736d27ad42", - "is_secret": false, - "is_verified": false, - "line_number": 1410, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4388f4a1071af57b8f44733a4b590114701c8263", - "is_secret": false, - "is_verified": false, - "line_number": 1416, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "c635956e53a2624afd87d3ac8a41701e377f00e7", - "is_secret": false, - "is_verified": false, - "line_number": 1429, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "5747d8af9684661644b7df2fe00e51dc16f6843d", - "is_secret": false, - "is_verified": false, - "line_number": 1440, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a5f5903bd227b9d811f037da91086231b40422a7", - "is_secret": false, - "is_verified": false, - "line_number": 1451, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "34cfa36d78842de5ac91d80bd7a8996d903abee8", - "is_secret": false, - "is_verified": false, - "line_number": 1457, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "6be5b6b2b339ebb0359e498fb3752087d91f5ea2", - "is_secret": false, - "is_verified": false, - "line_number": 1466, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f9de3f6e836fa1b0a50f4e0e06154a422c0ed705", - "is_secret": false, - "is_verified": false, - "line_number": 1475, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "41d9bb6a8fee6ba11182b2f7973302854a4d2247", - "is_secret": false, - "is_verified": false, - "line_number": 1484, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "0253824dccde843d47fa65b99bb29912e5ec3a0a", - "is_secret": false, - "is_verified": false, - "line_number": 1490, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "958222524529407d497c8c5bbbe8281b7c381e3c", - "is_secret": false, - "is_verified": false, - "line_number": 1496, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f1ff85189f1c0a5f11f0c0b11f33d9852075f0d4", - "is_secret": false, - "is_verified": false, - "line_number": 1505, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "94014c04a964970c30d309870f97ea5d8ea5bfd8", - "is_secret": false, - "is_verified": false, - "line_number": 1516, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "plugins_rust/retry_with_backoff/Cargo.lock": [ - { - "hashed_secret": "0c2b06b30faf95e40c75aaaefa957d835c1df392", - "is_secret": false, - "is_verified": false, - "line_number": 9, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "053152e6d6ec523a570bc615219d6c829a0d455b", - "is_secret": false, - "is_verified": false, - "line_number": 18, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b49da8f6f5252f3a2d11923d030cd29c0c704b28", - "is_secret": false, - "is_verified": false, - "line_number": 24, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ece49906f7a72ed3cfb012e5e2e1209e1b226172", - "is_secret": false, - "is_verified": false, - "line_number": 33, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b4808fb54c15b4bfe5268dd5ef3669d35302dac8", - "is_secret": false, - "is_verified": false, - "line_number": 39, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "435b5a83c55f72c24e5bd183795441de1a767dc2", - "is_secret": false, - "is_verified": false, - "line_number": 45, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "eac7344fb138e2bc70ec74dd15a52fe0c253887a", - "is_secret": false, - "is_verified": false, - "line_number": 55, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a01b71d602bc5018e96db1a555a02018f474f151", - "is_secret": false, - "is_verified": false, - "line_number": 61, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "aea69a0db4b6fb9c1801ecd9266c98e51a5b6515", - "is_secret": false, - "is_verified": false, - "line_number": 74, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "562e55d49724eaac4cff1b62be4137d8f5292a72", - "is_secret": false, - "is_verified": false, - "line_number": 80, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "11b0d546f276abc72c2c2b0afa721f73b9f02097", - "is_secret": false, - "is_verified": false, - "line_number": 86, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4af1b31ab033275a37cc32187a52e4c46cf762db", - "is_secret": false, - "is_verified": false, - "line_number": 95, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e3423649a06049806d8883193ffa13f6488898ba", - "is_secret": false, - "is_verified": false, - "line_number": 101, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4d2c6fb73af42ea62f965535a82e83df5411d419", - "is_secret": false, - "is_verified": false, - "line_number": 107, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "762fc498e829264bd84320c53f9bd6c8eb445b8e", - "is_secret": false, - "is_verified": false, - "line_number": 113, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "fb738e4a202642582d5a12f41a884e26001c4fa6", - "is_secret": false, - "is_verified": false, - "line_number": 122, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f3d3c26ed24a648081ced9182bb1b19f7d78e68d", - "is_secret": false, - "is_verified": false, - "line_number": 133, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "1d591d1374352e720051c9d3f7b1e2c5557d5fd6", - "is_secret": false, - "is_verified": false, - "line_number": 139, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ace4c15ef5d5b8c769f9d24cf4bf1de85fecb2c7", - "is_secret": false, - "is_verified": false, - "line_number": 145, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9bfd048ce5ceb106ba035489cea34b77d79814e4", - "is_secret": false, - "is_verified": false, - "line_number": 160, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4faeea7fc4d0ed985b094fe85a90aab7fc3d7ae8", - "is_secret": false, - "is_verified": false, - "line_number": 169, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "61098053a09b0d7751e1a1c50a3c6208309c5b0c", - "is_secret": false, - "is_verified": false, - "line_number": 179, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b2104d534fcbba7dd524595af7e52e29da1eca68", - "is_secret": false, - "is_verified": false, - "line_number": 188, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "aadc0453d2d3fdc05093cc42f8950f5445782e13", - "is_secret": false, - "is_verified": false, - "line_number": 200, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4c35f1105fef7dd05206e5c580220cae7fb0d31b", - "is_secret": false, - "is_verified": false, - "line_number": 209, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "dc02a9de5e15472f3ab7614b78d04a3f9c42aa61", - "is_secret": false, - "is_verified": false, - "line_number": 218, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "93408d219072bd897710a546d04acbdf8f518992", - "is_secret": false, - "is_verified": false, - "line_number": 224, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "0ff83af8f867301e963f7ad167917125f83e8b40", - "is_secret": false, - "is_verified": false, - "line_number": 234, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "29cc4d113d2b819fd175fde15139a9047b4714ae", - "is_secret": false, - "is_verified": false, - "line_number": 240, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2d70a27f9bdfdb0ebea3fa79f7962fa957bd11fa", - "is_secret": false, - "is_verified": false, - "line_number": 246, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a32cf693a7717063dad28f208cc038d14a3e847c", - "is_secret": false, - "is_verified": false, - "line_number": 252, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "db0c5903c40f2dcf6cc3179aa01c0026d18b3061", - "is_secret": false, - "is_verified": false, - "line_number": 258, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "52174d432f72b603ab3aeb65e046d8842f64eba8", - "is_secret": false, - "is_verified": false, - "line_number": 268, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e936de0f0fb5cb3b98a0a1fd5f06d47d714a948b", - "is_secret": false, - "is_verified": false, - "line_number": 274, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9f07d77e5781c78d5ccbce0d869c20816ce3211d", - "is_secret": false, - "is_verified": false, - "line_number": 289, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "3eb645bfb98952d218d44f82f285b49ff2e34dde", - "is_secret": false, - "is_verified": false, - "line_number": 299, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e213efa760058b8539bda18527fb7ed85aa954b9", - "is_secret": false, - "is_verified": false, - "line_number": 308, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "12061f2ebc56b7c0d1167ed4ead1f4570bf89831", - "is_secret": false, - "is_verified": false, - "line_number": 314, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ebbea2a8b6a37ded68465f81f315d4a23c27a8e8", - "is_secret": false, - "is_verified": false, - "line_number": 323, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "1da2731a9796fcb269eb9b244d1e98e23618af6e", - "is_secret": false, - "is_verified": false, - "line_number": 332, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9f45c3d9955af35039612741be99a94f2e719c26", - "is_secret": false, - "is_verified": false, - "line_number": 348, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d13f6ae109bf8e970b5d7d6a5c160a09f0642448", - "is_secret": false, - "is_verified": false, - "line_number": 354, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9fbf26ca6c2385c79c99e7cfc059be4768610da9", - "is_secret": false, - "is_verified": false, - "line_number": 363, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "1db3bfc3973b7b1848de6d62826146ea6ba8b79b", - "is_secret": false, - "is_verified": false, - "line_number": 372, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a83eec6a7f047ef3b54c6471da4fee13861fbc74", - "is_secret": false, - "is_verified": false, - "line_number": 382, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "09bd80f5c95312a3d6aef8957b831a6679e729ac", - "is_secret": false, - "is_verified": false, - "line_number": 392, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "32cad93d535d24a043ec493dcb579e8744a33d5b", - "is_secret": false, - "is_verified": false, - "line_number": 401, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4f2be6ef19a8d142a2b949275ede90051f493bec", - "is_secret": false, - "is_verified": false, - "line_number": 407, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "56397d9a520742406189d71ac27362811f4eb3f4", - "is_secret": false, - "is_verified": false, - "line_number": 416, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "978bcf06dc12b74b1579b446ae082265db1358f6", - "is_secret": false, - "is_verified": false, - "line_number": 422, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "6538f9a3e2204f918d5719f17a23367f4d2105c7", - "is_secret": false, - "is_verified": false, - "line_number": 431, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "50feffdb4b1d1ce1f1432a2628ae3cbe1dca5849", - "is_secret": false, - "is_verified": false, - "line_number": 440, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "397e6f1ae7f26850afeb84f8fc86d442a23a5d39", - "is_secret": false, - "is_verified": false, - "line_number": 454, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "13095d536a4a53c8d1e3461be1259680963bb20c", - "is_secret": false, - "is_verified": false, - "line_number": 463, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9c5e1ed2c3178270a610d4f296c453a23fe1f20c", - "is_secret": false, - "is_verified": false, - "line_number": 473, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "5477445d0fbadc7dffdaed724591de709ef47edf", - "is_secret": false, - "is_verified": false, - "line_number": 484, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "db5edc57fbb1b32f713e854ade035e9e48c39a09", - "is_secret": false, - "is_verified": false, - "line_number": 496, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e84cc493fd67427b615b414feca76a450c7840c3", - "is_secret": false, - "is_verified": false, - "line_number": 509, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "60dcabffca2924f2c9777b16acdcdd113632d1dd", - "is_secret": false, - "is_verified": false, - "line_number": 535, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b4259ea178dd7cb0249df5d8feb7d13a2d6fd820", - "is_secret": false, - "is_verified": false, - "line_number": 549, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "47aeac2fa4acd0b3b040c9927a26128bf0c029af", - "is_secret": false, - "is_verified": false, - "line_number": 558, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ef2e592f89cb7fff6bccacece98f099ca10add8f", - "is_secret": false, - "is_verified": false, - "line_number": 569, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "def835c5f3d29ef5143e04e29a61cd85a81fe09e", - "is_secret": false, - "is_verified": false, - "line_number": 579, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a12225190f4c63e09c71dac208d1979b10e19e55", - "is_secret": false, - "is_verified": false, - "line_number": 588, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "7595306f5da32ddff19f11930cfd7e021bca6a62", - "is_secret": false, - "is_verified": false, - "line_number": 605, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d2aacc8c22ee3e26d7a6f5d24bf528c87202c5f0", - "is_secret": false, - "is_verified": false, - "line_number": 611, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "bbe494c4c6d883d257069617aadba814a0b7af42", - "is_secret": false, - "is_verified": false, - "line_number": 617, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e045cb9f89b83706ea83457e157baa07f4da23d1", - "is_secret": false, - "is_verified": false, - "line_number": 629, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8b334846415d5595c15bebc8efe4ccd59c4a6668", - "is_secret": false, - "is_verified": false, - "line_number": 653, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e5eb894a4ae8142b70b7384d9ab7648abd1cc138", - "is_secret": false, - "is_verified": false, - "line_number": 664, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9daac1b266d3875ecff4a4b7932e73d63f51718f", - "is_secret": false, - "is_verified": false, - "line_number": 674, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "dd30e69b06fc9e7153b91d6152b47abee06bc76e", - "is_secret": false, - "is_verified": false, - "line_number": 680, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "edea2c54c8d390fdfff6535d3da4fd1fdf17b62a", - "is_secret": false, - "is_verified": false, - "line_number": 690, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "bfb212002ccc72caf73acbfb0a5bd6c1c2acaa42", - "is_secret": false, - "is_verified": false, - "line_number": 699, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "93658223830f17449a3b81a759279f8db378716a", - "is_secret": false, - "is_verified": false, - "line_number": 710, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e897339bce75e73161156f7f77f749c33210a6c4", - "is_secret": false, - "is_verified": false, - "line_number": 723, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8f48b5146d88920187ca1c8d1d52cd0b008f12de", - "is_secret": false, - "is_verified": false, - "line_number": 732, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9098c70eda3617323c613ed583e0a586a40bd2aa", - "is_secret": false, - "is_verified": false, - "line_number": 738, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "3cf7abf6e2a5256f811356be0f4b4f6911091224", - "is_secret": false, - "is_verified": false, - "line_number": 744, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "fed671d6a24626d6cf16f91b98f2b11ba736d095", - "is_secret": false, - "is_verified": false, - "line_number": 750, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d713c28bb4df4f085fe5021c9369a8f72c7bfaba", - "is_secret": false, - "is_verified": false, - "line_number": 761, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "09057af5054a5ad648e142f3d8f20650c1961474", - "is_secret": false, - "is_verified": false, - "line_number": 767, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b62ab1173e770de947a50fe2b3ae83805000809b", - "is_secret": false, - "is_verified": false, - "line_number": 780, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "7cf071642e1fbafa4f4139211d77123b9d5186f5", - "is_secret": false, - "is_verified": false, - "line_number": 786, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4bf343940338760d4db799219a72121298a7279e", - "is_secret": false, - "is_verified": false, - "line_number": 795, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "470c9288d98e825e05f5a332001fa763d37a45e5", - "is_secret": false, - "is_verified": false, - "line_number": 810, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "dc34d7cc096190da266ed434c8ebe76f502016af", - "is_secret": false, - "is_verified": false, - "line_number": 819, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2f37058f3d31cd1953e41b3fe49df36338c014be", - "is_secret": false, - "is_verified": false, - "line_number": 828, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "93696deb086eae01f6ade3bad646ad47f2569baf", - "is_secret": false, - "is_verified": false, - "line_number": 834, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "89d71d034c88d9fca865976bdee1e29923b47431", - "is_secret": false, - "is_verified": false, - "line_number": 843, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "89012c955a5b955d1bde634f9d5c1b6676a9ba5b", - "is_secret": false, - "is_verified": false, - "line_number": 849, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f2a4445a1ffb12565fcf36d3b7782c9c76220f68", - "is_secret": false, - "is_verified": false, - "line_number": 855, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8b26271bc5d1417e978b9b8eeea1363781d72a6b", - "is_secret": false, - "is_verified": false, - "line_number": 866, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "56374b43ea51a40841a831e9c8c603f5463c18e6", - "is_secret": false, - "is_verified": false, - "line_number": 877, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2cec7afe8f0f8f21d28f274299142cbbe949023c", - "is_secret": false, - "is_verified": false, - "line_number": 886, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4d520ee751c4e698d412228eb52c7be60d9cc467", - "is_secret": false, - "is_verified": false, - "line_number": 892, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "da6d928ee5aec0d6f03233e69f560c629e94627c", - "is_secret": false, - "is_verified": false, - "line_number": 898, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d90f1e7c44d076236dd0aefdf9d808e98e7099e0", - "is_secret": false, - "is_verified": false, - "line_number": 908, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d1837f7e31117d99b0cbffd82e7eabcea851a94c", - "is_secret": false, - "is_verified": false, - "line_number": 920, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "604edf3bd2202bebf89e23895a6ef68088f57e42", - "is_secret": false, - "is_verified": false, - "line_number": 926, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "7167df607c246fe7c20de1a7b7abc394c7026cd0", - "is_secret": false, - "is_verified": false, - "line_number": 939, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f0f817643483baa005cf31ad44d972f65508675e", - "is_secret": false, - "is_verified": false, - "line_number": 949, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "64c9975a7ab1b5bcd2439c334f2370a65d4aae82", - "is_secret": false, - "is_verified": false, - "line_number": 962, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4388f4a1071af57b8f44733a4b590114701c8263", - "is_secret": false, - "is_verified": false, - "line_number": 971, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "c635956e53a2624afd87d3ac8a41701e377f00e7", - "is_secret": false, - "is_verified": false, - "line_number": 984, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "5747d8af9684661644b7df2fe00e51dc16f6843d", - "is_secret": false, - "is_verified": false, - "line_number": 995, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a5f5903bd227b9d811f037da91086231b40422a7", - "is_secret": false, - "is_verified": false, - "line_number": 1006, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "34cfa36d78842de5ac91d80bd7a8996d903abee8", - "is_secret": false, - "is_verified": false, - "line_number": 1012, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "6be5b6b2b339ebb0359e498fb3752087d91f5ea2", - "is_secret": false, - "is_verified": false, - "line_number": 1021, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "06df8ab661f9188a94174448a5fc6643fca246c4", - "is_secret": false, - "is_verified": false, - "line_number": 1030, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "3b98f886da0f14b493bd4e915f301e538d33776e", - "is_secret": false, - "is_verified": false, - "line_number": 1036, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "958891bf3ce63f0001e6975b282f6d26461ca4bf", - "is_secret": false, - "is_verified": false, - "line_number": 1045, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "94014c04a964970c30d309870f97ea5d8ea5bfd8", - "is_secret": false, - "is_verified": false, - "line_number": 1056, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "plugins_rust/secrets_detection/Cargo.lock": [ - { - "hashed_secret": "e0f930ce4dc6ee91bd9c13f93bdd411a875847ad", - "is_secret": false, - "is_verified": false, - "line_number": 9, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e4eb0d22814fd1612e292742063af598468d65ea", - "is_secret": false, - "is_verified": false, - "line_number": 18, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "0c2b06b30faf95e40c75aaaefa957d835c1df392", - "is_secret": false, - "is_verified": false, - "line_number": 27, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b660bfa30c205b9b6687cd7c0fec21dd718dd211", - "is_secret": false, - "is_verified": false, - "line_number": 36, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a0b32b6f12e419acbd7b54d25cfcb35ea7d88b39", - "is_secret": false, - "is_verified": false, - "line_number": 42, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f31c8be2a83d8786ff5ea96f532898a2d296dd95", - "is_secret": false, - "is_verified": false, - "line_number": 48, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d9ea801e3f4d913d0b6721b819f2b90c35f2303b", - "is_secret": false, - "is_verified": false, - "line_number": 54, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ece49906f7a72ed3cfb012e5e2e1209e1b226172", - "is_secret": false, - "is_verified": false, - "line_number": 63, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "7b7f80d71c414ee28e24ec7699ce3db9bf4a698b", - "is_secret": false, - "is_verified": false, - "line_number": 69, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "6b72376f39b229e9e6b4be8fe9c459fe555929ea", - "is_secret": false, - "is_verified": false, - "line_number": 75, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a5f1914a4edf604fca63fe9c60cb62601b7c701c", - "is_secret": false, - "is_verified": false, - "line_number": 81, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "eac7344fb138e2bc70ec74dd15a52fe0c253887a", - "is_secret": false, - "is_verified": false, - "line_number": 91, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4292dceca17415f83418a3909507b7c29737d284", - "is_secret": false, - "is_verified": false, - "line_number": 97, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ac8b8c7acbe0e66015ad3a9344c3cead1643ae19", - "is_secret": false, - "is_verified": false, - "line_number": 110, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2a5dd330cc6af22e5e259877e4a6342ed6395f2f", - "is_secret": false, - "is_verified": false, - "line_number": 121, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "84a7dcddd87e289faa3d086dc9a6ac50292f8ee2", - "is_secret": false, - "is_verified": false, - "line_number": 127, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b8f5c1c55ac08a64fa7e5a59c160f7f8218c85b5", - "is_secret": false, - "is_verified": false, - "line_number": 137, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "0ac839a751934212ae0f2b7625a79fecfbf86878", - "is_secret": false, - "is_verified": false, - "line_number": 146, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "710066c385eadd0091f067e0a8e2573ac53d5f4b", - "is_secret": false, - "is_verified": false, - "line_number": 156, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "aea69a0db4b6fb9c1801ecd9266c98e51a5b6515", - "is_secret": false, - "is_verified": false, - "line_number": 162, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "43cc136a251b358a7a64968300d250877dbf86bd", - "is_secret": false, - "is_verified": false, - "line_number": 168, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9feb0877f4bbf6117f628b1362db082168af749c", - "is_secret": false, - "is_verified": false, - "line_number": 193, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "68a15e87a6b9806b9214862840726ef1acc2e5dd", - "is_secret": false, - "is_verified": false, - "line_number": 203, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "37296779892fa772bc497cc53fa15fb2ced95b84", - "is_secret": false, - "is_verified": false, - "line_number": 213, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "076f7448f7767df7a0af32c25c88174167b2b870", - "is_secret": false, - "is_verified": false, - "line_number": 222, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "562e55d49724eaac4cff1b62be4137d8f5292a72", - "is_secret": false, - "is_verified": false, - "line_number": 228, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8cb759d32a886afc6561de73e3102dcdc2cba219", - "is_secret": false, - "is_verified": false, - "line_number": 234, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4af1b31ab033275a37cc32187a52e4c46cf762db", - "is_secret": false, - "is_verified": false, - "line_number": 243, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e3423649a06049806d8883193ffa13f6488898ba", - "is_secret": false, - "is_verified": false, - "line_number": 249, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4d2c6fb73af42ea62f965535a82e83df5411d419", - "is_secret": false, - "is_verified": false, - "line_number": 255, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "762fc498e829264bd84320c53f9bd6c8eb445b8e", - "is_secret": false, - "is_verified": false, - "line_number": 261, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "fb738e4a202642582d5a12f41a884e26001c4fa6", - "is_secret": false, - "is_verified": false, - "line_number": 270, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8c73d56ba41f0eeb296413affaa812431de0bd07", - "is_secret": false, - "is_verified": false, - "line_number": 281, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f3d3c26ed24a648081ced9182bb1b19f7d78e68d", - "is_secret": false, - "is_verified": false, - "line_number": 292, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "1d591d1374352e720051c9d3f7b1e2c5557d5fd6", - "is_secret": false, - "is_verified": false, - "line_number": 298, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ace4c15ef5d5b8c769f9d24cf4bf1de85fecb2c7", - "is_secret": false, - "is_verified": false, - "line_number": 304, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9bfd048ce5ceb106ba035489cea34b77d79814e4", - "is_secret": false, - "is_verified": false, - "line_number": 319, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4faeea7fc4d0ed985b094fe85a90aab7fc3d7ae8", - "is_secret": false, - "is_verified": false, - "line_number": 328, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2518cc4854c30b1401b7bef0dc542237d5acce4c", - "is_secret": false, - "is_verified": false, - "line_number": 338, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b2104d534fcbba7dd524595af7e52e29da1eca68", - "is_secret": false, - "is_verified": false, - "line_number": 347, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "aadc0453d2d3fdc05093cc42f8950f5445782e13", - "is_secret": false, - "is_verified": false, - "line_number": 359, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d2e033fe193c9a2aa937c12c6b496692a3bfd0f3", - "is_secret": false, - "is_verified": false, - "line_number": 368, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4c35f1105fef7dd05206e5c580220cae7fb0d31b", - "is_secret": false, - "is_verified": false, - "line_number": 377, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2bbb23c710243f6c2dd0da44f0bce1512d40aeb0", - "is_secret": false, - "is_verified": false, - "line_number": 386, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f07469cb2b99055bd011f347860438b0957c4b5b", - "is_secret": false, - "is_verified": false, - "line_number": 392, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "0ff83af8f867301e963f7ad167917125f83e8b40", - "is_secret": false, - "is_verified": false, - "line_number": 402, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "1a5c1aafe91ad50de9b57d58c892732234dd2618", - "is_secret": false, - "is_verified": false, - "line_number": 408, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2d70a27f9bdfdb0ebea3fa79f7962fa957bd11fa", - "is_secret": false, - "is_verified": false, - "line_number": 414, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a32cf693a7717063dad28f208cc038d14a3e847c", - "is_secret": false, - "is_verified": false, - "line_number": 420, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "db0c5903c40f2dcf6cc3179aa01c0026d18b3061", - "is_secret": false, - "is_verified": false, - "line_number": 426, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "52174d432f72b603ab3aeb65e046d8842f64eba8", - "is_secret": false, - "is_verified": false, - "line_number": 436, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e936de0f0fb5cb3b98a0a1fd5f06d47d714a948b", - "is_secret": false, - "is_verified": false, - "line_number": 442, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9f07d77e5781c78d5ccbce0d869c20816ce3211d", - "is_secret": false, - "is_verified": false, - "line_number": 457, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "3eb645bfb98952d218d44f82f285b49ff2e34dde", - "is_secret": false, - "is_verified": false, - "line_number": 467, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "0a549d859d0064d99bd64aa5226d0588e42e1f32", - "is_secret": false, - "is_verified": false, - "line_number": 476, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "12061f2ebc56b7c0d1167ed4ead1f4570bf89831", - "is_secret": false, - "is_verified": false, - "line_number": 482, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ebbea2a8b6a37ded68465f81f315d4a23c27a8e8", - "is_secret": false, - "is_verified": false, - "line_number": 491, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "1da2731a9796fcb269eb9b244d1e98e23618af6e", - "is_secret": false, - "is_verified": false, - "line_number": 500, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "3558448508502aa5a344f39c803ec2665ee9e81b", - "is_secret": false, - "is_verified": false, - "line_number": 516, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "51d39c0f11bd886e295a5a60dbec0332b6900c4c", - "is_secret": false, - "is_verified": false, - "line_number": 522, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d13f6ae109bf8e970b5d7d6a5c160a09f0642448", - "is_secret": false, - "is_verified": false, - "line_number": 528, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f6f33d498bbbe78966c6d4cc9e02eff57dccd290", - "is_secret": false, - "is_verified": false, - "line_number": 537, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9fbf26ca6c2385c79c99e7cfc059be4768610da9", - "is_secret": false, - "is_verified": false, - "line_number": 547, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "1db3bfc3973b7b1848de6d62826146ea6ba8b79b", - "is_secret": false, - "is_verified": false, - "line_number": 556, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a83eec6a7f047ef3b54c6471da4fee13861fbc74", - "is_secret": false, - "is_verified": false, - "line_number": 566, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "09bd80f5c95312a3d6aef8957b831a6679e729ac", - "is_secret": false, - "is_verified": false, - "line_number": 576, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "7e21427077e475ce5749e288815b8c8e9fda21c8", - "is_secret": false, - "is_verified": false, - "line_number": 585, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "072ff15e3bdf3b38a4b02d9e3e0ef4d937e18e15", - "is_secret": false, - "is_verified": false, - "line_number": 598, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b7d9cbf27e5b10a62ae9848be4b6fbbbe93491e1", - "is_secret": false, - "is_verified": false, - "line_number": 604, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "32cad93d535d24a043ec493dcb579e8744a33d5b", - "is_secret": false, - "is_verified": false, - "line_number": 613, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "125cc8a1a532f3c912919ef250bb42eee5a94e07", - "is_secret": false, - "is_verified": false, - "line_number": 619, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "56397d9a520742406189d71ac27362811f4eb3f4", - "is_secret": false, - "is_verified": false, - "line_number": 628, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "978bcf06dc12b74b1579b446ae082265db1358f6", - "is_secret": false, - "is_verified": false, - "line_number": 634, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "6538f9a3e2204f918d5719f17a23367f4d2105c7", - "is_secret": false, - "is_verified": false, - "line_number": 643, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "50feffdb4b1d1ce1f1432a2628ae3cbe1dca5849", - "is_secret": false, - "is_verified": false, - "line_number": 652, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "397e6f1ae7f26850afeb84f8fc86d442a23a5d39", - "is_secret": false, - "is_verified": false, - "line_number": 666, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "13095d536a4a53c8d1e3461be1259680963bb20c", - "is_secret": false, - "is_verified": false, - "line_number": 675, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9c5e1ed2c3178270a610d4f296c453a23fe1f20c", - "is_secret": false, - "is_verified": false, - "line_number": 685, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "5477445d0fbadc7dffdaed724591de709ef47edf", - "is_secret": false, - "is_verified": false, - "line_number": 696, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "db5edc57fbb1b32f713e854ade035e9e48c39a09", - "is_secret": false, - "is_verified": false, - "line_number": 708, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e84cc493fd67427b615b414feca76a450c7840c3", - "is_secret": false, - "is_verified": false, - "line_number": 721, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "60dcabffca2924f2c9777b16acdcdd113632d1dd", - "is_secret": false, - "is_verified": false, - "line_number": 747, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9a2cdf6585922eea3346605d503aa1370139e781", - "is_secret": false, - "is_verified": false, - "line_number": 761, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "47aeac2fa4acd0b3b040c9927a26128bf0c029af", - "is_secret": false, - "is_verified": false, - "line_number": 770, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ef2e592f89cb7fff6bccacece98f099ca10add8f", - "is_secret": false, - "is_verified": false, - "line_number": 781, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "def835c5f3d29ef5143e04e29a61cd85a81fe09e", - "is_secret": false, - "is_verified": false, - "line_number": 791, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a12225190f4c63e09c71dac208d1979b10e19e55", - "is_secret": false, - "is_verified": false, - "line_number": 800, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "c6db2ba51720f54d6b7336f1eb4d8032d96c76ed", - "is_secret": false, - "is_verified": false, - "line_number": 806, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d8f23b8e5e11045f82a31210713d308de8c1a83e", - "is_secret": false, - "is_verified": false, - "line_number": 816, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "0675b70f52618de044d1d1703b6c8849b407c83a", - "is_secret": false, - "is_verified": false, - "line_number": 826, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "c63addae2503bfe26923f0460158bb2fc674b928", - "is_secret": false, - "is_verified": false, - "line_number": 838, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "0c6e3a84ec63c609153315d01f022e862468c93d", - "is_secret": false, - "is_verified": false, - "line_number": 849, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "7595306f5da32ddff19f11930cfd7e021bca6a62", - "is_secret": false, - "is_verified": false, - "line_number": 855, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d2aacc8c22ee3e26d7a6f5d24bf528c87202c5f0", - "is_secret": false, - "is_verified": false, - "line_number": 861, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "bbe494c4c6d883d257069617aadba814a0b7af42", - "is_secret": false, - "is_verified": false, - "line_number": 867, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e045cb9f89b83706ea83457e157baa07f4da23d1", - "is_secret": false, - "is_verified": false, - "line_number": 879, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8b334846415d5595c15bebc8efe4ccd59c4a6668", - "is_secret": false, - "is_verified": false, - "line_number": 903, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e5eb894a4ae8142b70b7384d9ab7648abd1cc138", - "is_secret": false, - "is_verified": false, - "line_number": 914, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9daac1b266d3875ecff4a4b7932e73d63f51718f", - "is_secret": false, - "is_verified": false, - "line_number": 924, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d4b5e511e6b010068fe8fc17a580b24af40fca83", - "is_secret": false, - "is_verified": false, - "line_number": 930, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "dd30e69b06fc9e7153b91d6152b47abee06bc76e", - "is_secret": false, - "is_verified": false, - "line_number": 951, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "edea2c54c8d390fdfff6535d3da4fd1fdf17b62a", - "is_secret": false, - "is_verified": false, - "line_number": 961, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "bfb212002ccc72caf73acbfb0a5bd6c1c2acaa42", - "is_secret": false, - "is_verified": false, - "line_number": 970, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "93658223830f17449a3b81a759279f8db378716a", - "is_secret": false, - "is_verified": false, - "line_number": 981, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "6d252f2c2f14196d2164788e909d288b535a7730", - "is_secret": false, - "is_verified": false, - "line_number": 994, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8f48b5146d88920187ca1c8d1d52cd0b008f12de", - "is_secret": false, - "is_verified": false, - "line_number": 1003, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9098c70eda3617323c613ed583e0a586a40bd2aa", - "is_secret": false, - "is_verified": false, - "line_number": 1009, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "3cf7abf6e2a5256f811356be0f4b4f6911091224", - "is_secret": false, - "is_verified": false, - "line_number": 1015, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4da8447e2ac1a122131506f348248adb6384a3cf", - "is_secret": false, - "is_verified": false, - "line_number": 1021, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d713c28bb4df4f085fe5021c9369a8f72c7bfaba", - "is_secret": false, - "is_verified": false, - "line_number": 1032, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "09057af5054a5ad648e142f3d8f20650c1961474", - "is_secret": false, - "is_verified": false, - "line_number": 1038, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b62ab1173e770de947a50fe2b3ae83805000809b", - "is_secret": false, - "is_verified": false, - "line_number": 1051, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "7cf071642e1fbafa4f4139211d77123b9d5186f5", - "is_secret": false, - "is_verified": false, - "line_number": 1057, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2409e70c2d1016521b66c38545b57d9d558bd817", - "is_secret": false, - "is_verified": false, - "line_number": 1066, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "bd9c7b2b6c7454d73e7adde21fb63bd376791445", - "is_secret": false, - "is_verified": false, - "line_number": 1076, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "548024f4442052f17fa80873dcdb06c8be34b7aa", - "is_secret": false, - "is_verified": false, - "line_number": 1091, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e9932a2f581ce1c412466aba20ce5938f4ce4b6f", - "is_secret": false, - "is_verified": false, - "line_number": 1100, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "544a059b6156aa5094965cbf44cacd5232cdc84f", - "is_secret": false, - "is_verified": false, - "line_number": 1109, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "93696deb086eae01f6ade3bad646ad47f2569baf", - "is_secret": false, - "is_verified": false, - "line_number": 1115, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "89d71d034c88d9fca865976bdee1e29923b47431", - "is_secret": false, - "is_verified": false, - "line_number": 1124, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "89012c955a5b955d1bde634f9d5c1b6676a9ba5b", - "is_secret": false, - "is_verified": false, - "line_number": 1130, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f2a4445a1ffb12565fcf36d3b7782c9c76220f68", - "is_secret": false, - "is_verified": false, - "line_number": 1136, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8b26271bc5d1417e978b9b8eeea1363781d72a6b", - "is_secret": false, - "is_verified": false, - "line_number": 1147, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "56374b43ea51a40841a831e9c8c603f5463c18e6", - "is_secret": false, - "is_verified": false, - "line_number": 1158, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2cec7afe8f0f8f21d28f274299142cbbe949023c", - "is_secret": false, - "is_verified": false, - "line_number": 1167, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4d520ee751c4e698d412228eb52c7be60d9cc467", - "is_secret": false, - "is_verified": false, - "line_number": 1173, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "da6d928ee5aec0d6f03233e69f560c629e94627c", - "is_secret": false, - "is_verified": false, - "line_number": 1179, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d90f1e7c44d076236dd0aefdf9d808e98e7099e0", - "is_secret": false, - "is_verified": false, - "line_number": 1189, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "5ab5e8c5a013b4ea054a281a403fd0b7ab4917bc", - "is_secret": false, - "is_verified": false, - "line_number": 1201, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d1837f7e31117d99b0cbffd82e7eabcea851a94c", - "is_secret": false, - "is_verified": false, - "line_number": 1211, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "808905688514585ccff789bd66dcc97ca8205bf4", - "is_secret": false, - "is_verified": false, - "line_number": 1217, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "06be809fff59940d36f06f8ebc746d2db102f91f", - "is_secret": false, - "is_verified": false, - "line_number": 1230, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "aa82ea49bab0380d31a6bd815081bcc00fb627b6", - "is_secret": false, - "is_verified": false, - "line_number": 1240, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "465af5b50e950d5cd94c411a1c48a87ae8c1742e", - "is_secret": false, - "is_verified": false, - "line_number": 1253, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a87970b12f487a7b4b2dde69c4553fc759412a7c", - "is_secret": false, - "is_verified": false, - "line_number": 1262, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "c210cc626902bf389b3d925d7e92a8d4664cb3ff", - "is_secret": false, - "is_verified": false, - "line_number": 1272, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "32e3bd24faa462bf1a7d222314a8db909821dfcb", - "is_secret": false, - "is_verified": false, - "line_number": 1282, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2dc6e8f934b74951c38b2c67b869de588648a255", - "is_secret": false, - "is_verified": false, - "line_number": 1288, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "fb6c73d1cf78d39e7b71760b572559736d27ad42", - "is_secret": false, - "is_verified": false, - "line_number": 1297, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4388f4a1071af57b8f44733a4b590114701c8263", - "is_secret": false, - "is_verified": false, - "line_number": 1303, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "c635956e53a2624afd87d3ac8a41701e377f00e7", - "is_secret": false, - "is_verified": false, - "line_number": 1316, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "5747d8af9684661644b7df2fe00e51dc16f6843d", - "is_secret": false, - "is_verified": false, - "line_number": 1327, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a5f5903bd227b9d811f037da91086231b40422a7", - "is_secret": false, - "is_verified": false, - "line_number": 1338, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "34cfa36d78842de5ac91d80bd7a8996d903abee8", - "is_secret": false, - "is_verified": false, - "line_number": 1344, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "6be5b6b2b339ebb0359e498fb3752087d91f5ea2", - "is_secret": false, - "is_verified": false, - "line_number": 1353, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f9de3f6e836fa1b0a50f4e0e06154a422c0ed705", - "is_secret": false, - "is_verified": false, - "line_number": 1362, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "41d9bb6a8fee6ba11182b2f7973302854a4d2247", - "is_secret": false, - "is_verified": false, - "line_number": 1371, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "958222524529407d497c8c5bbbe8281b7c381e3c", - "is_secret": false, - "is_verified": false, - "line_number": 1377, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f1ff85189f1c0a5f11f0c0b11f33d9852075f0d4", - "is_secret": false, - "is_verified": false, - "line_number": 1386, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "94014c04a964970c30d309870f97ea5d8ea5bfd8", - "is_secret": false, - "is_verified": false, - "line_number": 1397, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "plugins_rust/secrets_detection/benches/secrets_detection.rs": [ - { - "hashed_secret": "86de8c52637ec530fe39b0a8471da9b8764d5242", - "is_secret": false, - "is_verified": false, - "line_number": 53, - "type": "AWS Access Key", - "verified_result": null - }, - { - "hashed_secret": "5b8c02feb3811310ff452e9a812b9f98873f36bf", - "is_secret": false, - "is_verified": false, - "line_number": 69, - "type": "SoftLayer Credentials", - "verified_result": null - }, - { - "hashed_secret": "0fa6996ddd42e0f9db3f1c04d06453026d56da0f", - "is_secret": false, - "is_verified": false, - "line_number": 73, - "type": "SoftLayer Credentials", - "verified_result": null - }, - { - "hashed_secret": "de56e523c4cf688e56a7df48cf01e2a21db5b5a4", - "is_secret": false, - "is_verified": false, - "line_number": 77, - "type": "AWS Access Key", - "verified_result": null - } - ], - "plugins_rust/secrets_detection/compare_performance.py": [ - { - "hashed_secret": "86de8c52637ec530fe39b0a8471da9b8764d5242", - "is_secret": false, - "is_verified": false, - "line_number": 67, - "type": "AWS Access Key", - "verified_result": null - }, - { - "hashed_secret": "5b8c02feb3811310ff452e9a812b9f98873f36bf", - "is_secret": false, - "is_verified": false, - "line_number": 71, - "type": "SoftLayer Credentials", - "verified_result": null - }, - { - "hashed_secret": "0fa6996ddd42e0f9db3f1c04d06453026d56da0f", - "is_secret": false, - "is_verified": false, - "line_number": 72, - "type": "SoftLayer Credentials", - "verified_result": null - } - ], - "plugins_rust/secrets_detection/examples/heavy_workload.rs": [ - { - "hashed_secret": "86de8c52637ec530fe39b0a8471da9b8764d5242", - "is_secret": false, - "is_verified": false, - "line_number": 34, - "type": "AWS Access Key", - "verified_result": null - }, - { - "hashed_secret": "5b8c02feb3811310ff452e9a812b9f98873f36bf", - "is_secret": false, - "is_verified": false, - "line_number": 38, - "type": "SoftLayer Credentials", - "verified_result": null - }, - { - "hashed_secret": "0fa6996ddd42e0f9db3f1c04d06453026d56da0f", - "is_secret": false, - "is_verified": false, - "line_number": 39, - "type": "SoftLayer Credentials", - "verified_result": null - }, - { - "hashed_secret": "de56e523c4cf688e56a7df48cf01e2a21db5b5a4", - "is_secret": false, - "is_verified": false, - "line_number": 40, - "type": "AWS Access Key", - "verified_result": null - }, - { - "hashed_secret": "be4fc4886bd949b369d5e092eb87494f12e57e5b", - "is_secret": false, - "is_verified": false, - "line_number": 42, - "type": "Private Key", - "verified_result": null - } - ], - "plugins_rust/secrets_detection/src/patterns.rs": [ - { - "hashed_secret": "86de8c52637ec530fe39b0a8471da9b8764d5242", - "is_secret": false, - "is_verified": false, - "line_number": 94, - "type": "AWS Access Key", - "verified_result": null - }, - { - "hashed_secret": "de56e523c4cf688e56a7df48cf01e2a21db5b5a4", - "is_secret": false, - "is_verified": false, - "line_number": 98, - "type": "AWS Access Key", - "verified_result": null - }, - { - "hashed_secret": "a5f223264a7296f00627a4c5c00a9957c67f5479", - "is_secret": false, - "is_verified": false, - "line_number": 238, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "be4fc4886bd949b369d5e092eb87494f12e57e5b", - "is_secret": false, - "is_verified": false, - "line_number": 282, - "type": "Private Key", - "verified_result": null - }, - { - "hashed_secret": "daefe0b4345a654580dcad25c7c11ff4c944a8c0", - "is_secret": false, - "is_verified": false, - "line_number": 286, - "type": "Private Key", - "verified_result": null - }, - { - "hashed_secret": "f0778f3e140a61d5bbbed5430773e52af2f5fba4", - "is_secret": false, - "is_verified": false, - "line_number": 290, - "type": "Private Key", - "verified_result": null - }, - { - "hashed_secret": "27c6929aef41ae2bcadac15ca6abcaff72cda9cd", - "is_secret": false, - "is_verified": false, - "line_number": 294, - "type": "Private Key", - "verified_result": null - }, - { - "hashed_secret": "75f3be87351a63f1c2f677632838f9e2be066891", - "is_secret": false, - "is_verified": false, - "line_number": 336, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a19310f994b3a8974f03cddd1de4b862266947e8", - "is_secret": false, - "is_verified": false, - "line_number": 340, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b1775a785f09a6ebaf2dc33d6eaeb98974d9cdb8", - "is_secret": false, - "is_verified": false, - "line_number": 344, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "7326d685c1328a4b748e7c239ba227c8a7afc988", - "is_secret": false, - "is_verified": false, - "line_number": 350, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e0eca9acea92cdf77e594f0d3367116b9adffeec", - "is_secret": false, - "is_verified": false, - "line_number": 354, - "type": "Base64 High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e4d0f04f03d9183f843c9d764592ba5bcd17f356", - "is_secret": false, - "is_verified": false, - "line_number": 369, - "type": "Base64 High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b256e49b893f8b2eeb2384801910c70ed17a2191", - "is_secret": false, - "is_verified": false, - "line_number": 411, - "type": "Secret Keyword", - "verified_result": null - } - ], - "plugins_rust/secrets_detection/src/scanner.rs": [ - { - "hashed_secret": "50b68587c1c40248753c5e9190dcc3dd73aa142a", - "is_secret": false, - "is_verified": false, - "line_number": 243, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "4e8dd640c77be10809ea27a4a6e668d9cba82014", - "is_secret": false, - "is_verified": false, - "line_number": 248, - "type": "SoftLayer Credentials", - "verified_result": null - }, - { - "hashed_secret": "721b09413c9b91bf3278f966b62c7a03ae2ef9a0", - "is_secret": false, - "is_verified": false, - "line_number": 248, - "type": "Base64 High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "be4fc4886bd949b369d5e092eb87494f12e57e5b", - "is_secret": false, - "is_verified": false, - "line_number": 292, - "type": "Private Key", - "verified_result": null - }, - { - "hashed_secret": "de56e523c4cf688e56a7df48cf01e2a21db5b5a4", - "is_secret": false, - "is_verified": false, - "line_number": 726, - "type": "AWS Access Key", - "verified_result": null - }, - { - "hashed_secret": "2e3810897e838a5edcfd57b5fb117927fdc922da", - "is_secret": false, - "is_verified": false, - "line_number": 740, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "7f1d81c1d9cf850d6a1dbd32d5f110b6732fe60e", - "is_secret": false, - "is_verified": false, - "line_number": 740, - "type": "Base64 High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "86de8c52637ec530fe39b0a8471da9b8764d5242", - "is_secret": false, - "is_verified": false, - "line_number": 799, - "type": "AWS Access Key", - "verified_result": null - } - ], - "plugins_rust/url_reputation/Cargo.lock": [ - { - "hashed_secret": "e0f930ce4dc6ee91bd9c13f93bdd411a875847ad", - "is_secret": false, - "is_verified": false, - "line_number": 9, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e4eb0d22814fd1612e292742063af598468d65ea", - "is_secret": false, - "is_verified": false, - "line_number": 18, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b660bfa30c205b9b6687cd7c0fec21dd718dd211", - "is_secret": false, - "is_verified": false, - "line_number": 27, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "c010dcc93cdc02fef1dae55077be682aa0cbe6c7", - "is_secret": false, - "is_verified": false, - "line_number": 33, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d9ea801e3f4d913d0b6721b819f2b90c35f2303b", - "is_secret": false, - "is_verified": false, - "line_number": 39, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ece49906f7a72ed3cfb012e5e2e1209e1b226172", - "is_secret": false, - "is_verified": false, - "line_number": 48, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b4808fb54c15b4bfe5268dd5ef3669d35302dac8", - "is_secret": false, - "is_verified": false, - "line_number": 54, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "6b72376f39b229e9e6b4be8fe9c459fe555929ea", - "is_secret": false, - "is_verified": false, - "line_number": 60, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "435b5a83c55f72c24e5bd183795441de1a767dc2", - "is_secret": false, - "is_verified": false, - "line_number": 66, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "eac7344fb138e2bc70ec74dd15a52fe0c253887a", - "is_secret": false, - "is_verified": false, - "line_number": 76, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ac8b8c7acbe0e66015ad3a9344c3cead1643ae19", - "is_secret": false, - "is_verified": false, - "line_number": 82, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2a5dd330cc6af22e5e259877e4a6342ed6395f2f", - "is_secret": false, - "is_verified": false, - "line_number": 93, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "84a7dcddd87e289faa3d086dc9a6ac50292f8ee2", - "is_secret": false, - "is_verified": false, - "line_number": 99, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d4dbf662832f75c34b38426ed080a4b801d00f3d", - "is_secret": false, - "is_verified": false, - "line_number": 109, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ebccb6117556497e4da9b06f699ff6a27d45dd3f", - "is_secret": false, - "is_verified": false, - "line_number": 118, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f665662f68035bb677672e8a3b8b814f20f3d020", - "is_secret": false, - "is_verified": false, - "line_number": 128, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "43cc136a251b358a7a64968300d250877dbf86bd", - "is_secret": false, - "is_verified": false, - "line_number": 134, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9feb0877f4bbf6117f628b1362db082168af749c", - "is_secret": false, - "is_verified": false, - "line_number": 159, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "68a15e87a6b9806b9214862840726ef1acc2e5dd", - "is_secret": false, - "is_verified": false, - "line_number": 169, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "37296779892fa772bc497cc53fa15fb2ced95b84", - "is_secret": false, - "is_verified": false, - "line_number": 179, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "076f7448f7767df7a0af32c25c88174167b2b870", - "is_secret": false, - "is_verified": false, - "line_number": 188, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "562e55d49724eaac4cff1b62be4137d8f5292a72", - "is_secret": false, - "is_verified": false, - "line_number": 194, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8b80e3dbf214e8b5f42deaf56c671624fb8916a9", - "is_secret": false, - "is_verified": false, - "line_number": 200, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4af1b31ab033275a37cc32187a52e4c46cf762db", - "is_secret": false, - "is_verified": false, - "line_number": 211, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2e2d13eb72c2d2c46e0c94dd2259343b0933dda9", - "is_secret": false, - "is_verified": false, - "line_number": 217, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4d2c6fb73af42ea62f965535a82e83df5411d419", - "is_secret": false, - "is_verified": false, - "line_number": 223, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "5dd5a3b90a4aec184d6390d782a283adf53b87f9", - "is_secret": false, - "is_verified": false, - "line_number": 229, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8c73d56ba41f0eeb296413affaa812431de0bd07", - "is_secret": false, - "is_verified": false, - "line_number": 238, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "1d591d1374352e720051c9d3f7b1e2c5557d5fd6", - "is_secret": false, - "is_verified": false, - "line_number": 249, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "86b403165d79a78a2c4a595fe14902c5f4f47361", - "is_secret": false, - "is_verified": false, - "line_number": 255, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "6c94d59722bce3906d347ed600e37e119a7837df", - "is_secret": false, - "is_verified": false, - "line_number": 268, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "23db86ff034d7e9f799f0226a1f1a7baae4c7de1", - "is_secret": false, - "is_verified": false, - "line_number": 281, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "49b46f344ecff2a2a1820536df0334ca68050940", - "is_secret": false, - "is_verified": false, - "line_number": 295, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "50d2139960f135299c21e3cdaa6b6f076b1e2080", - "is_secret": false, - "is_verified": false, - "line_number": 301, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "08f998bf3af34432739a9ab2cddd2bb85654a4b2", - "is_secret": false, - "is_verified": false, - "line_number": 315, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "be61a53441932e4f82987dcaf2ef89fbcfaac0a5", - "is_secret": false, - "is_verified": false, - "line_number": 321, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "3062e8c5ec47afaf7ba2ce1532e8e56c0af6ea8f", - "is_secret": false, - "is_verified": false, - "line_number": 336, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "29aae00ffb8818a8acfa28442eb304f3862cbaf5", - "is_secret": false, - "is_verified": false, - "line_number": 347, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d2e033fe193c9a2aa937c12c6b496692a3bfd0f3", - "is_secret": false, - "is_verified": false, - "line_number": 357, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2bbb23c710243f6c2dd0da44f0bce1512d40aeb0", - "is_secret": false, - "is_verified": false, - "line_number": 366, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "93408d219072bd897710a546d04acbdf8f518992", - "is_secret": false, - "is_verified": false, - "line_number": 372, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "29cc4d113d2b819fd175fde15139a9047b4714ae", - "is_secret": false, - "is_verified": false, - "line_number": 382, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4d3dfbf42f79749cb066b45b4baa929fb891809a", - "is_secret": false, - "is_verified": false, - "line_number": 388, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2d70a27f9bdfdb0ebea3fa79f7962fa957bd11fa", - "is_secret": false, - "is_verified": false, - "line_number": 394, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "52174d432f72b603ab3aeb65e046d8842f64eba8", - "is_secret": false, - "is_verified": false, - "line_number": 400, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ebbea2a8b6a37ded68465f81f315d4a23c27a8e8", - "is_secret": false, - "is_verified": false, - "line_number": 406, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9f45c3d9955af35039612741be99a94f2e719c26", - "is_secret": false, - "is_verified": false, - "line_number": 415, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "51d39c0f11bd886e295a5a60dbec0332b6900c4c", - "is_secret": false, - "is_verified": false, - "line_number": 421, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f6f33d498bbbe78966c6d4cc9e02eff57dccd290", - "is_secret": false, - "is_verified": false, - "line_number": 427, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a02ff67f8e504e59af8eff72afb26fda27037a42", - "is_secret": false, - "is_verified": false, - "line_number": 437, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "7d554479a29539a4aac0c4838c6295fabc6c798e", - "is_secret": false, - "is_verified": false, - "line_number": 443, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "7b5b0e7936c7b3d920c9fdb1a7a3c618522e44bc", - "is_secret": false, - "is_verified": false, - "line_number": 454, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "5eca3f3bea5336ccef83fa88cd907152802dc63a", - "is_secret": false, - "is_verified": false, - "line_number": 464, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "cb3b81942ccfd8c332b82d08e97a26b0741000fd", - "is_secret": false, - "is_verified": false, - "line_number": 477, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "7e21427077e475ce5749e288815b8c8e9fda21c8", - "is_secret": false, - "is_verified": false, - "line_number": 486, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "072ff15e3bdf3b38a4b02d9e3e0ef4d937e18e15", - "is_secret": false, - "is_verified": false, - "line_number": 499, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b7d9cbf27e5b10a62ae9848be4b6fbbbe93491e1", - "is_secret": false, - "is_verified": false, - "line_number": 505, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "32cad93d535d24a043ec493dcb579e8744a33d5b", - "is_secret": false, - "is_verified": false, - "line_number": 514, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "20b3b413826d61a82664d755aaaa4a180123d161", - "is_secret": false, - "is_verified": false, - "line_number": 520, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "6538f9a3e2204f918d5719f17a23367f4d2105c7", - "is_secret": false, - "is_verified": false, - "line_number": 529, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "50feffdb4b1d1ce1f1432a2628ae3cbe1dca5849", - "is_secret": false, - "is_verified": false, - "line_number": 538, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "397e6f1ae7f26850afeb84f8fc86d442a23a5d39", - "is_secret": false, - "is_verified": false, - "line_number": 552, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "13095d536a4a53c8d1e3461be1259680963bb20c", - "is_secret": false, - "is_verified": false, - "line_number": 561, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9c5e1ed2c3178270a610d4f296c453a23fe1f20c", - "is_secret": false, - "is_verified": false, - "line_number": 571, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "5477445d0fbadc7dffdaed724591de709ef47edf", - "is_secret": false, - "is_verified": false, - "line_number": 582, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "db5edc57fbb1b32f713e854ade035e9e48c39a09", - "is_secret": false, - "is_verified": false, - "line_number": 594, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b4259ea178dd7cb0249df5d8feb7d13a2d6fd820", - "is_secret": false, - "is_verified": false, - "line_number": 607, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "c6db2ba51720f54d6b7336f1eb4d8032d96c76ed", - "is_secret": false, - "is_verified": false, - "line_number": 616, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d8f23b8e5e11045f82a31210713d308de8c1a83e", - "is_secret": false, - "is_verified": false, - "line_number": 626, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "0675b70f52618de044d1d1703b6c8849b407c83a", - "is_secret": false, - "is_verified": false, - "line_number": 636, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "c63addae2503bfe26923f0460158bb2fc674b928", - "is_secret": false, - "is_verified": false, - "line_number": 648, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9f7a7461ae8d5e27458e899b4e84416483680a51", - "is_secret": false, - "is_verified": false, - "line_number": 659, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9daac1b266d3875ecff4a4b7932e73d63f51718f", - "is_secret": false, - "is_verified": false, - "line_number": 665, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d4b5e511e6b010068fe8fc17a580b24af40fca83", - "is_secret": false, - "is_verified": false, - "line_number": 671, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "dd30e69b06fc9e7153b91d6152b47abee06bc76e", - "is_secret": false, - "is_verified": false, - "line_number": 680, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "edea2c54c8d390fdfff6535d3da4fd1fdf17b62a", - "is_secret": false, - "is_verified": false, - "line_number": 690, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "bfb212002ccc72caf73acbfb0a5bd6c1c2acaa42", - "is_secret": false, - "is_verified": false, - "line_number": 699, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "93658223830f17449a3b81a759279f8db378716a", - "is_secret": false, - "is_verified": false, - "line_number": 710, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8f48b5146d88920187ca1c8d1d52cd0b008f12de", - "is_secret": false, - "is_verified": false, - "line_number": 723, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9098c70eda3617323c613ed583e0a586a40bd2aa", - "is_secret": false, - "is_verified": false, - "line_number": 729, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "14b703e941e97ca4115fbfc7afc9d19920aa9c6c", - "is_secret": false, - "is_verified": false, - "line_number": 735, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "7d64118ef4ee08c6e431da22c00cc6db06fc8495", - "is_secret": false, - "is_verified": false, - "line_number": 741, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "fed671d6a24626d6cf16f91b98f2b11ba736d095", - "is_secret": false, - "is_verified": false, - "line_number": 747, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "24c6c50199abdbdf01f5bfc41c06d323bb57ac6b", - "is_secret": false, - "is_verified": false, - "line_number": 758, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d713c28bb4df4f085fe5021c9369a8f72c7bfaba", - "is_secret": false, - "is_verified": false, - "line_number": 769, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "86efcec63d7f5e5c3cf34e1acfd29fc257f3c382", - "is_secret": false, - "is_verified": false, - "line_number": 775, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2409e70c2d1016521b66c38545b57d9d558bd817", - "is_secret": false, - "is_verified": false, - "line_number": 785, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2822217bb14c0223fb86ef16fccaeebb3dd90119", - "is_secret": false, - "is_verified": false, - "line_number": 795, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "1e5be28ee09da48b8c2233118719dfc81d717f98", - "is_secret": false, - "is_verified": false, - "line_number": 804, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2cec7afe8f0f8f21d28f274299142cbbe949023c", - "is_secret": false, - "is_verified": false, - "line_number": 810, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d3921a77cbcdfd88b9f31e8434d163d7c20738b2", - "is_secret": false, - "is_verified": false, - "line_number": 816, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b6d0d1d511230bd02469e718d01be6374651925d", - "is_secret": false, - "is_verified": false, - "line_number": 825, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "97edd35e73708bd2f6a1bdb0b0fa3e06105b6c54", - "is_secret": false, - "is_verified": false, - "line_number": 831, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d8f1d314d800de9172c39e761f105816ae4af3e8", - "is_secret": false, - "is_verified": false, - "line_number": 841, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a0ccf26d3e5e73d1fea4cbd7e882f8a4269b51e6", - "is_secret": false, - "is_verified": false, - "line_number": 869, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "5ab5e8c5a013b4ea054a281a403fd0b7ab4917bc", - "is_secret": false, - "is_verified": false, - "line_number": 875, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "604edf3bd2202bebf89e23895a6ef68088f57e42", - "is_secret": false, - "is_verified": false, - "line_number": 885, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "7167df607c246fe7c20de1a7b7abc394c7026cd0", - "is_secret": false, - "is_verified": false, - "line_number": 898, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f0f817643483baa005cf31ad44d972f65508675e", - "is_secret": false, - "is_verified": false, - "line_number": 908, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "64c9975a7ab1b5bcd2439c334f2370a65d4aae82", - "is_secret": false, - "is_verified": false, - "line_number": 921, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "7493d3c7ad45735197216f5195a3a4b2f0dc05f8", - "is_secret": false, - "is_verified": false, - "line_number": 930, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "c210cc626902bf389b3d925d7e92a8d4664cb3ff", - "is_secret": false, - "is_verified": false, - "line_number": 940, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "32e3bd24faa462bf1a7d222314a8db909821dfcb", - "is_secret": false, - "is_verified": false, - "line_number": 950, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2dc6e8f934b74951c38b2c67b869de588648a255", - "is_secret": false, - "is_verified": false, - "line_number": 956, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "fb6c73d1cf78d39e7b71760b572559736d27ad42", - "is_secret": false, - "is_verified": false, - "line_number": 965, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a5f5903bd227b9d811f037da91086231b40422a7", - "is_secret": false, - "is_verified": false, - "line_number": 971, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f9de3f6e836fa1b0a50f4e0e06154a422c0ed705", - "is_secret": false, - "is_verified": false, - "line_number": 977, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "54f3ab20cd8669d07e74b7b8767b7c2d2be98e84", - "is_secret": false, - "is_verified": false, - "line_number": 986, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ed1ba03fabc77d3aa81f08b87029efbd7d9a1340", - "is_secret": false, - "is_verified": false, - "line_number": 992, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "3a49d16dc29026d169224f8bc14e6828a62128c2", - "is_secret": false, - "is_verified": false, - "line_number": 1003, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2384307a6cae139f038230b889458d239e9ff53b", - "is_secret": false, - "is_verified": false, - "line_number": 1015, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "76513044803a500e17fc0ad98fb266b139572891", - "is_secret": false, - "is_verified": false, - "line_number": 1024, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a58d51dc68f673a049f4d82bdf01e61cf73b93b2", - "is_secret": false, - "is_verified": false, - "line_number": 1035, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "86abb8da6326f0620dc9f0951186d370cad910b9", - "is_secret": false, - "is_verified": false, - "line_number": 1044, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "fa35aa28d81cf737ac8af26aae31b0b195eef3af", - "is_secret": false, - "is_verified": false, - "line_number": 1056, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "6274c331e3520248957d3a2497e2edab2c3aaee0", - "is_secret": false, - "is_verified": false, - "line_number": 1067, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "27ab5e55f6ba7afa0e99492446d43050c7b719fa", - "is_secret": false, - "is_verified": false, - "line_number": 1078, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "94014c04a964970c30d309870f97ea5d8ea5bfd8", - "is_secret": false, - "is_verified": false, - "line_number": 1089, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "podman-compose-sonarqube.yaml": [ - { - "hashed_secret": "345e9ea7c857e75dedd9edb24c232e1cab297c19", - "is_secret": false, - "is_verified": false, - "line_number": 28, - "type": "Secret Keyword", - "verified_result": null - } - ], - "run-granian.sh": [ - { - "hashed_secret": "64f5490a2808803712ef99d9dfb8bc3ea5a15077", - "is_secret": false, - "is_verified": false, - "line_number": 291, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "cff0d14e4337fa8bdb68dfa906f04b0df6fad72f", - "is_secret": false, - "is_verified": false, - "line_number": 406, - "type": "Secret Keyword", - "verified_result": null - } - ], - "run-gunicorn.sh": [ - { - "hashed_secret": "64f5490a2808803712ef99d9dfb8bc3ea5a15077", - "is_secret": false, - "is_verified": false, - "line_number": 296, - "type": "Secret Keyword", - "verified_result": null - } - ], - "run.sh": [ - { - "hashed_secret": "fa9beb99e4029ad5a6615399e7bbae21356086b3", - "is_secret": false, - "is_verified": false, - "line_number": 115, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "7b4455a56fbf1d198e45e04c437488514645a82c", - "is_secret": false, - "is_verified": false, - "line_number": 117, - "type": "Secret Keyword", - "verified_result": null - } - ], - "scripts/ci/run_upgrade_validation.sh": [ - { - "hashed_secret": "1d193899f802762003a56b1cbcfeb55f2b04d61b", - "is_secret": false, - "is_verified": false, - "line_number": 448, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "dcdf24a1d4440d5ebe319bf804f96c81e08350f9", - "is_secret": false, - "is_verified": false, - "line_number": 455, - "type": "Basic Auth Credentials", - "verified_result": null - } - ], - "scripts/demo_a2a_agent_auth.py": [ - { - "hashed_secret": "e415dfafffe0c1859396cee8659521eaeb789ced", - "is_secret": false, - "is_verified": false, - "line_number": 45, - "type": "Secret Keyword", - "verified_result": null - } - ], - "scripts/lib/common.sh": [ - { - "hashed_secret": "cff0d14e4337fa8bdb68dfa906f04b0df6fad72f", - "is_secret": false, - "is_verified": false, - "line_number": 176, - "type": "Secret Keyword", - "verified_result": null - } - ], - "scripts/test-sso-flow.sh": [ - { - "hashed_secret": "174c2b8209d705f3db2cd2f6f84b79e6e27ca793", - "is_secret": false, - "is_verified": false, - "line_number": 82, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "d86a48c00ab62d2631c5cd5735471e7b5e34b7dc", - "is_secret": false, - "is_verified": false, - "line_number": 84, - "type": "Secret Keyword", - "verified_result": null - } - ], - "scripts/test_email_auth_api.py": [ - { - "hashed_secret": "55ae024ac61278627948dc9375b31bbbbfd7b442", - "is_secret": false, - "is_verified": false, - "line_number": 304, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "364d84ed2a75ef4c9a3cba031109db88e504511b", - "is_secret": false, - "is_verified": false, - "line_number": 329, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "0378e32875a5f7f922d2cce11b0741096d61c5f2", - "is_secret": false, - "is_verified": false, - "line_number": 483, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "8b5f2dff4edd3be66dd04ef8f9b6da4f6a4d0463", - "is_secret": false, - "is_verified": false, - "line_number": 506, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "1c5d7338a53cbdce0a6d26c4572e9725eb94ffc0", - "is_secret": false, - "is_verified": false, - "line_number": 519, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "673563640ec0c172e8d6f1316ae097269e3986c4", - "is_secret": false, - "is_verified": false, - "line_number": 527, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "0269ac00d06201bd7bcf234fed607b713469d2d0", - "is_secret": false, - "is_verified": false, - "line_number": 551, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "42d7bc4801cb72f2559bd9f6c9af9a2707ca321e", - "is_secret": false, - "is_verified": false, - "line_number": 742, - "type": "Secret Keyword", - "verified_result": null - } - ], - "scripts/test_mcp_client.py": [ - { - "hashed_secret": "1ee61ed8e67eecf5f1e97c4786b6728b478b8fae", - "is_secret": false, - "is_verified": false, - "line_number": 30, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "scripts/test_mcp_token_scoping.py": [ - { - "hashed_secret": "ae28b10c8c7ef67658ef1de7b29ee23556a32eef", - "is_secret": false, - "is_verified": false, - "line_number": 325, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2df14e4719f299249cd9a97cf68cc87232a27cbb", - "is_secret": false, - "is_verified": false, - "line_number": 348, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "tests/conftest.py": [ - { - "hashed_secret": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", - "is_secret": false, - "is_verified": false, - "line_number": 115, - "type": "Basic Auth Credentials", - "verified_result": null - }, - { - "hashed_secret": "206c80413b9a96c1312cc346b7d2517b84463edd", - "is_secret": false, - "is_verified": false, - "line_number": 164, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/e2e/mcp_test_helpers.py": [ - { - "hashed_secret": "07b7b454e840ca79e1582edcf973b051fc56e079", - "is_secret": false, - "is_verified": false, - "line_number": 39, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/e2e/test_admin_apis.py": [ - { - "hashed_secret": "ba9cdae9b74942b8ac45ec20dfc3803a07fe6de9", - "is_secret": false, - "is_verified": false, - "line_number": 58, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/e2e/test_admin_mcp_pool_metrics.py": [ - { - "hashed_secret": "a1f09a2e61748dd65f724a3e680c1895ecf4c2cd", - "is_secret": false, - "is_verified": false, - "line_number": 61, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/e2e/test_main_apis.py": [ - { - "hashed_secret": "ba9cdae9b74942b8ac45ec20dfc3803a07fe6de9", - "is_secret": false, - "is_verified": false, - "line_number": 80, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "4dfad2d5130a5bcaf2716c495de92da13f4389e0", - "is_secret": false, - "is_verified": false, - "line_number": 758, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/e2e/test_mcp_rbac_transport.py": [ - { - "hashed_secret": "6e2e8a100b17f5c9083b5f8c6f2ed58ae6286e5b", - "is_secret": false, - "is_verified": false, - "line_number": 892, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/e2e/test_oauth_protected_resource.py": [ - { - "hashed_secret": "ba9cdae9b74942b8ac45ec20dfc3803a07fe6de9", - "is_secret": false, - "is_verified": false, - "line_number": 58, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/e2e/test_translate_dynamic_env_e2e.py": [ - { - "hashed_secret": "528ceffb33609fca3a5646ec64a032d4362d301b", - "is_secret": false, - "is_verified": false, - "line_number": 201, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "d4f1a495029e6aec0aeb30aa3ce002dab645f08c", - "is_secret": false, - "is_verified": false, - "line_number": 290, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "fb6c2cbb5141e95639121909fe1b5a7012d1041a", - "is_secret": false, - "is_verified": false, - "line_number": 338, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "8590ea4c4485851eeb808692bf07b80640a3619e", - "is_secret": false, - "is_verified": false, - "line_number": 554, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/integration/test_concurrency_row_locking.py": [ - { - "hashed_secret": "03aca59f05abb8e7b8ceb737167b6cd48dfa0a3f", - "is_secret": false, - "is_verified": false, - "line_number": 62, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/integration/test_dcr_flow_integration.py": [ - { - "hashed_secret": "352b2a4120b46b2e0221e753a1272cd34a6aa0da", - "is_secret": false, - "is_verified": false, - "line_number": 144, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/integration/test_integration.py": [ - { - "hashed_secret": "718cbcc5a4207c0d5f38e3a309bdba17cb0074b7", - "is_secret": false, - "is_verified": false, - "line_number": 218, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "tests/integration/test_translate_dynamic_env.py": [ - { - "hashed_secret": "528ceffb33609fca3a5646ec64a032d4362d301b", - "is_secret": false, - "is_verified": false, - "line_number": 211, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/js/admin-ui.test.js": [ - { - "hashed_secret": "1ded3053d0363079a4e681a3b700435d6d880290", - "is_secret": false, - "is_verified": false, - "line_number": 157, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "0e5ce87ec4f3ba937574c210b09d984dbc1f1ece", - "is_secret": false, - "is_verified": false, - "line_number": 172, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "9dbbba966fd8f1a21e6f0d38ffa4afc69cca66b6", - "is_secret": false, - "is_verified": false, - "line_number": 213, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/load/README.md": [ - { - "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", - "is_secret": false, - "is_verified": false, - "line_number": 80, - "type": "Basic Auth Credentials", - "verified_result": null - } - ], - "tests/loadtest/locustfile.py": [ - { - "hashed_secret": "d033e22ae348aeb5660fc2140aec35850c4da997", - "is_secret": false, - "is_verified": false, - "line_number": 3784, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "d73bccb432c5c7b1b166cfa603b3b99af63fd580", - "is_secret": false, - "is_verified": false, - "line_number": 6627, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "20d22126242b5c7e253d84dd6ff0ed7a6d2662a2", - "is_secret": false, - "is_verified": false, - "line_number": 6983, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "fa9beb99e4029ad5a6615399e7bbae21356086b3", - "is_secret": false, - "is_verified": false, - "line_number": 7007, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/loadtest/locustfile_baseline.py": [ - { - "hashed_secret": "2df14e4719f299249cd9a97cf68cc87232a27cbb", - "is_secret": false, - "is_verified": false, - "line_number": 96, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "tests/loadtest/locustfile_echo_delay.py": [ - { - "hashed_secret": "293324f6824bb3a6db5c4dc42a60ddd4a9851c99", - "is_secret": false, - "is_verified": false, - "line_number": 103, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "tests/loadtest/locustfile_mcp_isolation.py": [ - { - "hashed_secret": "07b7b454e840ca79e1582edcf973b051fc56e079", - "is_secret": false, - "is_verified": false, - "line_number": 34, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/manual/README.md": [ - { - "hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3", - "is_secret": false, - "is_verified": false, - "line_number": 214, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3", - "is_secret": false, - "is_verified": false, - "line_number": 219, - "type": "Basic Auth Credentials", - "verified_result": null - } - ], - "tests/manual/concurrency/conc_01_gateways_results.md": [ - { - "hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3", - "is_secret": false, - "is_verified": false, - "line_number": 42, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3", - "is_secret": false, - "is_verified": false, - "line_number": 126, - "type": "Basic Auth Credentials", - "verified_result": null - } - ], - "tests/manual/concurrency/conc_02_gateways_results.md": [ - { - "hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3", - "is_secret": false, - "is_verified": false, - "line_number": 43, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3", - "is_secret": false, - "is_verified": false, - "line_number": 58, - "type": "Basic Auth Credentials", - "verified_result": null - } - ], - "tests/manual/concurrency/run_conc_01_gateways.sh": [ - { - "hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3", - "is_secret": false, - "is_verified": false, - "line_number": 9, - "type": "Basic Auth Credentials", - "verified_result": null - } - ], - "tests/manual/concurrency/run_conc_02_gateways.sh": [ - { - "hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3", - "is_secret": false, - "is_verified": false, - "line_number": 9, - "type": "Basic Auth Credentials", - "verified_result": null - } - ], - "tests/manual/testcases/api_a2a.yaml": [ - { - "hashed_secret": "69dba6dfc0be932d10cdbfaae770c300cd7e0e3c", - "is_secret": false, - "is_verified": false, - "line_number": 43, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "fd9a45bc0b07706d849cf85021ecf9123fa83d82", - "is_secret": false, - "is_verified": false, - "line_number": 54, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/manual/testcases/api_authentication.yaml": [ - { - "hashed_secret": "5d14de7c2c81aaa796123494202ba7d1d27b207b", - "is_secret": false, - "is_verified": false, - "line_number": 31, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "fa9beb99e4029ad5a6615399e7bbae21356086b3", - "is_secret": false, - "is_verified": false, - "line_number": 47, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "0d8343a9d687097938d7d9e630c5e37991949108", - "is_secret": false, - "is_verified": false, - "line_number": 110, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/manual/testcases/api_federation.yaml": [ - { - "hashed_secret": "fa9beb99e4029ad5a6615399e7bbae21356086b3", - "is_secret": false, - "is_verified": false, - "line_number": 39, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/manual/testcases/database_tests.yaml": [ - { - "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", - "is_secret": false, - "is_verified": false, - "line_number": 79, - "type": "Basic Auth Credentials", - "verified_result": null - } - ], - "tests/manual/testcases/migration_tests.yaml": [ - { - "hashed_secret": "5f8b73c6cddcd6dd3a2eb5747a25be2e71b6d202", - "is_secret": false, - "is_verified": false, - "line_number": 118, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "6082663664461e9cbe453fe5325a381f10dcddda", - "is_secret": false, - "is_verified": false, - "line_number": 149, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/manual/testcases/security_tests.yaml": [ - { - "hashed_secret": "656ae8968017cd346560397f967a2c2dbfb70f6f", - "is_secret": false, - "is_verified": false, - "line_number": 122, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/manual/testcases/setup_instructions.yaml": [ - { - "hashed_secret": "46e6a38b6d25f56087891d1152d4d082b5767b3e", - "is_secret": false, - "is_verified": false, - "line_number": 84, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/migration/conftest.py": [ - { - "hashed_secret": "1b255e8e77fb3fafbf1fe2013347f762fbca9cd6", - "is_secret": false, - "is_verified": false, - "line_number": 273, - "type": "Basic Auth Credentials", - "verified_result": null - } - ], - "tests/migration/docker-compose.test.yml": [ - { - "hashed_secret": "1b255e8e77fb3fafbf1fe2013347f762fbca9cd6", - "is_secret": false, - "is_verified": false, - "line_number": 18, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "1b255e8e77fb3fafbf1fe2013347f762fbca9cd6", - "is_secret": false, - "is_verified": false, - "line_number": 35, - "type": "Basic Auth Credentials", - "verified_result": null - } - ], - "tests/migration/utils/container_manager.py": [ - { - "hashed_secret": "1b255e8e77fb3fafbf1fe2013347f762fbca9cd6", - "is_secret": false, - "is_verified": false, - "line_number": 384, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/performance/MANUAL_TESTING.md": [ - { - "hashed_secret": "fa9beb99e4029ad5a6615399e7bbae21356086b3", - "is_secret": false, - "is_verified": false, - "line_number": 434, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/performance/PERFORMANCE_STRATEGY.md": [ - { - "hashed_secret": "b1d12a49a9da5edec445c6ea003be1456337bd3e", - "is_secret": false, - "is_verified": false, - "line_number": 1292, - "type": "Basic Auth Credentials", - "verified_result": null - }, - { - "hashed_secret": "b1d12a49a9da5edec445c6ea003be1456337bd3e", - "is_secret": false, - "is_verified": false, - "line_number": 1305, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "d033e22ae348aeb5660fc2140aec35850c4da997", - "is_secret": false, - "is_verified": false, - "line_number": 1334, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/performance/scenarios/gateway-core-benchmark.sh": [ - { - "hashed_secret": "fa9beb99e4029ad5a6615399e7bbae21356086b3", - "is_secret": false, - "is_verified": false, - "line_number": 196, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/performance/test_postgresql_percentile_performance.py": [ - { - "hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3", - "is_secret": false, - "is_verified": false, - "line_number": 59, - "type": "Basic Auth Credentials", - "verified_result": null - } - ], - "tests/performance/utils/generate_docker_compose.py": [ - { - "hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3", - "is_secret": false, - "is_verified": false, - "line_number": 71, - "type": "Basic Auth Credentials", - "verified_result": null - } - ], - "tests/playwright/README.md": [ - { - "hashed_secret": "0df7be1bea60575a7469f5e07e13124ce428d263", - "is_secret": false, - "is_verified": false, - "line_number": 138, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "fa9beb99e4029ad5a6615399e7bbae21356086b3", - "is_secret": false, - "is_verified": false, - "line_number": 142, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "64438ee426438161da88554b3e2de796b0ca265e", - "is_secret": false, - "is_verified": false, - "line_number": 146, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/playwright/conftest.py": [ - { - "hashed_secret": "d6d6b8c66ab3ade1bfcf206e7df58e296827ef17", - "is_secret": false, - "is_verified": false, - "line_number": 520, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "1c58bd92003bbaa0538e249fff6ee19a270dec5f", - "is_secret": false, - "is_verified": false, - "line_number": 585, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "03f5e2d670af3e9183f3fe790785b0d41291a17d", - "is_secret": false, - "is_verified": false, - "line_number": 629, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/playwright/operations/test_llm_config.py": [ - { - "hashed_secret": "2a3575e3c0abe9772b3f98c92e7d530d7f131514", - "is_secret": false, - "is_verified": false, - "line_number": 132, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/playwright/pytest.ini": [ - { - "hashed_secret": "176939638191d5a13935ccb90c0c8a70a5e30773", - "is_secret": false, - "is_verified": false, - "line_number": 8, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/playwright/security/conftest.py": [ - { - "hashed_secret": "07b7b454e840ca79e1582edcf973b051fc56e079", - "is_secret": false, - "is_verified": false, - "line_number": 31, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/playwright/security/owasp/conftest.py": [ - { - "hashed_secret": "07b7b454e840ca79e1582edcf973b051fc56e079", - "is_secret": false, - "is_verified": false, - "line_number": 24, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/playwright/security/owasp/test_a01_broken_access_control.py": [ - { - "hashed_secret": "8e45fe2388a6c4604ee0ccdba14ca0df092bc904", - "is_secret": false, - "is_verified": false, - "line_number": 238, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/playwright/security/test_playwright_security_e2e_top30.py": [ - { - "hashed_secret": "07b7b454e840ca79e1582edcf973b051fc56e079", - "is_secret": false, - "is_verified": false, - "line_number": 35, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "42d7bc4801cb72f2559bd9f6c9af9a2707ca321e", - "is_secret": false, - "is_verified": false, - "line_number": 585, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/playwright/security/test_sso_management.py": [ - { - "hashed_secret": "03f5e2d670af3e9183f3fe790785b0d41291a17d", - "is_secret": false, - "is_verified": false, - "line_number": 36, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/playwright/security/test_team_selector_e2e.py": [ - { - "hashed_secret": "b4e44716dbbf57be3dae2f819d96795a85d06652", - "is_secret": false, - "is_verified": false, - "line_number": 290, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/playwright/teams/conftest.py": [ - { - "hashed_secret": "07b7b454e840ca79e1582edcf973b051fc56e079", - "is_secret": false, - "is_verified": false, - "line_number": 26, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/playwright/test_admin_menu_visibility.py": [ - { - "hashed_secret": "119ffab9fda36e29816a09097c441eb8bcd8b684", - "is_secret": false, - "is_verified": false, - "line_number": 45, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/playwright/test_agents.py": [ - { - "hashed_secret": "03f5e2d670af3e9183f3fe790785b0d41291a17d", - "is_secret": false, - "is_verified": false, - "line_number": 263, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/playwright/test_rbac_permissions.py": [ - { - "hashed_secret": "119ffab9fda36e29816a09097c441eb8bcd8b684", - "is_secret": false, - "is_verified": false, - "line_number": 46, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/populate/cleanup.py": [ - { - "hashed_secret": "c871a5b8b2e511a525b09edea0b9a4d9f46401e5", - "is_secret": false, - "is_verified": false, - "line_number": 42, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/populate/populators/users.py": [ - { - "hashed_secret": "c871a5b8b2e511a525b09edea0b9a4d9f46401e5", - "is_secret": false, - "is_verified": false, - "line_number": 14, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/populate/verify.py": [ - { - "hashed_secret": "c871a5b8b2e511a525b09edea0b9a4d9f46401e5", - "is_secret": false, - "is_verified": false, - "line_number": 120, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/security/test_input_validation.py": [ - { - "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", - "is_secret": false, - "is_verified": false, - "line_number": 286, - "type": "Basic Auth Credentials", - "verified_result": null - }, - { - "hashed_secret": "120340c3d011e9621b9bb88b5884221b25251b88", - "is_secret": false, - "is_verified": false, - "line_number": 2063, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/db/test_a2a_agents_auth_value_migration.py": [ - { - "hashed_secret": "52294c20155b397fbf140baa0cfb6d9400c82c25", - "is_secret": false, - "is_verified": false, - "line_number": 27, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9b094435d625c9385a1bb083604f87fa7c225da0", - "is_secret": false, - "is_verified": false, - "line_number": 28, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "tests/unit/mcpgateway/db/test_observability_migrations.py": [ - { - "hashed_secret": "9d88d7eece452a76b2955ecfbb251f23cf7b7905", - "is_secret": false, - "is_verified": false, - "line_number": 335, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "tests/unit/mcpgateway/db/test_rbac_permission_backfill_migration.py": [ - { - "hashed_secret": "dc7c15edd98c7c7ce4cd107fb4ccb17329510002", - "is_secret": false, - "is_verified": false, - "line_number": 27, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "tests/unit/mcpgateway/db/test_token_uniqueness_migration.py": [ - { - "hashed_secret": "45fc9ab5f41816c6979df8bc85ffdc160a0e696a", - "is_secret": false, - "is_verified": false, - "line_number": 25, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "c4770f062e9bfa8ec054475f98315913da1d16fd", - "is_secret": false, - "is_verified": false, - "line_number": 26, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "tests/unit/mcpgateway/middleware/test_auth_method_propagation.py": [ - { - "hashed_secret": "e7c4d1ba7ce017155823536efd57843fa15ef759", - "is_secret": false, - "is_verified": false, - "line_number": 52, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/middleware/test_http_auth_headers.py": [ - { - "hashed_secret": "65882b5e8dbab0e649474b2a626c1d24e1b317f5", - "is_secret": false, - "is_verified": false, - "line_number": 82, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "35e6ca489e73a8991165cdcfc2b79eb0d25ed0a3", - "is_secret": false, - "is_verified": false, - "line_number": 103, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "59df6723fb54fd8dde6dcf51034e51491bfbff24", - "is_secret": false, - "is_verified": false, - "line_number": 909, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "9b4cd28c8ecd18d533601bbb7d3345b9d007bd80", - "is_secret": false, - "is_verified": false, - "line_number": 915, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/middleware/test_http_auth_integration.py": [ - { - "hashed_secret": "6f865e7e20c773e3483eaa74a07d03cec02bde2e", - "is_secret": false, - "is_verified": false, - "line_number": 181, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "d58f8c42247f1cf100bf63f196d93d7d302e8ca9", - "is_secret": false, - "is_verified": false, - "line_number": 202, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "3acfb2c2b433c0ea7ff107e33df91b18e52f960f", - "is_secret": false, - "is_verified": false, - "line_number": 272, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "e48c47f1431afd3bb030deea266b4309e2228c5b", - "is_secret": false, - "is_verified": false, - "line_number": 284, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "4155f3a6f21409ec6066b6c974b058d3ea4a2c5c", - "is_secret": false, - "is_verified": false, - "line_number": 466, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "84a36e47e8bfcca13318bca53cdcb1270e2c4b9e", - "is_secret": false, - "is_verified": false, - "line_number": 487, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "c48ea9182291ca3f527914fd7d35a698830931d0", - "is_secret": false, - "is_verified": false, - "line_number": 525, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "534c8ebac109fde87e1eb0c6e35249fc8a38278a", - "is_secret": false, - "is_verified": false, - "line_number": 681, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/middleware/test_rbac.py": [ - { - "hashed_secret": "ab73a3eaca01a7059dcdff6f95556ec7fd83de96", - "is_secret": false, - "is_verified": false, - "line_number": 1389, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "92dd4a2de441b63e6ac51fdb81a05a416dffd182", - "is_secret": false, - "is_verified": false, - "line_number": 1476, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/middleware/test_request_logging_middleware.py": [ - { - "hashed_secret": "1a91d62f7ca67399625a4368a6ab5d4a3baa6073", - "is_secret": false, - "is_verified": false, - "line_number": 98, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "dbdab9be92cacdae6a97e8601332bfaa8545800f", - "is_secret": false, - "is_verified": false, - "line_number": 99, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "4ea8d2335b430796cf3f500368c5b0f5b1dc90f5", - "is_secret": false, - "is_verified": false, - "line_number": 151, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/plugins/framework/external/grpc/test_grpc_models.py": [ - { - "hashed_secret": "81f344a7686a80b4c5293e8fdc0b0160c82c06a8", - "is_secret": false, - "is_verified": false, - "line_number": 106, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "623e76c36aa2a886542011e28412cc761d7ceb01", - "is_secret": false, - "is_verified": false, - "line_number": 110, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "48a7b8889e1542650266c14a18c8708488fa1951", - "is_secret": false, - "is_verified": false, - "line_number": 132, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/plugins/framework/external/grpc/test_tls_utils.py": [ - { - "hashed_secret": "623e76c36aa2a886542011e28412cc761d7ceb01", - "is_secret": false, - "is_verified": false, - "line_number": 225, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "48a7b8889e1542650266c14a18c8708488fa1951", - "is_secret": false, - "is_verified": false, - "line_number": 248, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/plugins/framework/external/mcp/test_tls_utils.py": [ - { - "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", - "is_secret": false, - "is_verified": false, - "line_number": 104, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9", - "is_secret": false, - "is_verified": false, - "line_number": 230, - "type": "Private Key", - "verified_result": null - } - ], - "tests/unit/mcpgateway/plugins/framework/hooks/test_http.py": [ - { - "hashed_secret": "5dc0dc43e8ac987476af951a9e5bed1b19b24028", - "is_secret": false, - "is_verified": false, - "line_number": 247, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "7e7f74d2579ebec3b3f2c125e756269a29782497", - "is_secret": false, - "is_verified": false, - "line_number": 248, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "7b20dc22a6cb086e0d990ccc74ec819b16dc68b8", - "is_secret": false, - "is_verified": false, - "line_number": 265, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "ee977806d7286510da8b9a7492ba58e2484c0ecc", - "is_secret": false, - "is_verified": false, - "line_number": 311, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "f9ac14b63a75faf57d8db6f919bfabb2502d273c", - "is_secret": false, - "is_verified": false, - "line_number": 325, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/plugins/framework/test_plugin_models_coverage.py": [ - { - "hashed_secret": "48a7b8889e1542650266c14a18c8708488fa1951", - "is_secret": false, - "is_verified": false, - "line_number": 233, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "81f344a7686a80b4c5293e8fdc0b0160c82c06a8", - "is_secret": false, - "is_verified": false, - "line_number": 238, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/plugins/framework/test_policies.py": [ - { - "hashed_secret": "37c6c57bedf4305ef41249c1794760b5cb8fad17", - "is_secret": false, - "is_verified": false, - "line_number": 121, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "d73ef92426f2b11dfc4aed4d4bfc41c49ee1087c", - "is_secret": false, - "is_verified": false, - "line_number": 593, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "e812ba8d00b270ef3502bb53ceb31e8c5188f14e", - "is_secret": false, - "is_verified": false, - "line_number": 760, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "bf31c52c32b0fcda1dc3e7c0c91803a7d1c9e891", - "is_secret": false, - "is_verified": false, - "line_number": 774, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/plugins/framework/test_validators.py": [ - { - "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", - "is_secret": false, - "is_verified": false, - "line_number": 85, - "type": "Basic Auth Credentials", - "verified_result": null - } - ], - "tests/unit/mcpgateway/plugins/plugins/content_moderation/test_content_moderation.py": [ - { - "hashed_secret": "0bc02c6c973caffbda3e15adcb2f39c41634c728", - "is_secret": false, - "is_verified": false, - "line_number": 40, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "75ddfb45216fe09680dfe70eda4f559a910c832c", - "is_secret": false, - "is_verified": false, - "line_number": 247, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/plugins/plugins/content_moderation/test_content_moderation_integration.py": [ - { - "hashed_secret": "0bc02c6c973caffbda3e15adcb2f39c41634c728", - "is_secret": false, - "is_verified": false, - "line_number": 301, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "8cdf8b16b80c4f3f6e6af135b3c72efd706723c0", - "is_secret": false, - "is_verified": false, - "line_number": 381, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/plugins/plugins/header_filter/test_header_filter_plugin.py": [ - { - "hashed_secret": "a62f2225bf70bfaccbc7f1ef2a397836717377de", - "is_secret": false, - "is_verified": false, - "line_number": 417, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "83ff9f4e0d16d61727cbdf47d769fb707b652217", - "is_secret": false, - "is_verified": false, - "line_number": 462, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/plugins/plugins/pii_filter/test_pii_filter.py": [ - { - "hashed_secret": "25910f981e85ca04baf359199dd0bd4a3ae738b6", - "is_secret": false, - "is_verified": false, - "line_number": 505, - "type": "AWS Access Key", - "verified_result": null - } - ], - "tests/unit/mcpgateway/plugins/plugins/tools_telemetry_exporter/test_tools_telemetry_exporter.py": [ - { - "hashed_secret": "e8af0e18ff4805f4efd84f58b0fa69e3780f35a4", - "is_secret": false, - "is_verified": false, - "line_number": 62, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "cc84fa5a361f86a589169fde1e4e6d62bc786e6c", - "is_secret": false, - "is_verified": false, - "line_number": 143, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "tests/unit/mcpgateway/plugins/plugins/virus_total_checker/test_virus_total_checker.py": [ - { - "hashed_secret": "829c3804401b0727f70f73d4415e162400cbe57b", - "is_secret": false, - "is_verified": false, - "line_number": 436, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/plugins/plugins/webhook_notification/test_webhook_integration.py": [ - { - "hashed_secret": "a8ae1520ce69381a9cf8dbd5082d8512fcace493", - "is_secret": false, - "is_verified": false, - "line_number": 216, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/plugins/plugins/webhook_notification/test_webhook_notification.py": [ - { - "hashed_secret": "fe1bae27cb7c1fb823f496f286e78f1d2ae87734", - "is_secret": false, - "is_verified": false, - "line_number": 110, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "2e7a7ee14caebf378fc32d6cf6f557f347c96773", - "is_secret": false, - "is_verified": false, - "line_number": 210, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/routers/test_auth.py": [ - { - "hashed_secret": "6eb67d95dba1a614971e31e78146d44bd4a3ada3", - "is_secret": false, - "is_verified": false, - "line_number": 191, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "cbfdac6008f9cab4083784cbd1874f76618d2a97", - "is_secret": false, - "is_verified": false, - "line_number": 271, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/routers/test_email_auth_router.py": [ - { - "hashed_secret": "cbfdac6008f9cab4083784cbd1874f76618d2a97", - "is_secret": false, - "is_verified": false, - "line_number": 165, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "de14fdc5a7f3843d8db79053625a2169a4eed921", - "is_secret": false, - "is_verified": false, - "line_number": 523, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "f3e12d2b914fae6510cff60e40b097c0962654a1", - "is_secret": false, - "is_verified": false, - "line_number": 658, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "e6b6afbd6d76bb5d2041542d7d2e3fac5bb05593", - "is_secret": false, - "is_verified": false, - "line_number": 1422, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "23ace7331ef30c45051de4e683719db7391b9980", - "is_secret": false, - "is_verified": false, - "line_number": 1491, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "6aa6f10deee84f739ae8aca12a2755a2d0e103ac", - "is_secret": false, - "is_verified": false, - "line_number": 1595, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "e577bc0464e5dd0052fc9ab532b0630e58173f92", - "is_secret": false, - "is_verified": false, - "line_number": 1626, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "ef7d0dca079f43e7952971148dfaba10fbcce609", - "is_secret": false, - "is_verified": false, - "line_number": 1643, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "789b49606c321c8cf228d17942608eff0ccc4171", - "is_secret": false, - "is_verified": false, - "line_number": 1677, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "dc8002865f92070749b264e76045b04fa3b8de71", - "is_secret": false, - "is_verified": false, - "line_number": 2136, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/routers/test_llm_admin_router.py": [ - { - "hashed_secret": "f7a75342741955b2b9b66b55a78e5061f321ff44", - "is_secret": false, - "is_verified": false, - "line_number": 532, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "61ba747fe2f5b3d513c40550084c6284fbbc1094", - "is_secret": false, - "is_verified": false, - "line_number": 538, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/routers/test_llmchat_router.py": [ - { - "hashed_secret": "baf4e5770bde2def49611ed9c852b518038db759", - "is_secret": false, - "is_verified": false, - "line_number": 126, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/routers/test_oauth_router.py": [ - { - "hashed_secret": "72cb70dbbafe97e5ea13ad88acd65d08389439b0", - "is_secret": false, - "is_verified": false, - "line_number": 137, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/routers/test_reverse_proxy.py": [ - { - "hashed_secret": "c56486f8b638f63e04251d0c8ab0b4fbfee8e06b", - "is_secret": false, - "is_verified": false, - "line_number": 796, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/routers/test_teams.py": [ - { - "hashed_secret": "1e418031f4ec8505cff3cde4f41efc0ee83ec2d1", - "is_secret": false, - "is_verified": false, - "line_number": 254, - "type": "Base64 High Entropy String", - "verified_result": null - } - ], - "tests/unit/mcpgateway/services/test_a2a_query_param_auth.py": [ - { - "hashed_secret": "99834bc4eff3f1e1c1e4692d2476b593b501d045", - "is_secret": false, - "is_verified": false, - "line_number": 277, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "f4cbfd23b14432ac406aacc683f2a6485e52f356", - "is_secret": false, - "is_verified": false, - "line_number": 340, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "f5e6c51ff9f3703a753cde41e4cc8f506bc809db", - "is_secret": false, - "is_verified": false, - "line_number": 341, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "6108f529090a823e5cbaefba71604ccecc021ecc", - "is_secret": false, - "is_verified": false, - "line_number": 426, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "e3de258032685ca3a157e704090921039ff7f0b3", - "is_secret": false, - "is_verified": false, - "line_number": 555, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/services/test_a2a_service.py": [ - { - "hashed_secret": "41db7c65f665e40b44178f95f5f86473ca17160e", - "is_secret": false, - "is_verified": false, - "line_number": 72, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "4ea8d2335b430796cf3f500368c5b0f5b1dc90f5", - "is_secret": false, - "is_verified": false, - "line_number": 190, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "1a91d62f7ca67399625a4368a6ab5d4a3baa6073", - "is_secret": false, - "is_verified": false, - "line_number": 191, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "5ef3af6cd9b5aa907fa618fac829baaec6763b42", - "is_secret": false, - "is_verified": false, - "line_number": 385, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "94c9325c8ade4f6327d87e240713c2fd7fdbfc63", - "is_secret": false, - "is_verified": false, - "line_number": 386, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "7bb0c6efd2edb1ae20dab6dd260895ad8895e07e", - "is_secret": false, - "is_verified": false, - "line_number": 422, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "ddbeee31dc29b74fcede0bea4d3fc5276d9148c8", - "is_secret": false, - "is_verified": false, - "line_number": 448, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "122758dd535bc6d246f36a686b29c7c40fab1315", - "is_secret": false, - "is_verified": false, - "line_number": 799, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "fa727f587e9f6115da6d4eb600dd8b6fe06cf69a", - "is_secret": false, - "is_verified": false, - "line_number": 801, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "250cdef3ce09000fa9a939a13e5f73433d96a852", - "is_secret": false, - "is_verified": false, - "line_number": 2354, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", - "is_secret": false, - "is_verified": false, - "line_number": 2363, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "1902e3d6fc4e78a0bcc50ba12b882769afbf4a8c", - "is_secret": false, - "is_verified": false, - "line_number": 2395, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "d92342976d720ff38cf5dcb329be41959ab1ba6c", - "is_secret": false, - "is_verified": false, - "line_number": 2816, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "a343c9b5b1abfcf385d5c6f6dc0282f4d88a416e", - "is_secret": false, - "is_verified": false, - "line_number": 2977, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "c00dbbc9dadfbe1e232e93a729dd4752fade0abf", - "is_secret": false, - "is_verified": false, - "line_number": 3009, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/services/test_argon2_service.py": [ - { - "hashed_secret": "f13733f6dd9f1ed3118e2da31428c71eab5ffd99", - "is_secret": false, - "is_verified": false, - "line_number": 79, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "b5bc013af872265e389b3abee36dd4932a206ab8", - "is_secret": false, - "is_verified": false, - "line_number": 152, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "4be80aee9bf2223071eedfc002ba2546abd1f5ea", - "is_secret": false, - "is_verified": false, - "line_number": 230, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "9fb7fe1217aed442b04c0f5e43b5d5a7d3287097", - "is_secret": false, - "is_verified": false, - "line_number": 388, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "cbfdac6008f9cab4083784cbd1874f76618d2a97", - "is_secret": false, - "is_verified": false, - "line_number": 487, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "6d512cc3d1a73d1f0a7331746483447a60b9bd98", - "is_secret": false, - "is_verified": false, - "line_number": 538, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/services/test_async_crypto_wrappers.py": [ - { - "hashed_secret": "5a0aee0f3af308cd6d74d617fde6592c2bc94fa3", - "is_secret": false, - "is_verified": false, - "line_number": 44, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "b5bc013af872265e389b3abee36dd4932a206ab8", - "is_secret": false, - "is_verified": false, - "line_number": 74, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "d4bb297c014026ace455b1ad5926033d795bf016", - "is_secret": false, - "is_verified": false, - "line_number": 98, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "6d512cc3d1a73d1f0a7331746483447a60b9bd98", - "is_secret": false, - "is_verified": false, - "line_number": 112, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/services/test_catalog_service.py": [ - { - "hashed_secret": "816cc20437d859538736e1ef46558b7bda486c06", - "is_secret": false, - "is_verified": false, - "line_number": 563, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/services/test_correlation_id_json_formatter.py": [ - { - "hashed_secret": "125fbc14773f228e72f16d55be21bad750d30b19", - "is_secret": false, - "is_verified": false, - "line_number": 129, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ff998abc1ce6d8f01a675fa197368e44c8916e9c", - "is_secret": false, - "is_verified": false, - "line_number": 130, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "tests/unit/mcpgateway/services/test_dcr_service.py": [ - { - "hashed_secret": "352b2a4120b46b2e0221e753a1272cd34a6aa0da", - "is_secret": false, - "is_verified": false, - "line_number": 404, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "290180f809e70dd1bc99a4c65bd669f43dde426d", - "is_secret": false, - "is_verified": false, - "line_number": 650, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "945db841c03e42eef2f3d0a4ff310e2f3b3e59ec", - "is_secret": false, - "is_verified": false, - "line_number": 785, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/services/test_email_auth_basic.py": [ - { - "hashed_secret": "5a0aee0f3af308cd6d74d617fde6592c2bc94fa3", - "is_secret": false, - "is_verified": false, - "line_number": 283, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "1816851d10187b57a93235b52495e615629f906d", - "is_secret": false, - "is_verified": false, - "line_number": 701, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "454f9bfcc5f7523c755896f9d2c7281f83aec668", - "is_secret": false, - "is_verified": false, - "line_number": 727, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "aafdc23870ecbcd3d557b6423a8982134e17927e", - "is_secret": false, - "is_verified": false, - "line_number": 754, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "e8662cfb96bd9c7fe84c31d76819ec3a92c80e63", - "is_secret": false, - "is_verified": false, - "line_number": 959, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "b5bc013af872265e389b3abee36dd4932a206ab8", - "is_secret": false, - "is_verified": false, - "line_number": 991, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "be57d8e1a2a7a4e5c034e7790659309f5e5539c0", - "is_secret": false, - "is_verified": false, - "line_number": 1066, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "b4edd1d6f455752e0b214407e561d8d3823204fe", - "is_secret": false, - "is_verified": false, - "line_number": 1280, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "562c20a72724744e2529d163c304a3ec32d09c0a", - "is_secret": false, - "is_verified": false, - "line_number": 1504, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "addbd3aa5619f2932733104eb8ceef08f6fd2693", - "is_secret": false, - "is_verified": false, - "line_number": 1516, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "e9e4a6d29515c8e53e4df7bc6646a23237b8f862", - "is_secret": false, - "is_verified": false, - "line_number": 1579, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "6aa10d711ca6f233cc7d7e9c36a980cbbd6b1ed1", - "is_secret": false, - "is_verified": false, - "line_number": 1601, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "bdb6e328018a96636bca051255228c0d2a3543b5", - "is_secret": false, - "is_verified": false, - "line_number": 1643, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "36abac195cd57009a1013913a7ec5b2677faf5fd", - "is_secret": false, - "is_verified": false, - "line_number": 1660, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "4015d13806dacd6cf5436ce218b7f755ab9ddab3", - "is_secret": false, - "is_verified": false, - "line_number": 2176, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "dc8002865f92070749b264e76045b04fa3b8de71", - "is_secret": false, - "is_verified": false, - "line_number": 2261, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/services/test_email_auth_service.py": [ - { - "hashed_secret": "862511dc4b5b31885dd85a83c2af1a4adc685f19", - "is_secret": false, - "is_verified": false, - "line_number": 389, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/services/test_email_auth_service_admin_role_sync.py": [ - { - "hashed_secret": "759521118cabf4f8bbc46e6d6344d0e700aa4be5", - "is_secret": false, - "is_verified": false, - "line_number": 50, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "6c55803d6f1d7a177a0db3eb4b343b0d50f9c111", - "is_secret": false, - "is_verified": false, - "line_number": 321, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "6fa5edecdc44e7209e0678c5dea72a95d63bf554", - "is_secret": false, - "is_verified": false, - "line_number": 365, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/services/test_email_notification_service.py": [ - { - "hashed_secret": "584db8632ab5718672aab8670add109a862fff0c", - "is_secret": false, - "is_verified": false, - "line_number": 154, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/services/test_encryption_service.py": [ - { - "hashed_secret": "d6b66ddd9ea7dbe760114bfe9a97352a5e139134", - "is_secret": false, - "is_verified": false, - "line_number": 376, - "type": "JSON Web Token", - "verified_result": null - }, - { - "hashed_secret": "c6c580101c1bdaaad4e360d0679d7a8cac514e4f", - "is_secret": false, - "is_verified": false, - "line_number": 378, - "type": "JSON Web Token", - "verified_result": null - }, - { - "hashed_secret": "755f997eb6296e47fd6d145e1b541b9e98a4fab1", - "is_secret": false, - "is_verified": false, - "line_number": 416, - "type": "Base64 High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2538fa47a21f3a58be26e62c63b6dd0a6e87dae5", - "is_secret": false, - "is_verified": false, - "line_number": 417, - "type": "Base64 High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "25910f981e85ca04baf359199dd0bd4a3ae738b6", - "is_secret": false, - "is_verified": false, - "line_number": 418, - "type": "AWS Access Key", - "verified_result": null - }, - { - "hashed_secret": "e8af0e18ff4805f4efd84f58b0fa69e3780f35a4", - "is_secret": false, - "is_verified": false, - "line_number": 718, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "1a91d62f7ca67399625a4368a6ab5d4a3baa6073", - "is_secret": false, - "is_verified": false, - "line_number": 719, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "290180f809e70dd1bc99a4c65bd669f43dde426d", - "is_secret": false, - "is_verified": false, - "line_number": 781, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "eea0d9a29d51f43612c303ff2f64b57f56dad885", - "is_secret": false, - "is_verified": false, - "line_number": 782, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "2cee75752f97573ff8cae445096cc30e992b55c7", - "is_secret": false, - "is_verified": false, - "line_number": 803, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/services/test_export_service.py": [ - { - "hashed_secret": "a10b98d7340036e9c8c301704f623eddd733cc1a", - "is_secret": false, - "is_verified": false, - "line_number": 1162, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "6578fca8b62f49457e4cd7e66554a310a1a64be8", - "is_secret": false, - "is_verified": false, - "line_number": 1753, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/services/test_gateway_query_param_auth.py": [ - { - "hashed_secret": "67ece584ee3884563e532a35b1616e5aefc2c121", - "is_secret": false, - "is_verified": false, - "line_number": 205, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", - "is_secret": false, - "is_verified": false, - "line_number": 229, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/services/test_gateway_service.py": [ - { - "hashed_secret": "4ea8d2335b430796cf3f500368c5b0f5b1dc90f5", - "is_secret": false, - "is_verified": false, - "line_number": 634, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "57b2ad99044d337197c0c39fd3823568ff81e48a", - "is_secret": false, - "is_verified": false, - "line_number": 635, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "104c9bb58d7eb1aa5ab82080cd06f1134ac6040b", - "is_secret": false, - "is_verified": false, - "line_number": 1470, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "99834bc4eff3f1e1c1e4692d2476b593b501d045", - "is_secret": false, - "is_verified": false, - "line_number": 4908, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", - "is_secret": false, - "is_verified": false, - "line_number": 4913, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "1a91d62f7ca67399625a4368a6ab5d4a3baa6073", - "is_secret": false, - "is_verified": false, - "line_number": 6450, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "2a23dd4f2e8b50315e601a2867ea7b7bf5a43b8c", - "is_secret": false, - "is_verified": false, - "line_number": 7047, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "ed3e0017cb8e4b06a59af1a441f62cbe58d2ef59", - "is_secret": false, - "is_verified": false, - "line_number": 7066, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "b55c6dc4705dba8b151c59566175ad845d5a9104", - "is_secret": false, - "is_verified": false, - "line_number": 7272, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "3654bd5ef523a79741766b7c3f22228fd43c3836", - "is_secret": false, - "is_verified": false, - "line_number": 7291, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/services/test_gateway_service_extended.py": [ - { - "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", - "is_secret": false, - "is_verified": false, - "line_number": 183, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/services/test_gateway_service_health_oauth.py": [ - { - "hashed_secret": "277fd76456880437641f76de1bfa6d7ef61ae861", - "is_secret": false, - "is_verified": false, - "line_number": 571, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/services/test_gateway_service_oauth_comprehensive.py": [ - { - "hashed_secret": "ee131046fbfad0f5fac08926f9b928539da950cd", - "is_secret": false, - "is_verified": false, - "line_number": 102, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "72cb70dbbafe97e5ea13ad88acd65d08389439b0", - "is_secret": false, - "is_verified": false, - "line_number": 613, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "752cb354d4d101b52581dc820d6fa3f29e87a776", - "is_secret": false, - "is_verified": false, - "line_number": 635, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/services/test_import_service.py": [ - { - "hashed_secret": "9b4cd28c8ecd18d533601bbb7d3345b9d007bd80", - "is_secret": false, - "is_verified": false, - "line_number": 308, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "8d974e916edd2f663f202c91a1d775f32f7c8d62", - "is_secret": false, - "is_verified": false, - "line_number": 517, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "4b0873b633484d5de20b2d8f852a8c07b43336bc", - "is_secret": false, - "is_verified": false, - "line_number": 1449, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "2d6e80602a17e8309ee0317358582a1207e2fd44", - "is_secret": false, - "is_verified": false, - "line_number": 1602, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "bfbb13b4405a850385a367a1cb668c0fa8b00f3c", - "is_secret": false, - "is_verified": false, - "line_number": 1613, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "dff6d4ff5dc357cf451d1855ab9cbda562645c9f", - "is_secret": false, - "is_verified": false, - "line_number": 2504, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "1073ab6cda4b991cd29f9e83a307f34004ae9327", - "is_secret": false, - "is_verified": false, - "line_number": 2521, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "277fd76456880437641f76de1bfa6d7ef61ae861", - "is_secret": false, - "is_verified": false, - "line_number": 2670, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/services/test_llm_provider_service.py": [ - { - "hashed_secret": "51af665d1afaf4f399863df27521b41e6ac2484e", - "is_secret": false, - "is_verified": false, - "line_number": 112, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "133e3d6b775613651eaedbf7e609d244f2f852af", - "is_secret": false, - "is_verified": false, - "line_number": 269, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "83a5b8a7b2e181736b4cad2391e48691b4434fdb", - "is_secret": false, - "is_verified": false, - "line_number": 540, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "54ec81ecf625d2640f2d27dbb8343cb6ee1b7348", - "is_secret": false, - "is_verified": false, - "line_number": 551, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "a5645bb67778c147a3b366da521477d254f5c4e8", - "is_secret": false, - "is_verified": false, - "line_number": 769, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "ee977806d7286510da8b9a7492ba58e2484c0ecc", - "is_secret": false, - "is_verified": false, - "line_number": 775, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/services/test_llm_proxy_service.py": [ - { - "hashed_secret": "a5645bb67778c147a3b366da521477d254f5c4e8", - "is_secret": false, - "is_verified": false, - "line_number": 434, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "e48c47f1431afd3bb030deea266b4309e2228c5b", - "is_secret": false, - "is_verified": false, - "line_number": 447, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "277fd76456880437641f76de1bfa6d7ef61ae861", - "is_secret": false, - "is_verified": false, - "line_number": 460, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/services/test_mcp_client_chat_service.py": [ - { - "hashed_secret": "fb0123cbab73d8a58896fbe39b8b7b5b6df15c5e", - "is_secret": false, - "is_verified": false, - "line_number": 69, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "a022de4a00e7998ae897308ffaaf884fabb9ee1f", - "is_secret": false, - "is_verified": false, - "line_number": 100, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "766a6e8998aefe2117d62c2d48411e43161ffa11", - "is_secret": false, - "is_verified": false, - "line_number": 125, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "76ce15edd5a8548603b6fd281d185f323234758f", - "is_secret": false, - "is_verified": false, - "line_number": 148, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "e2c786c60e8fb4620403ebbd4ec4fd8e8766370e", - "is_secret": false, - "is_verified": false, - "line_number": 167, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "a62f2225bf70bfaccbc7f1ef2a397836717377de", - "is_secret": false, - "is_verified": false, - "line_number": 187, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/services/test_mcp_client_chat_service_extended.py": [ - { - "hashed_secret": "0474aee45985f5ae829f53849df476200e876990", - "is_secret": false, - "is_verified": false, - "line_number": 256, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "277fd76456880437641f76de1bfa6d7ef61ae861", - "is_secret": false, - "is_verified": false, - "line_number": 337, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "a022de4a00e7998ae897308ffaaf884fabb9ee1f", - "is_secret": false, - "is_verified": false, - "line_number": 807, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "766a6e8998aefe2117d62c2d48411e43161ffa11", - "is_secret": false, - "is_verified": false, - "line_number": 1155, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "64bd05d89142566144bce27f4904a322d8d9e836", - "is_secret": false, - "is_verified": false, - "line_number": 1191, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "a62f2225bf70bfaccbc7f1ef2a397836717377de", - "is_secret": false, - "is_verified": false, - "line_number": 1289, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "d576acab7ea77edc8aa4f9ebea6bd4c1cc0d1764", - "is_secret": false, - "is_verified": false, - "line_number": 1554, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/services/test_mcp_session_pool.py": [ - { - "hashed_secret": "90bd1b48e958257948487b90bee080ba5ed00caa", - "is_secret": false, - "is_verified": false, - "line_number": 1478, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "tests/unit/mcpgateway/services/test_metrics.py": [ - { - "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", - "is_secret": false, - "is_verified": false, - "line_number": 164, - "type": "Basic Auth Credentials", - "verified_result": null - } - ], - "tests/unit/mcpgateway/services/test_oauth_manager.py": [ - { - "hashed_secret": "34e587c8f9ba011db386d719d66ffe3cfaea5447", - "is_secret": false, - "is_verified": false, - "line_number": 399, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "a0f4ea7d91495df92bbac2e2149dfb850fe81396", - "is_secret": false, - "is_verified": false, - "line_number": 419, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "920a25ef686c4f7ca6ad23dd109d3ad653161832", - "is_secret": false, - "is_verified": false, - "line_number": 458, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "a62f2225bf70bfaccbc7f1ef2a397836717377de", - "is_secret": false, - "is_verified": false, - "line_number": 535, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "355e7ab792a8403301eb0732bab9d2b3950ac048", - "is_secret": false, - "is_verified": false, - "line_number": 538, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/services/test_oauth_manager_pkce.py": [ - { - "hashed_secret": "e582429052cdb908833560f6f7582d232de37c4d", - "is_secret": false, - "is_verified": false, - "line_number": 58, - "type": "Base64 High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "fe1bae27cb7c1fb823f496f286e78f1d2ae87734", - "is_secret": false, - "is_verified": false, - "line_number": 352, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "72cb70dbbafe97e5ea13ad88acd65d08389439b0", - "is_secret": false, - "is_verified": false, - "line_number": 475, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/services/test_observability_service.py": [ - { - "hashed_secret": "0a24796d4c71ce722a92f450f69dc36c60b21de4", - "is_secret": false, - "is_verified": false, - "line_number": 368, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8750a512f22c55598175aee8790ae2470ec88d16", - "is_secret": false, - "is_verified": false, - "line_number": 369, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "tests/unit/mcpgateway/services/test_resource_service.py": [ - { - "hashed_secret": "b0beaa298b4c296ba29df08b919548d17e68d6c8", - "is_secret": false, - "is_verified": false, - "line_number": 3496, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", - "is_secret": false, - "is_verified": false, - "line_number": 3511, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "718cbcc5a4207c0d5f38e3a309bdba17cb0074b7", - "is_secret": false, - "is_verified": false, - "line_number": 4203, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "tests/unit/mcpgateway/services/test_server_service.py": [ - { - "hashed_secret": "c377074d6473f35a91001981355da793dc808ffd", - "is_secret": false, - "is_verified": false, - "line_number": 1406, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4ea8d2335b430796cf3f500368c5b0f5b1dc90f5", - "is_secret": false, - "is_verified": false, - "line_number": 1494, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "57b2ad99044d337197c0c39fd3823568ff81e48a", - "is_secret": false, - "is_verified": false, - "line_number": 1495, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "e8af0e18ff4805f4efd84f58b0fa69e3780f35a4", - "is_secret": false, - "is_verified": false, - "line_number": 1635, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "25ab86bed149ca6ca9c1c0d5db7c9a91388ddeab", - "is_secret": false, - "is_verified": false, - "line_number": 1636, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "498e738c70c40fb156a240d26f866f2d50e9cb51", - "is_secret": false, - "is_verified": false, - "line_number": 1848, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "a6bf0a0e01efe836c52ef316f030bf50ac2f716c", - "is_secret": false, - "is_verified": false, - "line_number": 1866, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/services/test_sso_service.py": [ - { - "hashed_secret": "920a25ef686c4f7ca6ad23dd109d3ad653161832", - "is_secret": false, - "is_verified": false, - "line_number": 176, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "3340ad734a33028b9498d58dc8b49e9c02157b19", - "is_secret": false, - "is_verified": false, - "line_number": 197, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "ec09a041656818107eb855453ffbf7327d3bbc9d", - "is_secret": false, - "is_verified": false, - "line_number": 334, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/services/test_support_bundle_service.py": [ - { - "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", - "is_secret": false, - "is_verified": false, - "line_number": 82, - "type": "Basic Auth Credentials", - "verified_result": null - }, - { - "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", - "is_secret": false, - "is_verified": false, - "line_number": 100, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "d6b66ddd9ea7dbe760114bfe9a97352a5e139134", - "is_secret": false, - "is_verified": false, - "line_number": 106, - "type": "JSON Web Token", - "verified_result": null - }, - { - "hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8", - "is_secret": false, - "is_verified": false, - "line_number": 196, - "type": "Basic Auth Credentials", - "verified_result": null - }, - { - "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", - "is_secret": false, - "is_verified": false, - "line_number": 210, - "type": "Basic Auth Credentials", - "verified_result": null - } - ], - "tests/unit/mcpgateway/services/test_token_storage_service.py": [ - { - "hashed_secret": "f7cfcf33b07a640de88cca1a41a681d98213a43c", - "is_secret": false, - "is_verified": false, - "line_number": 25, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "920a25ef686c4f7ca6ad23dd109d3ad653161832", - "is_secret": false, - "is_verified": false, - "line_number": 255, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "48004e013423b89217e65eca07df9574fcd092a6", - "is_secret": false, - "is_verified": false, - "line_number": 322, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/services/test_tool_service.py": [ - { - "hashed_secret": "d63b39580934e062f89aae63426d2f2c77c3e258", - "is_secret": false, - "is_verified": false, - "line_number": 503, - "type": "Base64 High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "586a55a9b8b97f0cd88e24ce8279ebc955949688", - "is_secret": false, - "is_verified": false, - "line_number": 504, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "00cafd126182e8a9e7c01bb2f0dfd00496be724f", - "is_secret": false, - "is_verified": false, - "line_number": 520, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "7b1552c7c7ffb8bd70b5666e5997c8e017630aab", - "is_secret": false, - "is_verified": false, - "line_number": 1935, - "type": "Base64 High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9fb7fe1217aed442b04c0f5e43b5d5a7d3287097", - "is_secret": false, - "is_verified": false, - "line_number": 2703, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "72cb70dbbafe97e5ea13ad88acd65d08389439b0", - "is_secret": false, - "is_verified": false, - "line_number": 3331, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "ee977806d7286510da8b9a7492ba58e2484c0ecc", - "is_secret": false, - "is_verified": false, - "line_number": 5610, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "f2e7745f43b0ef0e2c2faf61d6c6a28be2965750", - "is_secret": false, - "is_verified": false, - "line_number": 6102, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "4a249743d4d2241bd2ae085b4fe654d089488295", - "is_secret": false, - "is_verified": false, - "line_number": 7347, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "0c8d051d3c7eada5d31b53d9936fce6bcc232ae2", - "is_secret": false, - "is_verified": false, - "line_number": 7485, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", - "is_secret": false, - "is_verified": false, - "line_number": 7830, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/services/test_tool_service_coverage.py": [ - { - "hashed_secret": "2dd2850bcc4ae4e74154aed99ee5f68292ecfec5", - "is_secret": false, - "is_verified": false, - "line_number": 2234, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "d92342976d720ff38cf5dcb329be41959ab1ba6c", - "is_secret": false, - "is_verified": false, - "line_number": 2844, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "f93640458964af294a485bc6d597694cf3308e1f", - "is_secret": false, - "is_verified": false, - "line_number": 2853, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "0857abc2d90c5d9821229fc0a880f1c38ffb0e04", - "is_secret": false, - "is_verified": false, - "line_number": 3244, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "99834bc4eff3f1e1c1e4692d2476b593b501d045", - "is_secret": false, - "is_verified": false, - "line_number": 5529, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", - "is_secret": false, - "is_verified": false, - "line_number": 5547, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "b8cec023ad982a1355abb9e7c7700e42d7e6fac3", - "is_secret": false, - "is_verified": false, - "line_number": 5571, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "0614eb27c6e0d2f4ed9a1cce2a5bcbbbc17aa556", - "is_secret": false, - "is_verified": false, - "line_number": 5635, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "a38fea79a043f0c9a62f851659d459dc3b716ea9", - "is_secret": false, - "is_verified": false, - "line_number": 5646, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "a50da1fb101595ac5158f2c9394a65a84061b56c", - "is_secret": false, - "is_verified": false, - "line_number": 5687, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "ed3e0017cb8e4b06a59af1a441f62cbe58d2ef59", - "is_secret": false, - "is_verified": false, - "line_number": 5693, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/test_admin.py": [ - { - "hashed_secret": "206c80413b9a96c1312cc346b7d2517b84463edd", - "is_secret": false, - "is_verified": false, - "line_number": 1600, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "5cbd0bf2db07a8f50fa9bbcc5ac720b1911c6380", - "is_secret": false, - "is_verified": false, - "line_number": 1772, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "a10b98d7340036e9c8c301704f623eddd733cc1a", - "is_secret": false, - "is_verified": false, - "line_number": 2736, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "3acfb2c2b433c0ea7ff107e33df91b18e52f960f", - "is_secret": false, - "is_verified": false, - "line_number": 5148, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "fe1bae27cb7c1fb823f496f286e78f1d2ae87734", - "is_secret": false, - "is_verified": false, - "line_number": 5795, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "2878cbdbbcfa6feafc04b8889f5ecc8c470ba32e", - "is_secret": false, - "is_verified": false, - "line_number": 5859, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "a0281cd072cea8e80e7866b05dc124815760b6c9", - "is_secret": false, - "is_verified": false, - "line_number": 6111, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "a0f4ea7d91495df92bbac2e2149dfb850fe81396", - "is_secret": false, - "is_verified": false, - "line_number": 9086, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "a75a7c7b31474f3f04f3a395228ded8d61ee1ae3", - "is_secret": false, - "is_verified": false, - "line_number": 9135, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "02c593fd9af8254b859d426a76b6cd42847fbec1", - "is_secret": false, - "is_verified": false, - "line_number": 9174, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "1ded3053d0363079a4e681a3b700435d6d880290", - "is_secret": false, - "is_verified": false, - "line_number": 9231, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "c00dbbc9dadfbe1e232e93a729dd4752fade0abf", - "is_secret": false, - "is_verified": false, - "line_number": 14001, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", - "is_secret": false, - "is_verified": false, - "line_number": 16758, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "a4b48a81cdab1e1a5dd37907d6c85ca1c61ddc7c", - "is_secret": false, - "is_verified": false, - "line_number": 16777, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "dc8002865f92070749b264e76045b04fa3b8de71", - "is_secret": false, - "is_verified": false, - "line_number": 20332, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/test_admin_catalog_htmx.py": [ - { - "hashed_secret": "dff6d4ff5dc357cf451d1855ab9cbda562645c9f", - "is_secret": false, - "is_verified": false, - "line_number": 244, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/test_admin_module.py": [ - { - "hashed_secret": "1a91d62f7ca67399625a4368a6ab5d4a3baa6073", - "is_secret": false, - "is_verified": false, - "line_number": 453, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "c00dbbc9dadfbe1e232e93a729dd4752fade0abf", - "is_secret": false, - "is_verified": false, - "line_number": 1106, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/test_auth.py": [ - { - "hashed_secret": "b5920aa41ccde1341077fdb2636afc16d6da0e1a", - "is_secret": false, - "is_verified": false, - "line_number": 180, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "467be688b21f5d43370f5dc983fe5ed58dea4b00", - "is_secret": false, - "is_verified": false, - "line_number": 206, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "d955debea8e960a293ecbb257048680898cc9d94", - "is_secret": false, - "is_verified": false, - "line_number": 221, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "4ee9e7b8482f6e0282b0117eb81651c98b537589", - "is_secret": false, - "is_verified": false, - "line_number": 255, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "500886a66d19ebfe2d5c0ee86cf4612f9458ee4f", - "is_secret": false, - "is_verified": false, - "line_number": 328, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "f2169f1a808382eb7b521206debaef01c91f1b60", - "is_secret": false, - "is_verified": false, - "line_number": 367, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "cd61d9f689e9992aec40aeec7210b45a681309f9", - "is_secret": false, - "is_verified": false, - "line_number": 401, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "ae97d9e9201a9bc1806069d80e5809ab745a91e8", - "is_secret": false, - "is_verified": false, - "line_number": 436, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "eb2344b0e2d6529ced74f8b09be362a460defa15", - "is_secret": false, - "is_verified": false, - "line_number": 473, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "89be7e8e88bc7b0cdb5ddaa01dab1cd1728541e2", - "is_secret": false, - "is_verified": false, - "line_number": 537, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "5a539b200edec3aaa5167e1cde5d8fd0e8cf4aee", - "is_secret": false, - "is_verified": false, - "line_number": 550, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "8be6db326ca95f74590bd09c9c328fc9a3db3b6c", - "is_secret": false, - "is_verified": false, - "line_number": 688, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "2368055dc74fca43f2f9c849dbbaaa04616670cf", - "is_secret": false, - "is_verified": false, - "line_number": 737, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "8ce647089e0614d3b924f4a0b8fb78268562ec7c", - "is_secret": false, - "is_verified": false, - "line_number": 1430, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "dc6f6cc49f767ae38db110a9e6c1adf5c651ecbf", - "is_secret": false, - "is_verified": false, - "line_number": 1474, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "7939ea1cdfc6d73115b0d736fa04012be2b25b18", - "is_secret": false, - "is_verified": false, - "line_number": 1565, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "91014bcbf183cf77807b4bd24f2e8652f67b56a0", - "is_secret": false, - "is_verified": false, - "line_number": 2073, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "61ac75f7e6677b79c219ce819d954e05f61b5d2c", - "is_secret": false, - "is_verified": false, - "line_number": 2091, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "8223fafd35f399e55c3cf611c3d6ebbf43d68d71", - "is_secret": false, - "is_verified": false, - "line_number": 2737, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "79bead8e6d65862a00cffaa12ccde1189ec34d29", - "is_secret": false, - "is_verified": false, - "line_number": 2953, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "dfd99b5f25f839608a3c275c0f8ceb363f8f0bc0", - "is_secret": false, - "is_verified": false, - "line_number": 3514, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "5038e18712161fca54e52805726d3c70b296eff6", - "is_secret": false, - "is_verified": false, - "line_number": 3623, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "f5cd573c18bf811f82a886ceb6866b05d5ef02cd", - "is_secret": false, - "is_verified": false, - "line_number": 3701, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/test_cli_export_import_coverage.py": [ - { - "hashed_secret": "5ef3af6cd9b5aa907fa618fac829baaec6763b42", - "is_secret": false, - "is_verified": false, - "line_number": 685, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/test_config.py": [ - { - "hashed_secret": "7952d9e777d5fa3933a6132f35493b970e1c8828", - "is_secret": false, - "is_verified": false, - "line_number": 163, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "78466ed9a08daa9faf88434c1dd6bb8761e98a61", - "is_secret": false, - "is_verified": false, - "line_number": 460, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "51beb0ccb2d6f1365ed1278c636dabcd8797db95", - "is_secret": false, - "is_verified": false, - "line_number": 473, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "95bb8a28fc0d18320a4d8deae3bd9c043709a22f", - "is_secret": false, - "is_verified": false, - "line_number": 479, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "cbfdac6008f9cab4083784cbd1874f76618d2a97", - "is_secret": false, - "is_verified": false, - "line_number": 548, - "type": "Basic Auth Credentials", - "verified_result": null - }, - { - "hashed_secret": "0f0ab1d14970dea160a53133a1b2487ba464fda3", - "is_secret": false, - "is_verified": false, - "line_number": 608, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "516b9783fca517eecbd1d064da2d165310b19759", - "is_secret": false, - "is_verified": false, - "line_number": 973, - "type": "Basic Auth Credentials", - "verified_result": null - }, - { - "hashed_secret": "ef4eb24299c517306652ffee61e05934f2224914", - "is_secret": false, - "is_verified": false, - "line_number": 1192, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/test_db.py": [ - { - "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", - "is_secret": false, - "is_verified": false, - "line_number": 2421, - "type": "Basic Auth Credentials", - "verified_result": null - } - ], - "tests/unit/mcpgateway/test_db_isready.py": [ - { - "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", - "is_secret": false, - "is_verified": false, - "line_number": 72, - "type": "Basic Auth Credentials", - "verified_result": null - }, - { - "hashed_secret": "1a91d62f7ca67399625a4368a6ab5d4a3baa6073", - "is_secret": false, - "is_verified": false, - "line_number": 115, - "type": "Basic Auth Credentials", - "verified_result": null - }, - { - "hashed_secret": "516b9783fca517eecbd1d064da2d165310b19759", - "is_secret": false, - "is_verified": false, - "line_number": 224, - "type": "Basic Auth Credentials", - "verified_result": null - } - ], - "tests/unit/mcpgateway/test_display_name_uuid_features.py": [ - { - "hashed_secret": "c377074d6473f35a91001981355da793dc808ffd", - "is_secret": false, - "is_verified": false, - "line_number": 620, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "99c962e8c62296bdc9a17f5caf91ce9bb4c7e0e6", - "is_secret": false, - "is_verified": false, - "line_number": 621, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "tests/unit/mcpgateway/test_final_coverage_push.py": [ - { - "hashed_secret": "5ef3af6cd9b5aa907fa618fac829baaec6763b42", - "is_secret": false, - "is_verified": false, - "line_number": 222, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/test_llm_schemas.py": [ - { - "hashed_secret": "3acfb2c2b433c0ea7ff107e33df91b18e52f960f", - "is_secret": false, - "is_verified": false, - "line_number": 67, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/test_main.py": [ - { - "hashed_secret": "cd024c09e5784e941e833bd8fabf1dcfc3fb6cd8", - "is_secret": false, - "is_verified": false, - "line_number": 53, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "718cbcc5a4207c0d5f38e3a309bdba17cb0074b7", - "is_secret": false, - "is_verified": false, - "line_number": 137, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a10b98d7340036e9c8c301704f623eddd733cc1a", - "is_secret": false, - "is_verified": false, - "line_number": 150, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "tests/unit/mcpgateway/test_main_error_handlers.py": [ - { - "hashed_secret": "cd024c09e5784e941e833bd8fabf1dcfc3fb6cd8", - "is_secret": false, - "is_verified": false, - "line_number": 30, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/test_main_extended.py": [ - { - "hashed_secret": "6745fa570d36be08400efa1cbc2f057bb001290e", - "is_secret": false, - "is_verified": false, - "line_number": 2278, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4f13f134744a2fadbbe2d624687246347d12fa63", - "is_secret": false, - "is_verified": false, - "line_number": 2565, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "tests/unit/mcpgateway/test_multi_auth_headers.py": [ - { - "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", - "is_secret": false, - "is_verified": false, - "line_number": 298, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/test_oauth_manager.py": [ - { - "hashed_secret": "9fb7fe1217aed442b04c0f5e43b5d5a7d3287097", - "is_secret": false, - "is_verified": false, - "line_number": 71, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "00942f4668670f34c5943cf52c7ef3139fe2b8d6", - "is_secret": false, - "is_verified": false, - "line_number": 1225, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "233243ef95e736679cb1d5664a4c71ba89c10664", - "is_secret": false, - "is_verified": false, - "line_number": 1361, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "72cb70dbbafe97e5ea13ad88acd65d08389439b0", - "is_secret": false, - "is_verified": false, - "line_number": 1829, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/test_observability.py": [ - { - "hashed_secret": "8f1b63868b5d31deb06a0ff740b192aa490e8950", - "is_secret": false, - "is_verified": false, - "line_number": 326, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "1fb844731bf1e5928ae606915b54e21264da4768", - "is_secret": false, - "is_verified": false, - "line_number": 755, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/test_postgresql_schema_config.py": [ - { - "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", - "is_secret": false, - "is_verified": false, - "line_number": 167, - "type": "Basic Auth Credentials", - "verified_result": null - } - ], - "tests/unit/mcpgateway/test_schemas.py": [ - { - "hashed_secret": "22fcf027f5caee316ca4f2963c475f62e87e9b76", - "is_secret": false, - "is_verified": false, - "line_number": 829, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "6af3c121ed4a752936c297cddfb7b00394eabf10", - "is_secret": false, - "is_verified": false, - "line_number": 1081, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/test_schemas_auth_validation.py": [ - { - "hashed_secret": "40461afdef055768f498c9f11ca7d42beaf667b6", - "is_secret": false, - "is_verified": false, - "line_number": 333, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "364d84ed2a75ef4c9a3cba031109db88e504511b", - "is_secret": false, - "is_verified": false, - "line_number": 352, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "42d7bc4801cb72f2559bd9f6c9af9a2707ca321e", - "is_secret": false, - "is_verified": false, - "line_number": 371, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "379b59bcc5d7088a11af63318381a83709402009", - "is_secret": false, - "is_verified": false, - "line_number": 382, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/test_schemas_validators_extra.py": [ - { - "hashed_secret": "277fd76456880437641f76de1bfa6d7ef61ae861", - "is_secret": false, - "is_verified": false, - "line_number": 775, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "4ea8d2335b430796cf3f500368c5b0f5b1dc90f5", - "is_secret": false, - "is_verified": false, - "line_number": 956, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "57b2ad99044d337197c0c39fd3823568ff81e48a", - "is_secret": false, - "is_verified": false, - "line_number": 961, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "3d12349760de61e8070eea4704d2c471731bdd15", - "is_secret": false, - "is_verified": false, - "line_number": 987, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "640d87e741e6aa4c669a82a4cd304787960513ab", - "is_secret": false, - "is_verified": false, - "line_number": 1005, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", - "is_secret": false, - "is_verified": false, - "line_number": 1218, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "8db15e8ba2f571ba5d630b4edf3a52d265668f0a", - "is_secret": false, - "is_verified": false, - "line_number": 1334, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/test_team_governance_flags.py": [ - { - "hashed_secret": "8ac21c6ecda35ffb18d58264aeb43ca800b3d758", - "is_secret": false, - "is_verified": false, - "line_number": 580, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/test_toolops_utils.py": [ - { - "hashed_secret": "a62f2225bf70bfaccbc7f1ef2a397836717377de", - "is_secret": false, - "is_verified": false, - "line_number": 135, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/test_translate_stdio_endpoint.py": [ - { - "hashed_secret": "528ceffb33609fca3a5646ec64a032d4362d301b", - "is_secret": false, - "is_verified": false, - "line_number": 277, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/test_version.py": [ - { - "hashed_secret": "516b9783fca517eecbd1d064da2d165310b19759", - "is_secret": false, - "is_verified": false, - "line_number": 183, - "type": "Basic Auth Credentials", - "verified_result": null - } - ], - "tests/unit/mcpgateway/tools/builder/test_common.py": [ - { - "hashed_secret": "206c80413b9a96c1312cc346b7d2517b84463edd", - "is_secret": false, - "is_verified": false, - "line_number": 668, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", - "is_secret": false, - "is_verified": false, - "line_number": 946, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/tools/builder/test_schema.py": [ - { - "hashed_secret": "fd858ef1e811e28a823d60908b38df56e9e359e7", - "is_secret": false, - "is_verified": false, - "line_number": 201, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/transports/test_streamablehttp_transport.py": [ - { - "hashed_secret": "b4c9248600a42f8c38c01b632f392dbcb4c7b19a", - "is_secret": false, - "is_verified": false, - "line_number": 11072, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "90bd1b48e958257948487b90bee080ba5ed00caa", - "is_secret": false, - "is_verified": false, - "line_number": 12209, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "tests/unit/mcpgateway/utils/test_create_jwt_token.py": [ - { - "hashed_secret": "cd024c09e5784e941e833bd8fabf1dcfc3fb6cd8", - "is_secret": false, - "is_verified": false, - "line_number": 42, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/utils/test_db_isready.py": [ - { - "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", - "is_secret": false, - "is_verified": false, - "line_number": 75, - "type": "Basic Auth Credentials", - "verified_result": null - } - ], - "tests/unit/mcpgateway/utils/test_generate_keys.py": [ - { - "hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9", - "is_secret": false, - "is_verified": false, - "line_number": 32, - "type": "Private Key", - "verified_result": null - } - ], - "tests/unit/mcpgateway/utils/test_proxy_auth.py": [ - { - "hashed_secret": "b2d898cda25886738c0cd002eb080745b09e2332", - "is_secret": false, - "is_verified": false, - "line_number": 23, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/utils/test_sso_bootstrap.py": [ - { - "hashed_secret": "fe1bae27cb7c1fb823f496f286e78f1d2ae87734", - "is_secret": false, - "is_verified": false, - "line_number": 370, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "945db841c03e42eef2f3d0a4ff310e2f3b3e59ec", - "is_secret": false, - "is_verified": false, - "line_number": 454, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/utils/test_trace_redaction.py": [ - { - "hashed_secret": "36c3eaa0e1e290f41e2810bae8d9502c785e92d9", - "is_secret": false, - "is_verified": false, - "line_number": 50, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/utils/test_url_auth.py": [ - { - "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", - "is_secret": false, - "is_verified": false, - "line_number": 40, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "e48c47f1431afd3bb030deea266b4309e2228c5b", - "is_secret": false, - "is_verified": false, - "line_number": 47, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "6367c48dd193d56ea7b0baad25b19455e529f5ee", - "is_secret": false, - "is_verified": false, - "line_number": 57, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "fd0ef1fb9f5dbc367c4ec061500002a22d109bab", - "is_secret": false, - "is_verified": false, - "line_number": 65, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "de9f9d75dc3dd5f8f16d484e569b75fd2e69c86f", - "is_secret": false, - "is_verified": false, - "line_number": 80, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", - "is_secret": false, - "is_verified": false, - "line_number": 153, - "type": "Basic Auth Credentials", - "verified_result": null - }, - { - "hashed_secret": "b02dff0ec9d24823e77c27c281a852247896b86d", - "is_secret": false, - "is_verified": false, - "line_number": 155, - "type": "Basic Auth Credentials", - "verified_result": null - }, - { - "hashed_secret": "9bebe8d61ea4965d1205e9e0f8f0c6bf5262880f", - "is_secret": false, - "is_verified": false, - "line_number": 215, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/utils/test_verify_credentials.py": [ - { - "hashed_secret": "cd024c09e5784e941e833bd8fabf1dcfc3fb6cd8", - "is_secret": false, - "is_verified": false, - "line_number": 53, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "29534257453ead25854695b1b7c95e3857a7238d", - "is_secret": false, - "is_verified": false, - "line_number": 114, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "a4b48a81cdab1e1a5dd37907d6c85ca1c61ddc7c", - "is_secret": false, - "is_verified": false, - "line_number": 1042, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "ee977806d7286510da8b9a7492ba58e2484c0ecc", - "is_secret": false, - "is_verified": false, - "line_number": 1191, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/mcpgateway/validation/test_validators_advanced.py": [ - { - "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", - "is_secret": false, - "is_verified": false, - "line_number": 1142, - "type": "Basic Auth Credentials", - "verified_result": null - } - ], - "tests/unit/plugins/test_encoded_exfil_detector.py": [ - { - "hashed_secret": "55d2534ed6ad4f269b428160428fa2f6f541ba7b", - "is_secret": false, - "is_verified": false, - "line_number": 156, - "type": "Base64 High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "cf743b3a58a4d0f91c1d7f5825c0b1b5f7758174", - "is_secret": false, - "is_verified": false, - "line_number": 538, - "type": "Base64 High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8e42b03e460b2cf358ffbcf4da3bc5d14a22c86e", - "is_secret": false, - "is_verified": false, - "line_number": 582, - "type": "Base64 High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2093dd9cf307518cfe1d2fa5a3985d6fec4e995e", - "is_secret": false, - "is_verified": false, - "line_number": 595, - "type": "Base64 High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "caa924f200b35ceb6f0e33878faff75203bdccb4", - "is_secret": false, - "is_verified": false, - "line_number": 971, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "f16da2820437f3c703ff5b95c813f310ce8e67a4", - "is_secret": false, - "is_verified": false, - "line_number": 1288, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/plugins/test_jwt_claims_extraction.py": [ - { - "hashed_secret": "dccfe30496fb85f5bb560ea18b582b9aa50372bb", - "is_secret": false, - "is_verified": false, - "line_number": 153, - "type": "Secret Keyword", - "verified_result": null - } - ], - "tests/unit/plugins/test_secrets_detection.py": [ - { - "hashed_secret": "86de8c52637ec530fe39b0a8471da9b8764d5242", - "is_secret": false, - "is_verified": false, - "line_number": 466, - "type": "AWS Access Key", - "verified_result": null - } - ], - "tests/unit/test_conc_01_helpers.py": [ - { - "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", - "is_secret": false, - "is_verified": false, - "line_number": 160, - "type": "Basic Auth Credentials", - "verified_result": null - } - ], - "tools_rust/mcp_runtime/Cargo.lock": [ - { - "hashed_secret": "e0f930ce4dc6ee91bd9c13f93bdd411a875847ad", - "is_secret": false, - "is_verified": false, - "line_number": 9, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "0c2b06b30faf95e40c75aaaefa957d835c1df392", - "is_secret": false, - "is_verified": false, - "line_number": 18, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "7500005ef7a05c3d09d4a3eff4cabcf9310a2143", - "is_secret": false, - "is_verified": false, - "line_number": 27, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "c010dcc93cdc02fef1dae55077be682aa0cbe6c7", - "is_secret": false, - "is_verified": false, - "line_number": 42, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "c23d3a48029faf26d09be1d049b492940cc27ab6", - "is_secret": false, - "is_verified": false, - "line_number": 48, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e4d87006218e91f030a57eb9b12decf51068e90b", - "is_secret": false, - "is_verified": false, - "line_number": 57, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f505b7d95ee94121d5d84074bb61cbfc8115242c", - "is_secret": false, - "is_verified": false, - "line_number": 66, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "053152e6d6ec523a570bc615219d6c829a0d455b", - "is_secret": false, - "is_verified": false, - "line_number": 77, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d9ea801e3f4d913d0b6721b819f2b90c35f2303b", - "is_secret": false, - "is_verified": false, - "line_number": 83, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9998c73d8f92b1d0dbaa73adeeca54e5826e15d3", - "is_secret": false, - "is_verified": false, - "line_number": 92, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "41ffd21d7ee9a2f88b61d16a02da64f29ade1334", - "is_secret": false, - "is_verified": false, - "line_number": 103, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "94da59c445b7bfedb03dfc1ee4ade601cee2c067", - "is_secret": false, - "is_verified": false, - "line_number": 114, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "07781af8811774ce3bf3152bbfa1c98d7ea03c61", - "is_secret": false, - "is_verified": false, - "line_number": 125, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ece49906f7a72ed3cfb012e5e2e1209e1b226172", - "is_secret": false, - "is_verified": false, - "line_number": 131, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "35a662e9823d0c45efa8b4361187ac3da20ce188", - "is_secret": false, - "is_verified": false, - "line_number": 137, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "1f909b8b2e2119dde700022dea1dc63d9204e4c7", - "is_secret": false, - "is_verified": false, - "line_number": 147, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "388b28008bbc41d55a4ab30e40375d58ef630445", - "is_secret": false, - "is_verified": false, - "line_number": 159, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "181759bd3798be44125fc8c50e62ba054d318783", - "is_secret": false, - "is_verified": false, - "line_number": 192, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2c5b1ea7adeec9e14e1b10939d924f90259dd446", - "is_secret": false, - "is_verified": false, - "line_number": 211, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b25a21f1a0e2fc65dc90424e07d7210587e8b296", - "is_secret": false, - "is_verified": false, - "line_number": 220, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "83d08d935192c6e805661d2152a4aca3914adb66", - "is_secret": false, - "is_verified": false, - "line_number": 226, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "457b24d26a9cd360a12b56f87c6f1daf3d30a895", - "is_secret": false, - "is_verified": false, - "line_number": 232, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "31584e466c43e3e97cd45c38e554e2273016a1aa", - "is_secret": false, - "is_verified": false, - "line_number": 238, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b4808fb54c15b4bfe5268dd5ef3669d35302dac8", - "is_secret": false, - "is_verified": false, - "line_number": 247, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ad1a0238d69b8c5443fa6d9abd6cdca087349517", - "is_secret": false, - "is_verified": false, - "line_number": 253, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "20af2a76b19cc7a1a93269f1e5f876f58bd266cc", - "is_secret": false, - "is_verified": false, - "line_number": 259, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "435b5a83c55f72c24e5bd183795441de1a767dc2", - "is_secret": false, - "is_verified": false, - "line_number": 265, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "6c58643683bf44bb3dca7f816b2b510dba8fc971", - "is_secret": false, - "is_verified": false, - "line_number": 277, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "eac7344fb138e2bc70ec74dd15a52fe0c253887a", - "is_secret": false, - "is_verified": false, - "line_number": 283, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "14f1ae00b0ced0fb5727df3261bec5f5ae0c4a0b", - "is_secret": false, - "is_verified": false, - "line_number": 289, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a01b71d602bc5018e96db1a555a02018f474f151", - "is_secret": false, - "is_verified": false, - "line_number": 295, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d4dbf662832f75c34b38426ed080a4b801d00f3d", - "is_secret": false, - "is_verified": false, - "line_number": 309, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ebccb6117556497e4da9b06f699ff6a27d45dd3f", - "is_secret": false, - "is_verified": false, - "line_number": 319, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ad2cb0c6197488868df8c8ecd71fadf0d560ad24", - "is_secret": false, - "is_verified": false, - "line_number": 331, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f665662f68035bb677672e8a3b8b814f20f3d020", - "is_secret": false, - "is_verified": false, - "line_number": 343, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4c6f0f8d388d48ef9745eaf1c886c003e7b23552", - "is_secret": false, - "is_verified": false, - "line_number": 349, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "86769319581bdbb9bba76471716201426d49f67f", - "is_secret": false, - "is_verified": false, - "line_number": 358, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d1fde732b623b2d93e8974cbefdda84ca0c32202", - "is_secret": false, - "is_verified": false, - "line_number": 364, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8fdfd35c89021a1276b5dc829ca310cdb4b313b6", - "is_secret": false, - "is_verified": false, - "line_number": 378, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8f5fced06242dc5e430978c695ce15b86635597a", - "is_secret": false, - "is_verified": false, - "line_number": 420, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "aea69a0db4b6fb9c1801ecd9266c98e51a5b6515", - "is_secret": false, - "is_verified": false, - "line_number": 430, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e863b03571e186ffa7385bf8fdef7786a21a0a1a", - "is_secret": false, - "is_verified": false, - "line_number": 436, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "088b03c54e39344f385c72c0d648cb5b98a63a02", - "is_secret": false, - "is_verified": false, - "line_number": 445, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b5fe04da6b823ea30cae0a2af23428426e84a9f1", - "is_secret": false, - "is_verified": false, - "line_number": 455, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b7608524652c9f0e66a1af73a1ccfcfbf8b77046", - "is_secret": false, - "is_verified": false, - "line_number": 467, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f47586da7970b3b77062621cbb484cc339c31a48", - "is_secret": false, - "is_verified": false, - "line_number": 481, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e0398979fe645c6fcfae36187d4f5886dba50003", - "is_secret": false, - "is_verified": false, - "line_number": 490, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "c67665036c3d161d17d25a240b9c1001b7d35e9b", - "is_secret": false, - "is_verified": false, - "line_number": 502, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "cc99ccaf83a5c3347fd6560d1fb362b09a92ae83", - "is_secret": false, - "is_verified": false, - "line_number": 513, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8b80e3dbf214e8b5f42deaf56c671624fb8916a9", - "is_secret": false, - "is_verified": false, - "line_number": 524, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "3e11544368930af980ae0880dd16cd34ac83ccb9", - "is_secret": false, - "is_verified": false, - "line_number": 535, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4af1b31ab033275a37cc32187a52e4c46cf762db", - "is_secret": false, - "is_verified": false, - "line_number": 541, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e3423649a06049806d8883193ffa13f6488898ba", - "is_secret": false, - "is_verified": false, - "line_number": 547, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "39e1dc7e8bd1f918717fde66f30bc3884780a612", - "is_secret": false, - "is_verified": false, - "line_number": 553, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b46b7f0229055d455ca31eb1973773cd67c36aa6", - "is_secret": false, - "is_verified": false, - "line_number": 563, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2e2d13eb72c2d2c46e0c94dd2259343b0933dda9", - "is_secret": false, - "is_verified": false, - "line_number": 569, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4d2c6fb73af42ea62f965535a82e83df5411d419", - "is_secret": false, - "is_verified": false, - "line_number": 575, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "96e4515c24f218f3da6404ad65b91c62bf844034", - "is_secret": false, - "is_verified": false, - "line_number": 581, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b35db91a9a2d96942d3a2d1afe84f4789942df4a", - "is_secret": false, - "is_verified": false, - "line_number": 587, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "fc40fb75e9edfd1c082708dbcc1384d9e68f90f7", - "is_secret": false, - "is_verified": false, - "line_number": 593, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "5dd5a3b90a4aec184d6390d782a283adf53b87f9", - "is_secret": false, - "is_verified": false, - "line_number": 599, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "22901138394b0b06b9cd917cdaf66d2c36c1ed2b", - "is_secret": false, - "is_verified": false, - "line_number": 608, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8d05fb42532fecd995f8eff2190f412088d6178f", - "is_secret": false, - "is_verified": false, - "line_number": 614, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "89cf74f6e03a27de9d661324442e0b372ad9eefb", - "is_secret": false, - "is_verified": false, - "line_number": 629, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e72195ef7791709c24e8be4349842896a88bab07", - "is_secret": false, - "is_verified": false, - "line_number": 639, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f2c63a981fbb2e2b39e4c05d8ec787e3befa3b07", - "is_secret": false, - "is_verified": false, - "line_number": 645, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f5cdeb5a0b086c5a674eb8ad81bf17fd63bd0a01", - "is_secret": false, - "is_verified": false, - "line_number": 656, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2ebad4ca4792566abd1ba0d665249302119cb457", - "is_secret": false, - "is_verified": false, - "line_number": 662, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "7f0ff8004a236b8bba117372cb42ca731446c4b6", - "is_secret": false, - "is_verified": false, - "line_number": 673, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "c4bf225e39c6d664754ca87cb8751f8a2a093134", - "is_secret": false, - "is_verified": false, - "line_number": 679, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8ee82c36fc0af17f1fdeb940d2bca6424bf2e453", - "is_secret": false, - "is_verified": false, - "line_number": 685, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ef842ccf084ffcac3b8030a3ac4c93c4be04860b", - "is_secret": false, - "is_verified": false, - "line_number": 702, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "fb738e4a202642582d5a12f41a884e26001c4fa6", - "is_secret": false, - "is_verified": false, - "line_number": 712, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "fb98da5b813e3ae31d0e430526334ae9f010a546", - "is_secret": false, - "is_verified": false, - "line_number": 725, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a7bb22eb6421d498f26d952fe0635a1d04a27bcd", - "is_secret": false, - "is_verified": false, - "line_number": 739, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a9789cf3b045f7fa5c8950ba8004ffa526b2d213", - "is_secret": false, - "is_verified": false, - "line_number": 752, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "399da540bd166b0c7bb3a2815bb400eb11e5638d", - "is_secret": false, - "is_verified": false, - "line_number": 771, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f3d3c26ed24a648081ced9182bb1b19f7d78e68d", - "is_secret": false, - "is_verified": false, - "line_number": 780, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "1d591d1374352e720051c9d3f7b1e2c5557d5fd6", - "is_secret": false, - "is_verified": false, - "line_number": 786, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a6ca3c2192cd478068a202fd4a1af6b0a0f9a8ae", - "is_secret": false, - "is_verified": false, - "line_number": 792, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "0cb1c0a931d8249bdaea63aebb40d6a73c269d7a", - "is_secret": false, - "is_verified": false, - "line_number": 798, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "0fa72c3d33bbc7feb5618105d7b5997001614612", - "is_secret": false, - "is_verified": false, - "line_number": 807, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "c9fc785a64064e9f54662a39349af4f8ca9fc333", - "is_secret": false, - "is_verified": false, - "line_number": 817, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ce3dad8a04c408de2c43ad7c3ed460e21897bc5d", - "is_secret": false, - "is_verified": false, - "line_number": 827, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "92f04de23131564ceddfbbd126b98292b0934577", - "is_secret": false, - "is_verified": false, - "line_number": 840, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "3976cf14dbb91cafcaaddad7d5b88cf44aba7b58", - "is_secret": false, - "is_verified": false, - "line_number": 846, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "1f969023518451bb7f23d59ff7c96c0a6a259877", - "is_secret": false, - "is_verified": false, - "line_number": 852, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "1a23359c2904a7e86a81ce7d2576ae9ab62f8bac", - "is_secret": false, - "is_verified": false, - "line_number": 875, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "e0448e6cde59d45c1095e75101f39d91093f905d", - "is_secret": false, - "is_verified": false, - "line_number": 892, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b46c956f080e29b6f05c5538b6f134e2417d839c", - "is_secret": false, - "is_verified": false, - "line_number": 905, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ace4c15ef5d5b8c769f9d24cf4bf1de85fecb2c7", - "is_secret": false, - "is_verified": false, - "line_number": 928, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9bfd048ce5ceb106ba035489cea34b77d79814e4", - "is_secret": false, - "is_verified": false, - "line_number": 943, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "86b403165d79a78a2c4a595fe14902c5f4f47361", - "is_secret": false, - "is_verified": false, - "line_number": 952, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "6c94d59722bce3906d347ed600e37e119a7837df", - "is_secret": false, - "is_verified": false, - "line_number": 965, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "23db86ff034d7e9f799f0226a1f1a7baae4c7de1", - "is_secret": false, - "is_verified": false, - "line_number": 978, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "49b46f344ecff2a2a1820536df0334ca68050940", - "is_secret": false, - "is_verified": false, - "line_number": 992, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "50d2139960f135299c21e3cdaa6b6f076b1e2080", - "is_secret": false, - "is_verified": false, - "line_number": 998, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "08f998bf3af34432739a9ab2cddd2bb85654a4b2", - "is_secret": false, - "is_verified": false, - "line_number": 1012, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "be61a53441932e4f82987dcaf2ef89fbcfaac0a5", - "is_secret": false, - "is_verified": false, - "line_number": 1018, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "7ed5883c89e869acd5ac1fb9e12730f4a42b29c4", - "is_secret": false, - "is_verified": false, - "line_number": 1033, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "3062e8c5ec47afaf7ba2ce1532e8e56c0af6ea8f", - "is_secret": false, - "is_verified": false, - "line_number": 1039, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "29aae00ffb8818a8acfa28442eb304f3862cbaf5", - "is_secret": false, - "is_verified": false, - "line_number": 1050, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4faeea7fc4d0ed985b094fe85a90aab7fc3d7ae8", - "is_secret": false, - "is_verified": false, - "line_number": 1060, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "7f471e64dc9e4d9e288dd5f95604e359310dd19d", - "is_secret": false, - "is_verified": false, - "line_number": 1072, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "7a3f11072e339bc675d446b98fb026bdd111d679", - "is_secret": false, - "is_verified": false, - "line_number": 1078, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8ff217a235ef34ef9bbdc8de3ba9d64919c5f3ad", - "is_secret": false, - "is_verified": false, - "line_number": 1088, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4c35f1105fef7dd05206e5c580220cae7fb0d31b", - "is_secret": false, - "is_verified": false, - "line_number": 1094, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2bbb23c710243f6c2dd0da44f0bce1512d40aeb0", - "is_secret": false, - "is_verified": false, - "line_number": 1103, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "011aba63050eb571fd20fca0d6fe1a1d4f47743d", - "is_secret": false, - "is_verified": false, - "line_number": 1109, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "0b06cc0c3892ff5000c0beef37995dc5ebb9b299", - "is_secret": false, - "is_verified": false, - "line_number": 1125, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "0058fc7def6004d3930de2cbd0f21f0e20b64a66", - "is_secret": false, - "is_verified": false, - "line_number": 1131, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "93408d219072bd897710a546d04acbdf8f518992", - "is_secret": false, - "is_verified": false, - "line_number": 1141, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "190d967672500a817f10cf8c5f8ae7f1f73c006a", - "is_secret": false, - "is_verified": false, - "line_number": 1151, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8977d964704890333a861cc91fb4738b73ca2c65", - "is_secret": false, - "is_verified": false, - "line_number": 1157, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "29cc4d113d2b819fd175fde15139a9047b4714ae", - "is_secret": false, - "is_verified": false, - "line_number": 1163, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "be0f2c00852f3522d21717c22ad0d6f2a57ae2ea", - "is_secret": false, - "is_verified": false, - "line_number": 1169, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4d3dfbf42f79749cb066b45b4baa929fb891809a", - "is_secret": false, - "is_verified": false, - "line_number": 1178, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "36b97c530b1f4c96e9033800c80908a6cfa4b761", - "is_secret": false, - "is_verified": false, - "line_number": 1184, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2d70a27f9bdfdb0ebea3fa79f7962fa957bd11fa", - "is_secret": false, - "is_verified": false, - "line_number": 1193, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9885581fee305ecda8675b98f5251a280f12b29f", - "is_secret": false, - "is_verified": false, - "line_number": 1199, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b7db53b4da996a7c3f03e516ecfb7b6663ad3a67", - "is_secret": false, - "is_verified": false, - "line_number": 1205, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "551358ac7f3ecd5113292ebdd3a797bc72f72f08", - "is_secret": false, - "is_verified": false, - "line_number": 1214, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d4c64ebe48af5d78af3a4596fd3525389fa636d3", - "is_secret": false, - "is_verified": false, - "line_number": 1220, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "52174d432f72b603ab3aeb65e046d8842f64eba8", - "is_secret": false, - "is_verified": false, - "line_number": 1230, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "6b855f1f95b14579ef94fd09bab1c34327526c1e", - "is_secret": false, - "is_verified": false, - "line_number": 1236, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "22c495789b158bf96a04f9bd11015ad54eace5c6", - "is_secret": false, - "is_verified": false, - "line_number": 1242, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "dc2e94f24b05f113bf345bb2afbcb6e26645cd5c", - "is_secret": false, - "is_verified": false, - "line_number": 1253, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9f07d77e5781c78d5ccbce0d869c20816ce3211d", - "is_secret": false, - "is_verified": false, - "line_number": 1262, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "12061f2ebc56b7c0d1167ed4ead1f4570bf89831", - "is_secret": false, - "is_verified": false, - "line_number": 1272, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ebbea2a8b6a37ded68465f81f315d4a23c27a8e8", - "is_secret": false, - "is_verified": false, - "line_number": 1281, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a9f8cce66980be35350e3df493456f2ebbdcfc7a", - "is_secret": false, - "is_verified": false, - "line_number": 1290, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8f47b7c135b28863b24233db5462009611f6a702", - "is_secret": false, - "is_verified": false, - "line_number": 1300, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9d103fdb08fedf328f8148c79a7cbbdfe117e347", - "is_secret": false, - "is_verified": false, - "line_number": 1309, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9f45c3d9955af35039612741be99a94f2e719c26", - "is_secret": false, - "is_verified": false, - "line_number": 1318, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a71d7ae63f77e488a44fc92784434919791f0c3f", - "is_secret": false, - "is_verified": false, - "line_number": 1324, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8d059a6ae441b561e2f1c076d396fae696e5a25a", - "is_secret": false, - "is_verified": false, - "line_number": 1330, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "82e6d09bf3bfbfeedaf4b4cb6d840e1dc8763633", - "is_secret": false, - "is_verified": false, - "line_number": 1336, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "03b483db2dad8fd5b33755b3a4740fb7f71eb179", - "is_secret": false, - "is_verified": false, - "line_number": 1350, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "7277246a7e041aa338cb3475c3ea0e03f642c236", - "is_secret": false, - "is_verified": false, - "line_number": 1363, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "780a6c6bba165e0471b5870dd3e60ca7f2a44140", - "is_secret": false, - "is_verified": false, - "line_number": 1381, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "c16ff72fdb055b166f57925fa57687f62c6ce382", - "is_secret": false, - "is_verified": false, - "line_number": 1394, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "57a3c2bd3253c61856df4bf13198109b0d51486d", - "is_secret": false, - "is_verified": false, - "line_number": 1411, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "802fb7ee4a23f1061a4a099e6518b448f5dea8d3", - "is_secret": false, - "is_verified": false, - "line_number": 1421, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a02ff67f8e504e59af8eff72afb26fda27037a42", - "is_secret": false, - "is_verified": false, - "line_number": 1434, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "7d554479a29539a4aac0c4838c6295fabc6c798e", - "is_secret": false, - "is_verified": false, - "line_number": 1440, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "cb3b81942ccfd8c332b82d08e97a26b0741000fd", - "is_secret": false, - "is_verified": false, - "line_number": 1450, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "7e58c37359a406f21c5572f3416e6b949a94bcab", - "is_secret": false, - "is_verified": false, - "line_number": 1459, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2cf97402fb992e3aa29dc39f781569ab4fb6e99f", - "is_secret": false, - "is_verified": false, - "line_number": 1468, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d978e33d84309bf97aa5af7e47486be164c0787e", - "is_secret": false, - "is_verified": false, - "line_number": 1479, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f2ceefa876785b2d7376d2af71e760c71ec0109e", - "is_secret": false, - "is_verified": false, - "line_number": 1485, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "0ab103852c992726f0ae9d130bbd614fdf4cade2", - "is_secret": false, - "is_verified": false, - "line_number": 1491, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "abd005444ac0ead70215fd86e8d6ffe3d0575bc6", - "is_secret": false, - "is_verified": false, - "line_number": 1509, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "20b3b413826d61a82664d755aaaa4a180123d161", - "is_secret": false, - "is_verified": false, - "line_number": 1522, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "978bcf06dc12b74b1579b446ae082265db1358f6", - "is_secret": false, - "is_verified": false, - "line_number": 1531, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8cc69a0306a6b8f4c32428f44505f11eaebb3ebf", - "is_secret": false, - "is_verified": false, - "line_number": 1540, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "6538f9a3e2204f918d5719f17a23367f4d2105c7", - "is_secret": false, - "is_verified": false, - "line_number": 1550, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "82dc42114dbb3c1f634fb7aaa90672fd19267cf9", - "is_secret": false, - "is_verified": false, - "line_number": 1559, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a9a4e2cb3a3c1bd59207279fbbdb815160327649", - "is_secret": false, - "is_verified": false, - "line_number": 1569, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "efdee842a63b1e53e7a161a961eb531eb13ee9cc", - "is_secret": false, - "is_verified": false, - "line_number": 1582, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "afeeb9520ef374e7dbc57d7000330276951b8c78", - "is_secret": false, - "is_verified": false, - "line_number": 1602, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "25b42393be6d558454e2ddf44da644f606c047c7", - "is_secret": false, - "is_verified": false, - "line_number": 1624, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b4259ea178dd7cb0249df5d8feb7d13a2d6fd820", - "is_secret": false, - "is_verified": false, - "line_number": 1638, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "fb82e6c6c8d3ed2486ef8249d7e59336b5c86610", - "is_secret": false, - "is_verified": false, - "line_number": 1647, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ba44f293d24fbbcbe1ef98385355f7fed4f8419d", - "is_secret": false, - "is_verified": false, - "line_number": 1653, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "47aeac2fa4acd0b3b040c9927a26128bf0c029af", - "is_secret": false, - "is_verified": false, - "line_number": 1659, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "bfdcf12c00b303dde9a57ba3e08927a60a89c6b8", - "is_secret": false, - "is_verified": false, - "line_number": 1670, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ef2e592f89cb7fff6bccacece98f099ca10add8f", - "is_secret": false, - "is_verified": false, - "line_number": 1680, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8be0eab839d1133fc403dcc2790b84d4dfff3381", - "is_secret": false, - "is_verified": false, - "line_number": 1690, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "def835c5f3d29ef5143e04e29a61cd85a81fe09e", - "is_secret": false, - "is_verified": false, - "line_number": 1700, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "891ddfb5f57f136198961b98f8014e3c7bc9e602", - "is_secret": false, - "is_verified": false, - "line_number": 1709, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "55ab473e846700e8b2403215489a5167189fdbf5", - "is_secret": false, - "is_verified": false, - "line_number": 1718, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "19c50b4f711d39a10ac1890b58e609934e3e7ff6", - "is_secret": false, - "is_verified": false, - "line_number": 1743, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "0675b70f52618de044d1d1703b6c8849b407c83a", - "is_secret": false, - "is_verified": false, - "line_number": 1752, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "c63addae2503bfe26923f0460158bb2fc674b928", - "is_secret": false, - "is_verified": false, - "line_number": 1764, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9f7a7461ae8d5e27458e899b4e84416483680a51", - "is_secret": false, - "is_verified": false, - "line_number": 1775, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "1adac20705d9a7950134843592c9f1fbdcd12145", - "is_secret": false, - "is_verified": false, - "line_number": 1781, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "5b35b10ce4ffd9e769dc5edeb91e3717ca35a110", - "is_secret": false, - "is_verified": false, - "line_number": 1823, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "de655789d424d5a5631a8000892feda124ff3528", - "is_secret": false, - "is_verified": false, - "line_number": 1863, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "bbf2c697167fb0954886a726005d59a7548b75e6", - "is_secret": false, - "is_verified": false, - "line_number": 1877, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d2aacc8c22ee3e26d7a6f5d24bf528c87202c5f0", - "is_secret": false, - "is_verified": false, - "line_number": 1899, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "63571b418c02815c6322eb603852d5e166f6810d", - "is_secret": false, - "is_verified": false, - "line_number": 1905, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "817db4e01123d4466b767559f74fa1205a2d5951", - "is_secret": false, - "is_verified": false, - "line_number": 1921, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "b64689f9c223de81042e74dbcfbe5a73a2c1544c", - "is_secret": false, - "is_verified": false, - "line_number": 1933, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "06565a2fc496ecc7ef4ba5d189c02e0322c9db16", - "is_secret": false, - "is_verified": false, - "line_number": 1943, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "5629c74be563f7ad9c58ce3ca203402133d0caba", - "is_secret": false, - "is_verified": false, - "line_number": 1964, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "eaeb3a3ad0de04e58cbfbbef0576b14c1381ce15", - "is_secret": false, - "is_verified": false, - "line_number": 1970, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9daac1b266d3875ecff4a4b7932e73d63f51718f", - "is_secret": false, - "is_verified": false, - "line_number": 1982, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "60a673ff967b996194d288e551490d960a18d4e2", - "is_secret": false, - "is_verified": false, - "line_number": 1988, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d4b5e511e6b010068fe8fc17a580b24af40fca83", - "is_secret": false, - "is_verified": false, - "line_number": 1994, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a3107fdf18905c00e548235e403128903eb2b8eb", - "is_secret": false, - "is_verified": false, - "line_number": 2003, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "190341c4e045ee5d256b052fa97f59b1d066169f", - "is_secret": false, - "is_verified": false, - "line_number": 2012, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "3632f3823bb444d1697daae3d3c00f8c1472fbac", - "is_secret": false, - "is_verified": false, - "line_number": 2018, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a3f550383fa80c876ecec52c42b21b375aec47a3", - "is_secret": false, - "is_verified": false, - "line_number": 2031, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ba8ac4ac3810916dd75cdf9bb539b045d8f828f4", - "is_secret": false, - "is_verified": false, - "line_number": 2041, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "dd30e69b06fc9e7153b91d6152b47abee06bc76e", - "is_secret": false, - "is_verified": false, - "line_number": 2047, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "edea2c54c8d390fdfff6535d3da4fd1fdf17b62a", - "is_secret": false, - "is_verified": false, - "line_number": 2057, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "bfb212002ccc72caf73acbfb0a5bd6c1c2acaa42", - "is_secret": false, - "is_verified": false, - "line_number": 2066, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "93658223830f17449a3b81a759279f8db378716a", - "is_secret": false, - "is_verified": false, - "line_number": 2077, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "cd5a59f8105c99afb30595e52a11d73e855f2c9f", - "is_secret": false, - "is_verified": false, - "line_number": 2090, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f4fd806c07aaf73c1529b88212a826fadcae43ae", - "is_secret": false, - "is_verified": false, - "line_number": 2101, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "5d1a2c403a9003e050ea65ac8fbda2245a562535", - "is_secret": false, - "is_verified": false, - "line_number": 2113, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "56e65ae2b96ac3a748f6ce0ec54a459188c6ba87", - "is_secret": false, - "is_verified": false, - "line_number": 2119, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "dafd46fad0421f3f00574f5c736ec0847f53d212", - "is_secret": false, - "is_verified": false, - "line_number": 2130, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "8f48b5146d88920187ca1c8d1d52cd0b008f12de", - "is_secret": false, - "is_verified": false, - "line_number": 2139, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "083e28bdae3cd5b3af135e50a5758bd732e076c4", - "is_secret": false, - "is_verified": false, - "line_number": 2145, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9098c70eda3617323c613ed583e0a586a40bd2aa", - "is_secret": false, - "is_verified": false, - "line_number": 2155, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "1eeb415c50a8d2b04a705ebf09786adf00d8cf59", - "is_secret": false, - "is_verified": false, - "line_number": 2161, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "14b703e941e97ca4115fbfc7afc9d19920aa9c6c", - "is_secret": false, - "is_verified": false, - "line_number": 2167, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f711a3e8a220ceb3c9b360745772e4ed8659f46c", - "is_secret": false, - "is_verified": false, - "line_number": 2173, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "325fc7b88a913c4e763b98114888a0b0d16c1fe9", - "is_secret": false, - "is_verified": false, - "line_number": 2183, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "ca0b633ac167cec25b04208a0e3caeec68ad6c15", - "is_secret": false, - "is_verified": false, - "line_number": 2193, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "7d64118ef4ee08c6e431da22c00cc6db06fc8495", - "is_secret": false, - "is_verified": false, - "line_number": 2206, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9fd54963e68135b5cf144f396e260ba0a8cb774f", - "is_secret": false, - "is_verified": false, - "line_number": 2212, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "fc643b8317813d4055f095a72f90dfc83ac98a52", - "is_secret": false, - "is_verified": false, - "line_number": 2223, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "09b70bdc3f035c1579508c2a4a7a626d980c5e0c", - "is_secret": false, - "is_verified": false, - "line_number": 2229, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "fed671d6a24626d6cf16f91b98f2b11ba736d095", - "is_secret": false, - "is_verified": false, - "line_number": 2235, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "33eb20252a41cba23b834a062ead51beccf29376", - "is_secret": false, - "is_verified": false, - "line_number": 2246, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "24c6c50199abdbdf01f5bfc41c06d323bb57ac6b", - "is_secret": false, - "is_verified": false, - "line_number": 2255, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "d15a51b5bec88115e57a5d8933c67b62ad61d677", - "is_secret": false, - "is_verified": false, - "line_number": 2266, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "f57b63e67ed485c5ffe3dec0888657ac4c281ca5", - "is_secret": false, - "is_verified": false, - "line_number": 2275, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "29a7a0315c4faea3e608988273ab75e55096e847", - "is_secret": false, - "is_verified": false, - "line_number": 2284, - "type": "Hex High Entropy String", + "line_number": 35, + "type": "Basic Auth Credentials", "verified_result": null - }, + } + ], + "tests/migration/utils/container_manager.py": [ { - "hashed_secret": "824096ad190248d40cef63862048a64d18e73093", + "hashed_secret": "1b255e8e77fb3fafbf1fe2013347f762fbca9cd6", "is_secret": false, "is_verified": false, - "line_number": 2295, - "type": "Hex High Entropy String", + "line_number": 384, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/performance/MANUAL_TESTING.md": [ { - "hashed_secret": "67eb96441e8f50509300c66e81b167f6e3f0f21d", + "hashed_secret": "fa9beb99e4029ad5a6615399e7bbae21356086b3", "is_secret": false, "is_verified": false, - "line_number": 2306, - "type": "Hex High Entropy String", + "line_number": 434, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/performance/PERFORMANCE_STRATEGY.md": [ { - "hashed_secret": "86efcec63d7f5e5c3cf34e1acfd29fc257f3c382", + "hashed_secret": "b1d12a49a9da5edec445c6ea003be1456337bd3e", "is_secret": false, "is_verified": false, - "line_number": 2315, - "type": "Hex High Entropy String", + "line_number": 1292, + "type": "Basic Auth Credentials", "verified_result": null }, { - "hashed_secret": "2822217bb14c0223fb86ef16fccaeebb3dd90119", + "hashed_secret": "b1d12a49a9da5edec445c6ea003be1456337bd3e", "is_secret": false, "is_verified": false, - "line_number": 2325, - "type": "Hex High Entropy String", + "line_number": 1305, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "1e5be28ee09da48b8c2233118719dfc81d717f98", + "hashed_secret": "d033e22ae348aeb5660fc2140aec35850c4da997", "is_secret": false, "is_verified": false, - "line_number": 2334, - "type": "Hex High Entropy String", + "line_number": 1334, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/performance/scenarios/gateway-core-benchmark.sh": [ { - "hashed_secret": "6644e6789f4de6f894ac5fcb2bd45dcb0b112297", + "hashed_secret": "fa9beb99e4029ad5a6615399e7bbae21356086b3", "is_secret": false, "is_verified": false, - "line_number": 2340, - "type": "Hex High Entropy String", + "line_number": 196, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/performance/test_postgresql_percentile_performance.py": [ { - "hashed_secret": "157f014e6a4d0e6d04480f8e560442eb04e612bb", + "hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3", "is_secret": false, "is_verified": false, - "line_number": 2350, - "type": "Hex High Entropy String", + "line_number": 59, + "type": "Basic Auth Credentials", "verified_result": null - }, + } + ], + "tests/performance/utils/generate_docker_compose.py": [ { - "hashed_secret": "8e11a5e472d1d4d98e2bdc00b90448cca073dd97", + "hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3", "is_secret": false, "is_verified": false, - "line_number": 2361, - "type": "Hex High Entropy String", + "line_number": 71, + "type": "Basic Auth Credentials", "verified_result": null - }, + } + ], + "tests/playwright/README.md": [ { - "hashed_secret": "a5ea7e9c1ce564f30e9551f8200f29d90aa6ec5e", + "hashed_secret": "0df7be1bea60575a7469f5e07e13124ce428d263", "is_secret": false, "is_verified": false, - "line_number": 2378, - "type": "Hex High Entropy String", + "line_number": 138, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "9672a8d4827f41d52ada718f6a6d0fdf1d3129de", + "hashed_secret": "fa9beb99e4029ad5a6615399e7bbae21356086b3", "is_secret": false, "is_verified": false, - "line_number": 2389, - "type": "Hex High Entropy String", + "line_number": 142, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "713365ba9d550dc1dc1a16d662edfd7564ac300f", + "hashed_secret": "64438ee426438161da88554b3e2de796b0ca265e", "is_secret": false, "is_verified": false, - "line_number": 2415, - "type": "Hex High Entropy String", + "line_number": 146, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/playwright/conftest.py": [ { - "hashed_secret": "8aecd4c13a7b746e2dd4cfe9762061501833943a", + "hashed_secret": "d6d6b8c66ab3ade1bfcf206e7df58e296827ef17", "is_secret": false, "is_verified": false, - "line_number": 2430, - "type": "Hex High Entropy String", + "line_number": 520, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "e5720b6d98058e32a6194a43416caa780707a3de", + "hashed_secret": "1c58bd92003bbaa0538e249fff6ee19a270dec5f", "is_secret": false, "is_verified": false, - "line_number": 2440, - "type": "Hex High Entropy String", + "line_number": 585, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "5a9e3d91c47e5f9099917f07fd7130195338b4ca", + "hashed_secret": "03f5e2d670af3e9183f3fe790785b0d41291a17d", "is_secret": false, "is_verified": false, - "line_number": 2451, - "type": "Hex High Entropy String", + "line_number": 629, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/playwright/operations/test_llm_config.py": [ { - "hashed_secret": "8d578a26052af26da0fe7e5f670b70867603ac82", + "hashed_secret": "2a3575e3c0abe9772b3f98c92e7d530d7f131514", "is_secret": false, "is_verified": false, - "line_number": 2464, - "type": "Hex High Entropy String", + "line_number": 132, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/playwright/pytest.ini": [ { - "hashed_secret": "8c673dc84a6e435ba180a71ec7514d1ea121a2d0", + "hashed_secret": "176939638191d5a13935ccb90c0c8a70a5e30773", "is_secret": false, "is_verified": false, - "line_number": 2490, - "type": "Hex High Entropy String", + "line_number": 8, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/playwright/security/conftest.py": [ { - "hashed_secret": "79a0a8be10240450c72322f433eb92948903500f", + "hashed_secret": "07b7b454e840ca79e1582edcf973b051fc56e079", "is_secret": false, "is_verified": false, - "line_number": 2501, - "type": "Hex High Entropy String", + "line_number": 31, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/playwright/security/owasp/conftest.py": [ { - "hashed_secret": "102dfadad7f84fbd0da59ec7a4360651e8018702", + "hashed_secret": "07b7b454e840ca79e1582edcf973b051fc56e079", "is_secret": false, "is_verified": false, - "line_number": 2520, - "type": "Hex High Entropy String", + "line_number": 24, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/playwright/security/owasp/test_a01_broken_access_control.py": [ { - "hashed_secret": "2bb2fe1e57f60f9972d8dc8aafecc14ea140b262", + "hashed_secret": "8e45fe2388a6c4604ee0ccdba14ca0df092bc904", "is_secret": false, "is_verified": false, - "line_number": 2538, - "type": "Hex High Entropy String", + "line_number": 238, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/playwright/security/test_playwright_security_e2e_top30.py": [ { - "hashed_secret": "d93f2cd8895569df797eb6a57011aa1489604dee", + "hashed_secret": "07b7b454e840ca79e1582edcf973b051fc56e079", "is_secret": false, "is_verified": false, - "line_number": 2544, - "type": "Hex High Entropy String", + "line_number": 35, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "3f85aa33de16e014a62133be00e2fae3fee16a15", + "hashed_secret": "42d7bc4801cb72f2559bd9f6c9af9a2707ca321e", "is_secret": false, "is_verified": false, - "line_number": 2550, - "type": "Hex High Entropy String", + "line_number": 585, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/playwright/security/test_sso_management.py": [ { - "hashed_secret": "319d3f52ed81b3c7c6a69e0ddb313dba40f94795", + "hashed_secret": "03f5e2d670af3e9183f3fe790785b0d41291a17d", "is_secret": false, "is_verified": false, - "line_number": 2562, - "type": "Hex High Entropy String", + "line_number": 36, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/playwright/security/test_team_selector_e2e.py": [ { - "hashed_secret": "99bce2688418d3adf96547726401effde2101407", + "hashed_secret": "b4e44716dbbf57be3dae2f819d96795a85d06652", "is_secret": false, "is_verified": false, - "line_number": 2573, - "type": "Hex High Entropy String", + "line_number": 290, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/playwright/teams/conftest.py": [ { - "hashed_secret": "611ee95376df586616103d908ee6f0e50de5ce6f", + "hashed_secret": "07b7b454e840ca79e1582edcf973b051fc56e079", "is_secret": false, "is_verified": false, - "line_number": 2583, - "type": "Hex High Entropy String", + "line_number": 26, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/playwright/test_admin_menu_visibility.py": [ { - "hashed_secret": "4ab7af43e6895ed452f77344ea6a08d6e653ba0c", + "hashed_secret": "119ffab9fda36e29816a09097c441eb8bcd8b684", "is_secret": false, "is_verified": false, - "line_number": 2594, - "type": "Hex High Entropy String", + "line_number": 45, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/playwright/test_agents.py": [ { - "hashed_secret": "ada69b50f3a7e11d7d7e84d61c7cd446583bc38b", + "hashed_secret": "03f5e2d670af3e9183f3fe790785b0d41291a17d", "is_secret": false, "is_verified": false, - "line_number": 2610, - "type": "Hex High Entropy String", + "line_number": 263, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/playwright/test_rbac_permissions.py": [ { - "hashed_secret": "2bba312da1074a195e616dd95b0d9970dcbf623b", + "hashed_secret": "119ffab9fda36e29816a09097c441eb8bcd8b684", "is_secret": false, "is_verified": false, - "line_number": 2628, - "type": "Hex High Entropy String", + "line_number": 46, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/populate/cleanup.py": [ { - "hashed_secret": "c6f4db06cf473f26b17a37c45c8da55362dc8477", + "hashed_secret": "c871a5b8b2e511a525b09edea0b9a4d9f46401e5", "is_secret": false, "is_verified": false, - "line_number": 2634, - "type": "Hex High Entropy String", + "line_number": 42, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/populate/populators/users.py": [ { - "hashed_secret": "b0983f4e18d469c65b1af234430ea7d7cc134612", + "hashed_secret": "c871a5b8b2e511a525b09edea0b9a4d9f46401e5", "is_secret": false, "is_verified": false, - "line_number": 2640, - "type": "Hex High Entropy String", + "line_number": 14, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/populate/verify.py": [ { - "hashed_secret": "2cec7afe8f0f8f21d28f274299142cbbe949023c", + "hashed_secret": "c871a5b8b2e511a525b09edea0b9a4d9f46401e5", "is_secret": false, "is_verified": false, - "line_number": 2646, - "type": "Hex High Entropy String", + "line_number": 120, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/security/test_input_validation.py": [ { - "hashed_secret": "d3921a77cbcdfd88b9f31e8434d163d7c20738b2", + "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", "is_secret": false, "is_verified": false, - "line_number": 2652, - "type": "Hex High Entropy String", + "line_number": 286, + "type": "Basic Auth Credentials", "verified_result": null }, { - "hashed_secret": "1bc2d5e801d4eb29617028d980db1d573c2f08d1", + "hashed_secret": "120340c3d011e9621b9bb88b5884221b25251b88", "is_secret": false, "is_verified": false, - "line_number": 2661, - "type": "Hex High Entropy String", + "line_number": 2063, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/db/test_a2a_agents_auth_value_migration.py": [ { - "hashed_secret": "370f2dab232e01715bb1463fe9a85a8a0e3b41ed", + "hashed_secret": "52294c20155b397fbf140baa0cfb6d9400c82c25", "is_secret": false, "is_verified": false, - "line_number": 2667, + "line_number": 27, "type": "Hex High Entropy String", "verified_result": null }, { - "hashed_secret": "c461b9dd39d7217a96ec3c56280634a76454701d", + "hashed_secret": "9b094435d625c9385a1bb083604f87fa7c225da0", "is_secret": false, "is_verified": false, - "line_number": 2673, + "line_number": 28, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/db/test_observability_migrations.py": [ { - "hashed_secret": "d8f1d314d800de9172c39e761f105816ae4af3e8", + "hashed_secret": "9d88d7eece452a76b2955ecfbb251f23cf7b7905", "is_secret": false, "is_verified": false, - "line_number": 2679, + "line_number": 335, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/db/test_rbac_permission_backfill_migration.py": [ { - "hashed_secret": "a0ccf26d3e5e73d1fea4cbd7e882f8a4269b51e6", + "hashed_secret": "dc7c15edd98c7c7ce4cd107fb4ccb17329510002", "is_secret": false, "is_verified": false, - "line_number": 2691, + "line_number": 27, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/db/test_token_uniqueness_migration.py": [ { - "hashed_secret": "83719d2d8284b30dee2823349c2d7f600e237d8c", + "hashed_secret": "45fc9ab5f41816c6979df8bc85ffdc160a0e696a", "is_secret": false, "is_verified": false, - "line_number": 2697, + "line_number": 25, "type": "Hex High Entropy String", "verified_result": null }, { - "hashed_secret": "04a1a3194c674cd1244ae336e0fe7054fb9bc64f", + "hashed_secret": "c4770f062e9bfa8ec054475f98315913da1d16fd", "is_secret": false, "is_verified": false, - "line_number": 2703, + "line_number": 26, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/middleware/test_auth_method_propagation.py": [ { - "hashed_secret": "788b34ba07c35e639f6e3e82f608189791e3239f", + "hashed_secret": "e7c4d1ba7ce017155823536efd57843fa15ef759", "is_secret": false, "is_verified": false, - "line_number": 2714, - "type": "Hex High Entropy String", + "line_number": 52, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/middleware/test_http_auth_headers.py": [ { - "hashed_secret": "cbd62b2fef0566cbd4adcae3c005b9fb1d23170d", + "hashed_secret": "65882b5e8dbab0e649474b2a626c1d24e1b317f5", "is_secret": false, "is_verified": false, - "line_number": 2720, - "type": "Hex High Entropy String", + "line_number": 82, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "5ab5e8c5a013b4ea054a281a403fd0b7ab4917bc", + "hashed_secret": "35e6ca489e73a8991165cdcfc2b79eb0d25ed0a3", "is_secret": false, "is_verified": false, - "line_number": 2726, - "type": "Hex High Entropy String", + "line_number": 103, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "831b1f0f0af506c6b1b5c2de459b9b4b4f3ac633", + "hashed_secret": "59df6723fb54fd8dde6dcf51034e51491bfbff24", "is_secret": false, "is_verified": false, - "line_number": 2736, - "type": "Hex High Entropy String", + "line_number": 909, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "d1837f7e31117d99b0cbffd82e7eabcea851a94c", + "hashed_secret": "9b4cd28c8ecd18d533601bbb7d3345b9d007bd80", "is_secret": false, "is_verified": false, - "line_number": 2745, - "type": "Hex High Entropy String", + "line_number": 915, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/middleware/test_http_auth_integration.py": [ { - "hashed_secret": "8773b28b7ba6a12c738b3cdd1b980ae660b3c0de", + "hashed_secret": "6f865e7e20c773e3483eaa74a07d03cec02bde2e", "is_secret": false, "is_verified": false, - "line_number": 2751, - "type": "Hex High Entropy String", + "line_number": 181, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "d146f9686c9c372d5c5f3d85c964777ce5a583f0", + "hashed_secret": "d58f8c42247f1cf100bf63f196d93d7d302e8ca9", "is_secret": false, "is_verified": false, - "line_number": 2760, - "type": "Hex High Entropy String", + "line_number": 202, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "b85b391225b20c3137322bcc23c7e2c31320a83e", + "hashed_secret": "3acfb2c2b433c0ea7ff107e33df91b18e52f960f", "is_secret": false, "is_verified": false, - "line_number": 2769, - "type": "Hex High Entropy String", + "line_number": 272, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "ee7f4bf6e85139039bdd2af2df821dde5a5138d1", + "hashed_secret": "e48c47f1431afd3bb030deea266b4309e2228c5b", "is_secret": false, "is_verified": false, - "line_number": 2778, - "type": "Hex High Entropy String", + "line_number": 284, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "604edf3bd2202bebf89e23895a6ef68088f57e42", + "hashed_secret": "4155f3a6f21409ec6066b6c974b058d3ea4a2c5c", "is_secret": false, "is_verified": false, - "line_number": 2787, - "type": "Hex High Entropy String", + "line_number": 466, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "a98f930ed9e869e7534be3e4b8ca121b2f46b3cc", + "hashed_secret": "84a36e47e8bfcca13318bca53cdcb1270e2c4b9e", "is_secret": false, "is_verified": false, - "line_number": 2800, - "type": "Hex High Entropy String", + "line_number": 487, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "7167df607c246fe7c20de1a7b7abc394c7026cd0", + "hashed_secret": "c48ea9182291ca3f527914fd7d35a698830931d0", "is_secret": false, "is_verified": false, - "line_number": 2814, - "type": "Hex High Entropy String", + "line_number": 525, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "f0f817643483baa005cf31ad44d972f65508675e", + "hashed_secret": "534c8ebac109fde87e1eb0c6e35249fc8a38278a", "is_secret": false, "is_verified": false, - "line_number": 2824, - "type": "Hex High Entropy String", + "line_number": 681, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/middleware/test_rbac.py": [ { - "hashed_secret": "64c9975a7ab1b5bcd2439c334f2370a65d4aae82", + "hashed_secret": "ab73a3eaca01a7059dcdff6f95556ec7fd83de96", "is_secret": false, "is_verified": false, - "line_number": 2837, - "type": "Hex High Entropy String", + "line_number": 1389, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "d3858567b9054a3163ab6dfd94a79713a72f7cec", + "hashed_secret": "92dd4a2de441b63e6ac51fdb81a05a416dffd182", "is_secret": false, "is_verified": false, - "line_number": 2846, - "type": "Hex High Entropy String", + "line_number": 1476, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/middleware/test_request_logging_middleware.py": [ { - "hashed_secret": "042d13670968f7c52be4bc75a67035bdc4128e45", + "hashed_secret": "1a91d62f7ca67399625a4368a6ab5d4a3baa6073", "is_secret": false, "is_verified": false, - "line_number": 2856, - "type": "Hex High Entropy String", + "line_number": 98, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "931e511686c955e2983b45d8c609826922700e63", + "hashed_secret": "dbdab9be92cacdae6a97e8601332bfaa8545800f", "is_secret": false, "is_verified": false, - "line_number": 2868, - "type": "Hex High Entropy String", + "line_number": 99, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "af4aec7dea6087b343019e88153e61a650d6e6dc", + "hashed_secret": "4ea8d2335b430796cf3f500368c5b0f5b1dc90f5", "is_secret": false, "is_verified": false, - "line_number": 2881, - "type": "Hex High Entropy String", + "line_number": 151, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/plugins/framework/external/grpc/test_grpc_models.py": [ { - "hashed_secret": "35f86be9fc56fed00caa9d04d07259deee6e5713", + "hashed_secret": "81f344a7686a80b4c5293e8fdc0b0160c82c06a8", "is_secret": false, "is_verified": false, - "line_number": 2894, - "type": "Hex High Entropy String", + "line_number": 106, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "7493d3c7ad45735197216f5195a3a4b2f0dc05f8", + "hashed_secret": "623e76c36aa2a886542011e28412cc761d7ceb01", "is_secret": false, "is_verified": false, - "line_number": 2906, - "type": "Hex High Entropy String", + "line_number": 110, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "96c10bfe923f2aba4a17b67b4874d09b7895eefe", + "hashed_secret": "48a7b8889e1542650266c14a18c8708488fa1951", "is_secret": false, "is_verified": false, - "line_number": 2916, - "type": "Hex High Entropy String", + "line_number": 132, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/plugins/framework/external/grpc/test_tls_utils.py": [ { - "hashed_secret": "d2059503bfb113f3e88f51318d9db05989b82fe8", + "hashed_secret": "623e76c36aa2a886542011e28412cc761d7ceb01", "is_secret": false, "is_verified": false, - "line_number": 2926, - "type": "Hex High Entropy String", + "line_number": 225, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "26553c8c93ce66fc3b66c38ddb1df8eca8f8dba0", + "hashed_secret": "48a7b8889e1542650266c14a18c8708488fa1951", "is_secret": false, "is_verified": false, - "line_number": 2935, - "type": "Hex High Entropy String", + "line_number": 248, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/plugins/framework/external/mcp/test_tls_utils.py": [ { - "hashed_secret": "2dc6e8f934b74951c38b2c67b869de588648a255", + "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", "is_secret": false, "is_verified": false, - "line_number": 2948, - "type": "Hex High Entropy String", + "line_number": 104, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "4388f4a1071af57b8f44733a4b590114701c8263", + "hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9", "is_secret": false, "is_verified": false, - "line_number": 2957, - "type": "Hex High Entropy String", + "line_number": 230, + "type": "Private Key", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/plugins/framework/hooks/test_http.py": [ { - "hashed_secret": "c635956e53a2624afd87d3ac8a41701e377f00e7", + "hashed_secret": "5dc0dc43e8ac987476af951a9e5bed1b19b24028", "is_secret": false, "is_verified": false, - "line_number": 2970, - "type": "Hex High Entropy String", + "line_number": 247, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "5747d8af9684661644b7df2fe00e51dc16f6843d", + "hashed_secret": "7e7f74d2579ebec3b3f2c125e756269a29782497", "is_secret": false, "is_verified": false, - "line_number": 2981, - "type": "Hex High Entropy String", + "line_number": 248, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "a5f5903bd227b9d811f037da91086231b40422a7", + "hashed_secret": "7b20dc22a6cb086e0d990ccc74ec819b16dc68b8", "is_secret": false, "is_verified": false, - "line_number": 2992, - "type": "Hex High Entropy String", + "line_number": 265, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "34cfa36d78842de5ac91d80bd7a8996d903abee8", + "hashed_secret": "ee977806d7286510da8b9a7492ba58e2484c0ecc", "is_secret": false, "is_verified": false, - "line_number": 2998, - "type": "Hex High Entropy String", + "line_number": 311, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "6be5b6b2b339ebb0359e498fb3752087d91f5ea2", + "hashed_secret": "f9ac14b63a75faf57d8db6f919bfabb2502d273c", "is_secret": false, "is_verified": false, - "line_number": 3007, - "type": "Hex High Entropy String", + "line_number": 325, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/plugins/framework/test_plugin_models_coverage.py": [ { - "hashed_secret": "b04fbbc992f0ef166c77d9d041a47769d8d54b74", + "hashed_secret": "48a7b8889e1542650266c14a18c8708488fa1951", "is_secret": false, "is_verified": false, - "line_number": 3016, - "type": "Hex High Entropy String", + "line_number": 233, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "84195544d364bd618a1ff2934f7820b59206d21a", + "hashed_secret": "81f344a7686a80b4c5293e8fdc0b0160c82c06a8", "is_secret": false, "is_verified": false, - "line_number": 3025, - "type": "Hex High Entropy String", + "line_number": 238, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/plugins/framework/test_policies.py": [ { - "hashed_secret": "de230a3c66580155ba845c7202e7fc4158d1c82b", + "hashed_secret": "37c6c57bedf4305ef41249c1794760b5cb8fad17", "is_secret": false, "is_verified": false, - "line_number": 3034, - "type": "Hex High Entropy String", + "line_number": 121, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "f9de3f6e836fa1b0a50f4e0e06154a422c0ed705", + "hashed_secret": "d73ef92426f2b11dfc4aed4d4bfc41c49ee1087c", "is_secret": false, "is_verified": false, - "line_number": 3043, - "type": "Hex High Entropy String", + "line_number": 593, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "fd835612664533826730fe318edc16280b266943", + "hashed_secret": "e812ba8d00b270ef3502bb53ceb31e8c5188f14e", "is_secret": false, "is_verified": false, - "line_number": 3052, - "type": "Hex High Entropy String", + "line_number": 760, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "822e00a0f89ac349cbf702531f4f033260828e21", + "hashed_secret": "bf31c52c32b0fcda1dc3e7c0c91803a7d1c9e891", "is_secret": false, "is_verified": false, - "line_number": 3067, - "type": "Hex High Entropy String", + "line_number": 774, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/plugins/framework/test_validators.py": [ { - "hashed_secret": "366ae613a15b15409330b2645e3be1d2e1558404", + "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", "is_secret": false, "is_verified": false, - "line_number": 3083, - "type": "Hex High Entropy String", + "line_number": 85, + "type": "Basic Auth Credentials", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/plugins/plugins/content_moderation/test_content_moderation.py": [ { - "hashed_secret": "8f82d82d4ddd51ac44bed84f6fe1e359937c694b", + "hashed_secret": "0bc02c6c973caffbda3e15adcb2f39c41634c728", "is_secret": false, "is_verified": false, - "line_number": 3100, - "type": "Hex High Entropy String", + "line_number": 40, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "0fdd9df07d0f5a4b49a81d92bbb086763150fbc0", + "hashed_secret": "75ddfb45216fe09680dfe70eda4f559a910c832c", "is_secret": false, "is_verified": false, - "line_number": 3106, - "type": "Hex High Entropy String", + "line_number": 247, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/plugins/plugins/content_moderation/test_content_moderation_integration.py": [ { - "hashed_secret": "09a5f306b0571281b96bea85abcab3c0e42cfea7", + "hashed_secret": "0bc02c6c973caffbda3e15adcb2f39c41634c728", "is_secret": false, "is_verified": false, - "line_number": 3112, - "type": "Hex High Entropy String", + "line_number": 301, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "461e363b4482af5e18ec5f4c46460e3725ad49b8", + "hashed_secret": "8cdf8b16b80c4f3f6e6af135b3c72efd706723c0", "is_secret": false, "is_verified": false, - "line_number": 3118, - "type": "Hex High Entropy String", + "line_number": 381, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/plugins/plugins/header_filter/test_header_filter_plugin.py": [ { - "hashed_secret": "faa0ddbbe89ccdee24b39e7fb6cd924d84b9f95c", + "hashed_secret": "a62f2225bf70bfaccbc7f1ef2a397836717377de", "is_secret": false, "is_verified": false, - "line_number": 3124, - "type": "Hex High Entropy String", + "line_number": 417, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "0d66c99a1872e857fa11e63d18d4c2a02d9f86a3", + "hashed_secret": "83ff9f4e0d16d61727cbdf47d769fb707b652217", "is_secret": false, "is_verified": false, - "line_number": 3130, - "type": "Hex High Entropy String", + "line_number": 462, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/plugins/plugins/pii_filter/test_pii_filter.py": [ { - "hashed_secret": "31b6bf1d17e46e58fcdf0ab5f8ceb6d4929a3d89", + "hashed_secret": "25910f981e85ca04baf359199dd0bd4a3ae738b6", "is_secret": false, "is_verified": false, - "line_number": 3136, - "type": "Hex High Entropy String", + "line_number": 505, + "type": "AWS Access Key", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/plugins/plugins/tools_telemetry_exporter/test_tools_telemetry_exporter.py": [ { - "hashed_secret": "729601340d94890d89e69f03762589a8cbf89cec", + "hashed_secret": "e8af0e18ff4805f4efd84f58b0fa69e3780f35a4", "is_secret": false, "is_verified": false, - "line_number": 3142, - "type": "Hex High Entropy String", + "line_number": 62, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "4e31e81adcdb519b5b43ab0f6580c32e90a30506", + "hashed_secret": "cc84fa5a361f86a589169fde1e4e6d62bc786e6c", "is_secret": false, "is_verified": false, - "line_number": 3148, + "line_number": 143, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/plugins/plugins/virus_total_checker/test_virus_total_checker.py": [ { - "hashed_secret": "ea581e0c5da35c1c464cd32062801ed7632d833c", + "hashed_secret": "829c3804401b0727f70f73d4415e162400cbe57b", "is_secret": false, "is_verified": false, - "line_number": 3154, - "type": "Hex High Entropy String", + "line_number": 436, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/plugins/plugins/webhook_notification/test_webhook_integration.py": [ { - "hashed_secret": "74b05331f23492f76054b20a7b762f3fc2c7b31b", + "hashed_secret": "a8ae1520ce69381a9cf8dbd5082d8512fcace493", "is_secret": false, "is_verified": false, - "line_number": 3160, - "type": "Hex High Entropy String", + "line_number": 216, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/plugins/plugins/webhook_notification/test_webhook_notification.py": [ { - "hashed_secret": "2a889052b1a2813537b70d604c6194f818ee8f99", + "hashed_secret": "fe1bae27cb7c1fb823f496f286e78f1d2ae87734", "is_secret": false, "is_verified": false, - "line_number": 3166, - "type": "Hex High Entropy String", + "line_number": 110, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "a9df4b3b5d643994b2d1015d8de874ffe74eebd6", + "hashed_secret": "2e7a7ee14caebf378fc32d6cf6f557f347c96773", "is_secret": false, "is_verified": false, - "line_number": 3172, - "type": "Hex High Entropy String", + "line_number": 210, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/routers/test_auth.py": [ { - "hashed_secret": "40b3d506c9a7fbd27dfcb5fe39bacba2c0453d04", + "hashed_secret": "6eb67d95dba1a614971e31e78146d44bd4a3ada3", "is_secret": false, "is_verified": false, - "line_number": 3178, - "type": "Hex High Entropy String", + "line_number": 191, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "579637b947f70dfcf00faa3c290bcc66113ab031", + "hashed_secret": "cbfdac6008f9cab4083784cbd1874f76618d2a97", "is_secret": false, "is_verified": false, - "line_number": 3184, - "type": "Hex High Entropy String", + "line_number": 271, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/routers/test_email_auth_router.py": [ { - "hashed_secret": "15b35fb3118b66a879fd0b785ea79a11a3b69040", + "hashed_secret": "cbfdac6008f9cab4083784cbd1874f76618d2a97", "is_secret": false, "is_verified": false, - "line_number": 3190, - "type": "Hex High Entropy String", + "line_number": 165, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "da6ff9b9fc982eeb5de2c594b6db02921ba25370", + "hashed_secret": "de14fdc5a7f3843d8db79053625a2169a4eed921", "is_secret": false, "is_verified": false, - "line_number": 3196, - "type": "Hex High Entropy String", + "line_number": 523, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "4667cb6823cb7eb0e6a8b6d18e6fc0b7cfb4bc85", + "hashed_secret": "f3e12d2b914fae6510cff60e40b097c0962654a1", "is_secret": false, "is_verified": false, - "line_number": 3202, - "type": "Hex High Entropy String", + "line_number": 658, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "fbe26dbbaa1b01b1932e5a13b04b7ffb88ef2d21", + "hashed_secret": "e6b6afbd6d76bb5d2041542d7d2e3fac5bb05593", "is_secret": false, "is_verified": false, - "line_number": 3208, - "type": "Hex High Entropy String", + "line_number": 1422, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "5a6732cdbd63c028ba5070b9ef350228816b09e8", + "hashed_secret": "23ace7331ef30c45051de4e683719db7391b9980", "is_secret": false, "is_verified": false, - "line_number": 3214, - "type": "Hex High Entropy String", + "line_number": 1491, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "26542b504e2307445106925533c81fef8adafb47", + "hashed_secret": "6aa6f10deee84f739ae8aca12a2755a2d0e103ac", "is_secret": false, "is_verified": false, - "line_number": 3220, - "type": "Hex High Entropy String", + "line_number": 1595, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "cf45dbb800154fd3ee6025a606eec2d87171f1ba", + "hashed_secret": "e577bc0464e5dd0052fc9ab532b0630e58173f92", "is_secret": false, "is_verified": false, - "line_number": 3226, - "type": "Hex High Entropy String", + "line_number": 1626, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "0d331d19e8ef297cd4158210436c303fc743e723", + "hashed_secret": "ef7d0dca079f43e7952971148dfaba10fbcce609", "is_secret": false, "is_verified": false, - "line_number": 3232, - "type": "Hex High Entropy String", + "line_number": 1643, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "0aa052d5eab28b8d8d4d377a9ff0c8fb9be671ce", + "hashed_secret": "789b49606c321c8cf228d17942608eff0ccc4171", "is_secret": false, "is_verified": false, - "line_number": 3238, - "type": "Hex High Entropy String", + "line_number": 1677, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "0253824dccde843d47fa65b99bb29912e5ec3a0a", + "hashed_secret": "dc8002865f92070749b264e76045b04fa3b8de71", "is_secret": false, "is_verified": false, - "line_number": 3244, - "type": "Hex High Entropy String", + "line_number": 2136, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/routers/test_llm_admin_router.py": [ { - "hashed_secret": "d8a8d970a479e89ac4678dabd4216dfa8b22e040", + "hashed_secret": "f7a75342741955b2b9b66b55a78e5061f321ff44", "is_secret": false, "is_verified": false, - "line_number": 3253, - "type": "Hex High Entropy String", + "line_number": 532, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "d4e3e4ca451b86ca4ea86a0b2db6ec3ff7c01c22", + "hashed_secret": "61ba747fe2f5b3d513c40550084c6284fbbc1094", "is_secret": false, "is_verified": false, - "line_number": 3264, - "type": "Hex High Entropy String", + "line_number": 538, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/routers/test_llmchat_router.py": [ { - "hashed_secret": "84469e279d411898bc4109bd5c351e99cec1a050", + "hashed_secret": "baf4e5770bde2def49611ed9c852b518038db759", "is_secret": false, "is_verified": false, - "line_number": 3280, - "type": "Hex High Entropy String", + "line_number": 126, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/routers/test_oauth_router.py": [ { - "hashed_secret": "8f9c265c3866a77dcb45fd46fa0b83dc78acf82d", + "hashed_secret": "72cb70dbbafe97e5ea13ad88acd65d08389439b0", "is_secret": false, "is_verified": false, - "line_number": 3295, - "type": "Hex High Entropy String", + "line_number": 137, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/routers/test_reverse_proxy.py": [ { - "hashed_secret": "4eee70986dc22664f0597495a6d0dd22fc5422c4", + "hashed_secret": "c56486f8b638f63e04251d0c8ab0b4fbfee8e06b", "is_secret": false, "is_verified": false, - "line_number": 3314, - "type": "Hex High Entropy String", + "line_number": 796, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/routers/test_teams.py": [ { - "hashed_secret": "54f3ab20cd8669d07e74b7b8767b7c2d2be98e84", + "hashed_secret": "1e418031f4ec8505cff3cde4f41efc0ee83ec2d1", "is_secret": false, "is_verified": false, - "line_number": 3332, - "type": "Hex High Entropy String", + "line_number": 254, + "type": "Base64 High Entropy String", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/services/test_a2a_query_param_auth.py": [ { - "hashed_secret": "4e2820b8b3adaf1e4b81474eccc6a3ae071d2760", + "hashed_secret": "99834bc4eff3f1e1c1e4692d2476b593b501d045", "is_secret": false, "is_verified": false, - "line_number": 3338, - "type": "Hex High Entropy String", + "line_number": 277, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "ed1ba03fabc77d3aa81f08b87029efbd7d9a1340", + "hashed_secret": "f4cbfd23b14432ac406aacc683f2a6485e52f356", "is_secret": false, "is_verified": false, - "line_number": 3350, - "type": "Hex High Entropy String", + "line_number": 340, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "3a49d16dc29026d169224f8bc14e6828a62128c2", + "hashed_secret": "f5e6c51ff9f3703a753cde41e4cc8f506bc809db", "is_secret": false, "is_verified": false, - "line_number": 3361, - "type": "Hex High Entropy String", + "line_number": 341, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "2384307a6cae139f038230b889458d239e9ff53b", + "hashed_secret": "6108f529090a823e5cbaefba71604ccecc021ecc", "is_secret": false, "is_verified": false, - "line_number": 3373, - "type": "Hex High Entropy String", + "line_number": 426, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "76513044803a500e17fc0ad98fb266b139572891", + "hashed_secret": "e3de258032685ca3a157e704090921039ff7f0b3", "is_secret": false, "is_verified": false, - "line_number": 3382, - "type": "Hex High Entropy String", + "line_number": 555, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/services/test_a2a_service.py": [ { - "hashed_secret": "a58d51dc68f673a049f4d82bdf01e61cf73b93b2", + "hashed_secret": "41db7c65f665e40b44178f95f5f86473ca17160e", "is_secret": false, "is_verified": false, - "line_number": 3393, - "type": "Hex High Entropy String", + "line_number": 72, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "86abb8da6326f0620dc9f0951186d370cad910b9", + "hashed_secret": "4ea8d2335b430796cf3f500368c5b0f5b1dc90f5", "is_secret": false, "is_verified": false, - "line_number": 3402, - "type": "Hex High Entropy String", + "line_number": 190, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "3b7aac6c5358eda96f36b1d1a7df21dd571dec15", + "hashed_secret": "1a91d62f7ca67399625a4368a6ab5d4a3baa6073", "is_secret": false, "is_verified": false, - "line_number": 3414, - "type": "Hex High Entropy String", + "line_number": 191, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "cf861d7c0a379aa373abaee68eeffcb37caec0a0", + "hashed_secret": "5ef3af6cd9b5aa907fa618fac829baaec6763b42", "is_secret": false, "is_verified": false, - "line_number": 3423, - "type": "Hex High Entropy String", + "line_number": 385, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "fa35aa28d81cf737ac8af26aae31b0b195eef3af", + "hashed_secret": "94c9325c8ade4f6327d87e240713c2fd7fdbfc63", "is_secret": false, "is_verified": false, - "line_number": 3434, - "type": "Hex High Entropy String", + "line_number": 386, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "6274c331e3520248957d3a2497e2edab2c3aaee0", + "hashed_secret": "7bb0c6efd2edb1ae20dab6dd260895ad8895e07e", "is_secret": false, "is_verified": false, - "line_number": 3445, - "type": "Hex High Entropy String", + "line_number": 422, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "27ab5e55f6ba7afa0e99492446d43050c7b719fa", + "hashed_secret": "ddbeee31dc29b74fcede0bea4d3fc5276d9148c8", "is_secret": false, "is_verified": false, - "line_number": 3456, - "type": "Hex High Entropy String", + "line_number": 448, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "94014c04a964970c30d309870f97ea5d8ea5bfd8", + "hashed_secret": "122758dd535bc6d246f36a686b29c7c40fab1315", "is_secret": false, "is_verified": false, - "line_number": 3467, - "type": "Hex High Entropy String", + "line_number": 799, + "type": "Secret Keyword", "verified_result": null - } - ], - "tools_rust/mcp_runtime/src/lib.rs": [ + }, { - "hashed_secret": "df1431b489758b92c84bdec3c9283b96066a44b8", + "hashed_secret": "fa727f587e9f6115da6d4eb600dd8b6fe06cf69a", "is_secret": false, "is_verified": false, - "line_number": 90, + "line_number": 801, "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", + "hashed_secret": "250cdef3ce09000fa9a939a13e5f73433d96a852", "is_secret": false, "is_verified": false, - "line_number": 10450, - "type": "Basic Auth Credentials", + "line_number": 2354, + "type": "Secret Keyword", "verified_result": null - } - ], - "tools_rust/mcp_runtime/src/observability.rs": [ + }, { - "hashed_secret": "b7dd0ec3dc49487982011219e66db3716b6669c6", + "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", "is_secret": false, "is_verified": false, - "line_number": 596, + "line_number": 2363, "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "d2b1620ca7a5314280e595624e8b13c185f8a2e1", + "hashed_secret": "1902e3d6fc4e78a0bcc50ba12b882769afbf4a8c", "is_secret": false, "is_verified": false, - "line_number": 785, + "line_number": 2395, "type": "Secret Keyword", "verified_result": null - } - ], - "tools_rust/mcp_runtime/tests/runtime.rs": [ + }, { - "hashed_secret": "5b204323030835cdda5d258742d1452e812988de", + "hashed_secret": "d92342976d720ff38cf5dcb329be41959ab1ba6c", "is_secret": false, "is_verified": false, - "line_number": 1611, + "line_number": 2816, "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3", + "hashed_secret": "a343c9b5b1abfcf385d5c6f6dc0282f4d88a416e", "is_secret": false, "is_verified": false, - "line_number": 4425, - "type": "Basic Auth Credentials", + "line_number": 2977, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "d6c1622f5e897dac7dcc4fab2cded03cb8240caa", + "hashed_secret": "c00dbbc9dadfbe1e232e93a729dd4752fade0abf", "is_secret": false, "is_verified": false, - "line_number": 5836, + "line_number": 3009, "type": "Secret Keyword", "verified_result": null } ], - "tools_rust/wrapper/Cargo.lock": [ + "tests/unit/mcpgateway/services/test_argon2_service.py": [ { - "hashed_secret": "e4858ab617f84095362dc894e4561bb077ca9239", + "hashed_secret": "f13733f6dd9f1ed3118e2da31428c71eab5ffd99", "is_secret": false, "is_verified": false, - "line_number": 9, - "type": "Hex High Entropy String", + "line_number": 79, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "e0f930ce4dc6ee91bd9c13f93bdd411a875847ad", + "hashed_secret": "b5bc013af872265e389b3abee36dd4932a206ab8", "is_secret": false, "is_verified": false, - "line_number": 20, - "type": "Hex High Entropy String", + "line_number": 152, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "0c2b06b30faf95e40c75aaaefa957d835c1df392", + "hashed_secret": "4be80aee9bf2223071eedfc002ba2546abd1f5ea", "is_secret": false, "is_verified": false, - "line_number": 29, - "type": "Hex High Entropy String", + "line_number": 230, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "950ea3a71e9f5d30d3723b3f7595d1524bd0de20", + "hashed_secret": "9fb7fe1217aed442b04c0f5e43b5d5a7d3287097", "is_secret": false, "is_verified": false, - "line_number": 38, - "type": "Hex High Entropy String", + "line_number": 388, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "a0b32b6f12e419acbd7b54d25cfcb35ea7d88b39", + "hashed_secret": "cbfdac6008f9cab4083784cbd1874f76618d2a97", "is_secret": false, "is_verified": false, - "line_number": 53, - "type": "Hex High Entropy String", + "line_number": 487, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "4d8ca7a1c773e7850208f4b7e18b740a9d526e28", + "hashed_secret": "6d512cc3d1a73d1f0a7331746483447a60b9bd98", "is_secret": false, "is_verified": false, - "line_number": 59, - "type": "Hex High Entropy String", + "line_number": 538, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/services/test_async_crypto_wrappers.py": [ { - "hashed_secret": "e4d87006218e91f030a57eb9b12decf51068e90b", + "hashed_secret": "5a0aee0f3af308cd6d74d617fde6592c2bc94fa3", "is_secret": false, "is_verified": false, - "line_number": 68, - "type": "Hex High Entropy String", + "line_number": 44, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "f505b7d95ee94121d5d84074bb61cbfc8115242c", + "hashed_secret": "b5bc013af872265e389b3abee36dd4932a206ab8", "is_secret": false, "is_verified": false, - "line_number": 77, - "type": "Hex High Entropy String", + "line_number": 74, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "053152e6d6ec523a570bc615219d6c829a0d455b", + "hashed_secret": "d4bb297c014026ace455b1ad5926033d795bf016", "is_secret": false, "is_verified": false, - "line_number": 88, - "type": "Hex High Entropy String", + "line_number": 98, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "d9ea801e3f4d913d0b6721b819f2b90c35f2303b", + "hashed_secret": "6d512cc3d1a73d1f0a7331746483447a60b9bd98", "is_secret": false, "is_verified": false, - "line_number": 94, - "type": "Hex High Entropy String", + "line_number": 112, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/services/test_catalog_service.py": [ { - "hashed_secret": "09be821f9be21476915db96c4e53315d40c0a2b4", + "hashed_secret": "816cc20437d859538736e1ef46558b7bda486c06", "is_secret": false, "is_verified": false, - "line_number": 103, - "type": "Hex High Entropy String", + "line_number": 563, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/services/test_correlation_id_json_formatter.py": [ { - "hashed_secret": "94da59c445b7bfedb03dfc1ee4ade601cee2c067", + "hashed_secret": "125fbc14773f228e72f16d55be21bad750d30b19", "is_secret": false, "is_verified": false, - "line_number": 113, + "line_number": 129, "type": "Hex High Entropy String", "verified_result": null }, { - "hashed_secret": "07781af8811774ce3bf3152bbfa1c98d7ea03c61", + "hashed_secret": "ff998abc1ce6d8f01a675fa197368e44c8916e9c", "is_secret": false, "is_verified": false, - "line_number": 124, + "line_number": 130, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/services/test_dcr_service.py": [ { - "hashed_secret": "ece49906f7a72ed3cfb012e5e2e1209e1b226172", + "hashed_secret": "352b2a4120b46b2e0221e753a1272cd34a6aa0da", "is_secret": false, "is_verified": false, - "line_number": 130, - "type": "Hex High Entropy String", + "line_number": 404, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "b25a21f1a0e2fc65dc90424e07d7210587e8b296", + "hashed_secret": "290180f809e70dd1bc99a4c65bd669f43dde426d", "is_secret": false, "is_verified": false, - "line_number": 136, - "type": "Hex High Entropy String", + "line_number": 650, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "457b24d26a9cd360a12b56f87c6f1daf3d30a895", + "hashed_secret": "945db841c03e42eef2f3d0a4ff310e2f3b3e59ec", "is_secret": false, "is_verified": false, - "line_number": 142, - "type": "Hex High Entropy String", + "line_number": 785, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/services/test_email_auth_basic.py": [ { - "hashed_secret": "614e05dffa21f28c7507d6df0f4f6e8d2993c93b", + "hashed_secret": "5a0aee0f3af308cd6d74d617fde6592c2bc94fa3", "is_secret": false, "is_verified": false, - "line_number": 148, - "type": "Hex High Entropy String", + "line_number": 283, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "b4808fb54c15b4bfe5268dd5ef3669d35302dac8", + "hashed_secret": "1816851d10187b57a93235b52495e615629f906d", "is_secret": false, "is_verified": false, - "line_number": 157, - "type": "Hex High Entropy String", + "line_number": 701, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "20af2a76b19cc7a1a93269f1e5f876f58bd266cc", + "hashed_secret": "454f9bfcc5f7523c755896f9d2c7281f83aec668", "is_secret": false, "is_verified": false, - "line_number": 163, - "type": "Hex High Entropy String", + "line_number": 727, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "a5f1914a4edf604fca63fe9c60cb62601b7c701c", + "hashed_secret": "aafdc23870ecbcd3d557b6423a8982134e17927e", "is_secret": false, "is_verified": false, - "line_number": 169, - "type": "Hex High Entropy String", + "line_number": 754, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "eac7344fb138e2bc70ec74dd15a52fe0c253887a", + "hashed_secret": "e8662cfb96bd9c7fe84c31d76819ec3a92c80e63", "is_secret": false, "is_verified": false, - "line_number": 179, - "type": "Hex High Entropy String", + "line_number": 959, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "14f1ae00b0ced0fb5727df3261bec5f5ae0c4a0b", + "hashed_secret": "b5bc013af872265e389b3abee36dd4932a206ab8", "is_secret": false, "is_verified": false, - "line_number": 185, - "type": "Hex High Entropy String", + "line_number": 991, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "a01b71d602bc5018e96db1a555a02018f474f151", + "hashed_secret": "be57d8e1a2a7a4e5c034e7790659309f5e5539c0", "is_secret": false, "is_verified": false, - "line_number": 191, - "type": "Hex High Entropy String", + "line_number": 1066, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "ba9e3cd70200d1008c1cd6e43fc0f2265452d3d6", + "hashed_secret": "b4edd1d6f455752e0b214407e561d8d3823204fe", "is_secret": false, "is_verified": false, - "line_number": 205, - "type": "Hex High Entropy String", + "line_number": 1280, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "a79fc0f9c28c100ce8b3f3a1fe5cbaee6864eed6", + "hashed_secret": "562c20a72724744e2529d163c304a3ec32d09c0a", "is_secret": false, "is_verified": false, - "line_number": 215, - "type": "Hex High Entropy String", + "line_number": 1504, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "bc197f5ee9458fce96164b6cd1c28520136bac52", + "hashed_secret": "addbd3aa5619f2932733104eb8ceef08f6fd2693", "is_secret": false, "is_verified": false, - "line_number": 227, - "type": "Hex High Entropy String", + "line_number": 1516, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "710066c385eadd0091f067e0a8e2573ac53d5f4b", + "hashed_secret": "e9e4a6d29515c8e53e4df7bc6646a23237b8f862", "is_secret": false, "is_verified": false, - "line_number": 239, - "type": "Hex High Entropy String", + "line_number": 1579, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "20ba5750c60eb4d6c0385d1c99ceddd591d40cb1", + "hashed_secret": "6aa10d711ca6f233cc7d7e9c36a980cbbd6b1ed1", "is_secret": false, "is_verified": false, - "line_number": 245, - "type": "Hex High Entropy String", + "line_number": 1601, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "400bb579b938346d207b921b0791cd725ad8559d", + "hashed_secret": "bdb6e328018a96636bca051255228c0d2a3543b5", "is_secret": false, "is_verified": false, - "line_number": 251, - "type": "Hex High Entropy String", + "line_number": 1643, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "8f5fced06242dc5e430978c695ce15b86635597a", + "hashed_secret": "36abac195cd57009a1013913a7ec5b2677faf5fd", "is_secret": false, "is_verified": false, - "line_number": 260, - "type": "Hex High Entropy String", + "line_number": 1660, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "aea69a0db4b6fb9c1801ecd9266c98e51a5b6515", + "hashed_secret": "4015d13806dacd6cf5436ce218b7f755ab9ddab3", "is_secret": false, "is_verified": false, - "line_number": 270, - "type": "Hex High Entropy String", + "line_number": 2176, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "a30d4f2c3c9fde84f3eed7588f2e5fcb795cb52b", + "hashed_secret": "dc8002865f92070749b264e76045b04fa3b8de71", "is_secret": false, "is_verified": false, - "line_number": 276, - "type": "Hex High Entropy String", + "line_number": 2261, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/services/test_email_auth_service.py": [ { - "hashed_secret": "076f7448f7767df7a0af32c25c88174167b2b870", + "hashed_secret": "862511dc4b5b31885dd85a83c2af1a4adc685f19", "is_secret": false, "is_verified": false, - "line_number": 285, - "type": "Hex High Entropy String", + "line_number": 389, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/services/test_email_auth_service_admin_role_sync.py": [ { - "hashed_secret": "9cdec82ff07c4062846684c9ba65e4dc199923d2", + "hashed_secret": "759521118cabf4f8bbc46e6d6344d0e700aa4be5", "is_secret": false, "is_verified": false, - "line_number": 291, - "type": "Hex High Entropy String", + "line_number": 50, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "9264404b12f829d6dfbfdac1825b97e54fcbb535", + "hashed_secret": "6c55803d6f1d7a177a0db3eb4b343b0d50f9c111", "is_secret": false, "is_verified": false, - "line_number": 301, - "type": "Hex High Entropy String", + "line_number": 321, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "df7d483ec7a8e3b39542662290f8d52a71439b7a", + "hashed_secret": "6fa5edecdc44e7209e0678c5dea72a95d63bf554", "is_secret": false, "is_verified": false, - "line_number": 314, - "type": "Hex High Entropy String", + "line_number": 365, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/services/test_email_notification_service.py": [ { - "hashed_secret": "11b0d546f276abc72c2c2b0afa721f73b9f02097", + "hashed_secret": "584db8632ab5718672aab8670add109a862fff0c", "is_secret": false, "is_verified": false, - "line_number": 325, - "type": "Hex High Entropy String", + "line_number": 154, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/services/test_encryption_service.py": [ { - "hashed_secret": "8b80e3dbf214e8b5f42deaf56c671624fb8916a9", + "hashed_secret": "d6b66ddd9ea7dbe760114bfe9a97352a5e139134", "is_secret": false, "is_verified": false, - "line_number": 334, - "type": "Hex High Entropy String", + "line_number": 376, + "type": "JSON Web Token", "verified_result": null }, { - "hashed_secret": "fe74f8a10fe0b9110fe9dde4a8cf00fa101034c2", + "hashed_secret": "c6c580101c1bdaaad4e360d0679d7a8cac514e4f", "is_secret": false, "is_verified": false, - "line_number": 345, - "type": "Hex High Entropy String", + "line_number": 378, + "type": "JSON Web Token", "verified_result": null }, { - "hashed_secret": "1652117d4ce1279d3c4dc2c3dd53cfea2d615645", + "hashed_secret": "755f997eb6296e47fd6d145e1b541b9e98a4fab1", "is_secret": false, "is_verified": false, - "line_number": 351, - "type": "Hex High Entropy String", + "line_number": 416, + "type": "Base64 High Entropy String", "verified_result": null }, { - "hashed_secret": "e3423649a06049806d8883193ffa13f6488898ba", + "hashed_secret": "2538fa47a21f3a58be26e62c63b6dd0a6e87dae5", "is_secret": false, "is_verified": false, - "line_number": 357, - "type": "Hex High Entropy String", + "line_number": 417, + "type": "Base64 High Entropy String", "verified_result": null }, { - "hashed_secret": "39e1dc7e8bd1f918717fde66f30bc3884780a612", + "hashed_secret": "25910f981e85ca04baf359199dd0bd4a3ae738b6", "is_secret": false, "is_verified": false, - "line_number": 363, - "type": "Hex High Entropy String", + "line_number": 418, + "type": "AWS Access Key", "verified_result": null }, { - "hashed_secret": "2e2d13eb72c2d2c46e0c94dd2259343b0933dda9", + "hashed_secret": "e8af0e18ff4805f4efd84f58b0fa69e3780f35a4", "is_secret": false, "is_verified": false, - "line_number": 373, - "type": "Hex High Entropy String", + "line_number": 718, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "4d2c6fb73af42ea62f965535a82e83df5411d419", + "hashed_secret": "1a91d62f7ca67399625a4368a6ab5d4a3baa6073", "is_secret": false, "is_verified": false, - "line_number": 382, - "type": "Hex High Entropy String", + "line_number": 719, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "03f3e1962fc611ebeaa8ff185167042498bc06d5", + "hashed_secret": "290180f809e70dd1bc99a4c65bd669f43dde426d", "is_secret": false, "is_verified": false, - "line_number": 388, - "type": "Hex High Entropy String", + "line_number": 781, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "b35db91a9a2d96942d3a2d1afe84f4789942df4a", + "hashed_secret": "eea0d9a29d51f43612c303ff2f64b57f56dad885", "is_secret": false, "is_verified": false, - "line_number": 400, - "type": "Hex High Entropy String", + "line_number": 782, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "fc40fb75e9edfd1c082708dbcc1384d9e68f90f7", + "hashed_secret": "2cee75752f97573ff8cae445096cc30e992b55c7", "is_secret": false, "is_verified": false, - "line_number": 406, - "type": "Hex High Entropy String", + "line_number": 803, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/services/test_export_service.py": [ { - "hashed_secret": "a05ef788debbaeb03dbdc3e5db4ee44747b18eb7", + "hashed_secret": "a10b98d7340036e9c8c301704f623eddd733cc1a", "is_secret": false, "is_verified": false, - "line_number": 412, + "line_number": 1162, "type": "Hex High Entropy String", "verified_result": null }, { - "hashed_secret": "1e57f3e0354398a25902ba8a10b12a84283243cc", + "hashed_secret": "6578fca8b62f49457e4cd7e66554a310a1a64be8", "is_secret": false, "is_verified": false, - "line_number": 421, - "type": "Hex High Entropy String", + "line_number": 1753, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/services/test_gateway_query_param_auth.py": [ { - "hashed_secret": "5dd5a3b90a4aec184d6390d782a283adf53b87f9", + "hashed_secret": "67ece584ee3884563e532a35b1616e5aefc2c121", "is_secret": false, "is_verified": false, - "line_number": 427, - "type": "Hex High Entropy String", + "line_number": 205, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "8d05fb42532fecd995f8eff2190f412088d6178f", + "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", "is_secret": false, "is_verified": false, - "line_number": 436, - "type": "Hex High Entropy String", + "line_number": 229, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/services/test_gateway_service.py": [ { - "hashed_secret": "89cf74f6e03a27de9d661324442e0b372ad9eefb", + "hashed_secret": "4ea8d2335b430796cf3f500368c5b0f5b1dc90f5", "is_secret": false, "is_verified": false, - "line_number": 451, - "type": "Hex High Entropy String", + "line_number": 634, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "e72195ef7791709c24e8be4349842896a88bab07", + "hashed_secret": "57b2ad99044d337197c0c39fd3823568ff81e48a", "is_secret": false, "is_verified": false, - "line_number": 461, - "type": "Hex High Entropy String", + "line_number": 635, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "f2c63a981fbb2e2b39e4c05d8ec787e3befa3b07", + "hashed_secret": "104c9bb58d7eb1aa5ab82080cd06f1134ac6040b", "is_secret": false, "is_verified": false, - "line_number": 467, - "type": "Hex High Entropy String", + "line_number": 1470, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "f5cdeb5a0b086c5a674eb8ad81bf17fd63bd0a01", + "hashed_secret": "99834bc4eff3f1e1c1e4692d2476b593b501d045", "is_secret": false, "is_verified": false, - "line_number": 478, - "type": "Hex High Entropy String", + "line_number": 4908, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "2ebad4ca4792566abd1ba0d665249302119cb457", + "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", "is_secret": false, "is_verified": false, - "line_number": 484, - "type": "Hex High Entropy String", + "line_number": 4913, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "7f0ff8004a236b8bba117372cb42ca731446c4b6", + "hashed_secret": "1a91d62f7ca67399625a4368a6ab5d4a3baa6073", "is_secret": false, "is_verified": false, - "line_number": 495, - "type": "Hex High Entropy String", + "line_number": 6450, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "c4bf225e39c6d664754ca87cb8751f8a2a093134", + "hashed_secret": "2a23dd4f2e8b50315e601a2867ea7b7bf5a43b8c", "is_secret": false, "is_verified": false, - "line_number": 501, - "type": "Hex High Entropy String", + "line_number": 7047, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "8ee82c36fc0af17f1fdeb940d2bca6424bf2e453", + "hashed_secret": "ed3e0017cb8e4b06a59af1a441f62cbe58d2ef59", "is_secret": false, "is_verified": false, - "line_number": 507, - "type": "Hex High Entropy String", + "line_number": 7066, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "fb738e4a202642582d5a12f41a884e26001c4fa6", + "hashed_secret": "b55c6dc4705dba8b151c59566175ad845d5a9104", "is_secret": false, "is_verified": false, - "line_number": 524, - "type": "Hex High Entropy String", + "line_number": 7272, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "fb98da5b813e3ae31d0e430526334ae9f010a546", + "hashed_secret": "3654bd5ef523a79741766b7c3f22228fd43c3836", "is_secret": false, "is_verified": false, - "line_number": 537, - "type": "Hex High Entropy String", + "line_number": 7291, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/services/test_gateway_service_extended.py": [ { - "hashed_secret": "a7bb22eb6421d498f26d952fe0635a1d04a27bcd", + "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", "is_secret": false, "is_verified": false, - "line_number": 549, - "type": "Hex High Entropy String", + "line_number": 183, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/services/test_gateway_service_health_oauth.py": [ { - "hashed_secret": "a9789cf3b045f7fa5c8950ba8004ffa526b2d213", + "hashed_secret": "277fd76456880437641f76de1bfa6d7ef61ae861", "is_secret": false, "is_verified": false, - "line_number": 562, - "type": "Hex High Entropy String", + "line_number": 571, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/services/test_gateway_service_oauth_comprehensive.py": [ { - "hashed_secret": "399da540bd166b0c7bb3a2815bb400eb11e5638d", + "hashed_secret": "ee131046fbfad0f5fac08926f9b928539da950cd", "is_secret": false, "is_verified": false, - "line_number": 581, - "type": "Hex High Entropy String", + "line_number": 102, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "f3d3c26ed24a648081ced9182bb1b19f7d78e68d", + "hashed_secret": "72cb70dbbafe97e5ea13ad88acd65d08389439b0", "is_secret": false, "is_verified": false, - "line_number": 590, - "type": "Hex High Entropy String", + "line_number": 613, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "1d591d1374352e720051c9d3f7b1e2c5557d5fd6", + "hashed_secret": "752cb354d4d101b52581dc820d6fa3f29e87a776", "is_secret": false, "is_verified": false, - "line_number": 596, - "type": "Hex High Entropy String", + "line_number": 635, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/services/test_import_service.py": [ { - "hashed_secret": "0fa72c3d33bbc7feb5618105d7b5997001614612", + "hashed_secret": "9b4cd28c8ecd18d533601bbb7d3345b9d007bd80", "is_secret": false, "is_verified": false, - "line_number": 602, - "type": "Hex High Entropy String", + "line_number": 308, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "c9fc785a64064e9f54662a39349af4f8ca9fc333", + "hashed_secret": "8d974e916edd2f663f202c91a1d775f32f7c8d62", "is_secret": false, "is_verified": false, - "line_number": 612, - "type": "Hex High Entropy String", + "line_number": 517, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "ce3dad8a04c408de2c43ad7c3ed460e21897bc5d", + "hashed_secret": "4b0873b633484d5de20b2d8f852a8c07b43336bc", "is_secret": false, "is_verified": false, - "line_number": 622, - "type": "Hex High Entropy String", + "line_number": 1449, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "92f04de23131564ceddfbbd126b98292b0934577", + "hashed_secret": "2d6e80602a17e8309ee0317358582a1207e2fd44", "is_secret": false, "is_verified": false, - "line_number": 635, - "type": "Hex High Entropy String", + "line_number": 1602, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "3976cf14dbb91cafcaaddad7d5b88cf44aba7b58", + "hashed_secret": "bfbb13b4405a850385a367a1cb668c0fa8b00f3c", "is_secret": false, "is_verified": false, - "line_number": 641, - "type": "Hex High Entropy String", + "line_number": 1613, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "1f969023518451bb7f23d59ff7c96c0a6a259877", + "hashed_secret": "dff6d4ff5dc357cf451d1855ab9cbda562645c9f", "is_secret": false, "is_verified": false, - "line_number": 647, - "type": "Hex High Entropy String", + "line_number": 2504, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "1a23359c2904a7e86a81ce7d2576ae9ab62f8bac", + "hashed_secret": "1073ab6cda4b991cd29f9e83a307f34004ae9327", "is_secret": false, "is_verified": false, - "line_number": 670, - "type": "Hex High Entropy String", + "line_number": 2521, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "64476db6085c2f677304cd3398408043caab5651", + "hashed_secret": "277fd76456880437641f76de1bfa6d7ef61ae861", "is_secret": false, "is_verified": false, - "line_number": 686, - "type": "Hex High Entropy String", + "line_number": 2670, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/services/test_llm_provider_service.py": [ { - "hashed_secret": "b46c956f080e29b6f05c5538b6f134e2417d839c", + "hashed_secret": "51af665d1afaf4f399863df27521b41e6ac2484e", "is_secret": false, "is_verified": false, - "line_number": 702, - "type": "Hex High Entropy String", + "line_number": 112, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "ace4c15ef5d5b8c769f9d24cf4bf1de85fecb2c7", + "hashed_secret": "133e3d6b775613651eaedbf7e609d244f2f852af", "is_secret": false, "is_verified": false, - "line_number": 725, - "type": "Hex High Entropy String", + "line_number": 269, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "9bfd048ce5ceb106ba035489cea34b77d79814e4", + "hashed_secret": "83a5b8a7b2e181736b4cad2391e48691b4434fdb", "is_secret": false, "is_verified": false, - "line_number": 740, - "type": "Hex High Entropy String", + "line_number": 540, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "86b403165d79a78a2c4a595fe14902c5f4f47361", + "hashed_secret": "54ec81ecf625d2640f2d27dbb8343cb6ee1b7348", "is_secret": false, "is_verified": false, - "line_number": 749, - "type": "Hex High Entropy String", + "line_number": 551, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "6c94d59722bce3906d347ed600e37e119a7837df", + "hashed_secret": "a5645bb67778c147a3b366da521477d254f5c4e8", "is_secret": false, "is_verified": false, - "line_number": 762, - "type": "Hex High Entropy String", + "line_number": 769, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "23db86ff034d7e9f799f0226a1f1a7baae4c7de1", + "hashed_secret": "ee977806d7286510da8b9a7492ba58e2484c0ecc", "is_secret": false, "is_verified": false, "line_number": 775, - "type": "Hex High Entropy String", + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/services/test_llm_proxy_service.py": [ { - "hashed_secret": "49b46f344ecff2a2a1820536df0334ca68050940", + "hashed_secret": "a5645bb67778c147a3b366da521477d254f5c4e8", "is_secret": false, "is_verified": false, - "line_number": 789, - "type": "Hex High Entropy String", + "line_number": 434, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "50d2139960f135299c21e3cdaa6b6f076b1e2080", + "hashed_secret": "e48c47f1431afd3bb030deea266b4309e2228c5b", "is_secret": false, "is_verified": false, - "line_number": 795, - "type": "Hex High Entropy String", + "line_number": 447, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "08f998bf3af34432739a9ab2cddd2bb85654a4b2", + "hashed_secret": "277fd76456880437641f76de1bfa6d7ef61ae861", "is_secret": false, "is_verified": false, - "line_number": 809, - "type": "Hex High Entropy String", + "line_number": 460, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/services/test_mcp_client_chat_service.py": [ { - "hashed_secret": "be61a53441932e4f82987dcaf2ef89fbcfaac0a5", + "hashed_secret": "fb0123cbab73d8a58896fbe39b8b7b5b6df15c5e", "is_secret": false, "is_verified": false, - "line_number": 815, - "type": "Hex High Entropy String", + "line_number": 69, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "7ed5883c89e869acd5ac1fb9e12730f4a42b29c4", + "hashed_secret": "a022de4a00e7998ae897308ffaaf884fabb9ee1f", "is_secret": false, "is_verified": false, - "line_number": 830, - "type": "Hex High Entropy String", + "line_number": 100, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "c6818e944c28f358430e50279f8552ba70d43b1c", + "hashed_secret": "766a6e8998aefe2117d62c2d48411e43161ffa11", "is_secret": false, "is_verified": false, - "line_number": 836, - "type": "Hex High Entropy String", + "line_number": 125, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "3062e8c5ec47afaf7ba2ce1532e8e56c0af6ea8f", + "hashed_secret": "76ce15edd5a8548603b6fd281d185f323234758f", "is_secret": false, "is_verified": false, - "line_number": 842, - "type": "Hex High Entropy String", + "line_number": 148, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "29aae00ffb8818a8acfa28442eb304f3862cbaf5", + "hashed_secret": "e2c786c60e8fb4620403ebbd4ec4fd8e8766370e", "is_secret": false, "is_verified": false, - "line_number": 853, - "type": "Hex High Entropy String", + "line_number": 167, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "4faeea7fc4d0ed985b094fe85a90aab7fc3d7ae8", + "hashed_secret": "a62f2225bf70bfaccbc7f1ef2a397836717377de", "is_secret": false, "is_verified": false, - "line_number": 863, - "type": "Hex High Entropy String", + "line_number": 187, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/services/test_mcp_client_chat_service_extended.py": [ { - "hashed_secret": "7f471e64dc9e4d9e288dd5f95604e359310dd19d", + "hashed_secret": "0474aee45985f5ae829f53849df476200e876990", "is_secret": false, "is_verified": false, - "line_number": 875, - "type": "Hex High Entropy String", + "line_number": 256, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "7a3f11072e339bc675d446b98fb026bdd111d679", + "hashed_secret": "277fd76456880437641f76de1bfa6d7ef61ae861", "is_secret": false, "is_verified": false, - "line_number": 881, - "type": "Hex High Entropy String", + "line_number": 337, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "8ff217a235ef34ef9bbdc8de3ba9d64919c5f3ad", + "hashed_secret": "a022de4a00e7998ae897308ffaaf884fabb9ee1f", "is_secret": false, "is_verified": false, - "line_number": 891, - "type": "Hex High Entropy String", + "line_number": 807, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "2bbb23c710243f6c2dd0da44f0bce1512d40aeb0", + "hashed_secret": "766a6e8998aefe2117d62c2d48411e43161ffa11", "is_secret": false, "is_verified": false, - "line_number": 897, - "type": "Hex High Entropy String", + "line_number": 1155, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "93408d219072bd897710a546d04acbdf8f518992", + "hashed_secret": "64bd05d89142566144bce27f4904a322d8d9e836", "is_secret": false, "is_verified": false, - "line_number": 903, - "type": "Hex High Entropy String", + "line_number": 1191, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "6fcce879c9ab3becbec8929825db785c6d52437b", + "hashed_secret": "a62f2225bf70bfaccbc7f1ef2a397836717377de", "is_secret": false, "is_verified": false, - "line_number": 913, - "type": "Hex High Entropy String", + "line_number": 1289, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "190d967672500a817f10cf8c5f8ae7f1f73c006a", + "hashed_secret": "d576acab7ea77edc8aa4f9ebea6bd4c1cc0d1764", "is_secret": false, "is_verified": false, - "line_number": 928, - "type": "Hex High Entropy String", + "line_number": 1554, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/services/test_mcp_session_pool.py": [ { - "hashed_secret": "8977d964704890333a861cc91fb4738b73ca2c65", + "hashed_secret": "90bd1b48e958257948487b90bee080ba5ed00caa", "is_secret": false, "is_verified": false, - "line_number": 934, + "line_number": 1478, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/services/test_metrics.py": [ { - "hashed_secret": "29cc4d113d2b819fd175fde15139a9047b4714ae", + "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", "is_secret": false, "is_verified": false, - "line_number": 940, - "type": "Hex High Entropy String", + "line_number": 164, + "type": "Basic Auth Credentials", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/services/test_oauth_manager.py": [ { - "hashed_secret": "312b2b00dee44085e5b75a37287b37fad4cbffd3", + "hashed_secret": "34e587c8f9ba011db386d719d66ffe3cfaea5447", "is_secret": false, "is_verified": false, - "line_number": 946, - "type": "Hex High Entropy String", + "line_number": 399, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "0e41bc284bfc2877d6684a4ecc1ac5095172bb24", + "hashed_secret": "a0f4ea7d91495df92bbac2e2149dfb850fe81396", "is_secret": false, "is_verified": false, - "line_number": 956, - "type": "Hex High Entropy String", + "line_number": 419, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "4d3dfbf42f79749cb066b45b4baa929fb891809a", + "hashed_secret": "920a25ef686c4f7ca6ad23dd109d3ad653161832", "is_secret": false, "is_verified": false, - "line_number": 962, - "type": "Hex High Entropy String", + "line_number": 458, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "36b97c530b1f4c96e9033800c80908a6cfa4b761", + "hashed_secret": "a62f2225bf70bfaccbc7f1ef2a397836717377de", "is_secret": false, "is_verified": false, - "line_number": 968, - "type": "Hex High Entropy String", + "line_number": 535, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "2d70a27f9bdfdb0ebea3fa79f7962fa957bd11fa", + "hashed_secret": "355e7ab792a8403301eb0732bab9d2b3950ac048", "is_secret": false, "is_verified": false, - "line_number": 977, - "type": "Hex High Entropy String", + "line_number": 538, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/services/test_oauth_manager_pkce.py": [ { - "hashed_secret": "b7db53b4da996a7c3f03e516ecfb7b6663ad3a67", + "hashed_secret": "e582429052cdb908833560f6f7582d232de37c4d", "is_secret": false, "is_verified": false, - "line_number": 983, - "type": "Hex High Entropy String", + "line_number": 58, + "type": "Base64 High Entropy String", "verified_result": null }, { - "hashed_secret": "52174d432f72b603ab3aeb65e046d8842f64eba8", + "hashed_secret": "fe1bae27cb7c1fb823f496f286e78f1d2ae87734", "is_secret": false, "is_verified": false, - "line_number": 1026, - "type": "Hex High Entropy String", + "line_number": 352, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "e5a4c25fa3dd5c2d5b9286d7ea3b484ca10cf36f", + "hashed_secret": "72cb70dbbafe97e5ea13ad88acd65d08389439b0", "is_secret": false, "is_verified": false, - "line_number": 1032, - "type": "Hex High Entropy String", + "line_number": 475, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/services/test_observability_service.py": [ { - "hashed_secret": "cb470d371783f789843ee40423cebb2556210097", + "hashed_secret": "0a24796d4c71ce722a92f450f69dc36c60b21de4", "is_secret": false, "is_verified": false, - "line_number": 1041, + "line_number": 368, "type": "Hex High Entropy String", "verified_result": null }, { - "hashed_secret": "22c495789b158bf96a04f9bd11015ad54eace5c6", + "hashed_secret": "8750a512f22c55598175aee8790ae2470ec88d16", "is_secret": false, "is_verified": false, - "line_number": 1047, + "line_number": 369, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/services/test_resource_service.py": [ { - "hashed_secret": "5b64e437c260658f1d7cdcd48a1225f857e0e5b8", + "hashed_secret": "b0beaa298b4c296ba29df08b919548d17e68d6c8", "is_secret": false, "is_verified": false, - "line_number": 1058, - "type": "Hex High Entropy String", + "line_number": 3496, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "1c467bf7b5d8839eb1e74702a5f267741ec101c2", + "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", "is_secret": false, "is_verified": false, - "line_number": 1083, - "type": "Hex High Entropy String", + "line_number": 3511, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "a01ed733db2d22229836d58b183434ba75b584c2", + "hashed_secret": "718cbcc5a4207c0d5f38e3a309bdba17cb0074b7", "is_secret": false, "is_verified": false, - "line_number": 1100, + "line_number": 4203, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/services/test_server_service.py": [ { - "hashed_secret": "7622026df2230df64fc957b1de93a7a2ffa53523", + "hashed_secret": "c377074d6473f35a91001981355da793dc808ffd", "is_secret": false, "is_verified": false, - "line_number": 1112, + "line_number": 1406, "type": "Hex High Entropy String", "verified_result": null }, { - "hashed_secret": "dc2e94f24b05f113bf345bb2afbcb6e26645cd5c", + "hashed_secret": "4ea8d2335b430796cf3f500368c5b0f5b1dc90f5", "is_secret": false, "is_verified": false, - "line_number": 1122, - "type": "Hex High Entropy String", + "line_number": 1494, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "0a549d859d0064d99bd64aa5226d0588e42e1f32", + "hashed_secret": "57b2ad99044d337197c0c39fd3823568ff81e48a", "is_secret": false, "is_verified": false, - "line_number": 1131, - "type": "Hex High Entropy String", + "line_number": 1495, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "ebbea2a8b6a37ded68465f81f315d4a23c27a8e8", + "hashed_secret": "e8af0e18ff4805f4efd84f58b0fa69e3780f35a4", "is_secret": false, "is_verified": false, - "line_number": 1137, - "type": "Hex High Entropy String", + "line_number": 1635, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "3558448508502aa5a344f39c803ec2665ee9e81b", + "hashed_secret": "25ab86bed149ca6ca9c1c0d5db7c9a91388ddeab", "is_secret": false, "is_verified": false, - "line_number": 1146, - "type": "Hex High Entropy String", + "line_number": 1636, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "a71d7ae63f77e488a44fc92784434919791f0c3f", + "hashed_secret": "498e738c70c40fb156a240d26f866f2d50e9cb51", "is_secret": false, "is_verified": false, - "line_number": 1152, - "type": "Hex High Entropy String", + "line_number": 1848, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "e591abf3379f4a9791ed9fe0134efe5eeb70d9ee", + "hashed_secret": "a6bf0a0e01efe836c52ef316f030bf50ac2f716c", "is_secret": false, "is_verified": false, - "line_number": 1158, - "type": "Hex High Entropy String", + "line_number": 1866, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/services/test_sso_service.py": [ { - "hashed_secret": "069b2d0ee6a81123a6f841920f52bab96e24cd15", + "hashed_secret": "920a25ef686c4f7ca6ad23dd109d3ad653161832", "is_secret": false, "is_verified": false, - "line_number": 1173, - "type": "Hex High Entropy String", + "line_number": 176, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "8d059a6ae441b561e2f1c076d396fae696e5a25a", + "hashed_secret": "3340ad734a33028b9498d58dc8b49e9c02157b19", "is_secret": false, "is_verified": false, - "line_number": 1184, - "type": "Hex High Entropy String", + "line_number": 197, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "32daac6899f5795631ba20f4716dc2b264fc32f2", + "hashed_secret": "ec09a041656818107eb855453ffbf7327d3bbc9d", "is_secret": false, "is_verified": false, - "line_number": 1190, - "type": "Hex High Entropy String", + "line_number": 334, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/services/test_support_bundle_service.py": [ { - "hashed_secret": "57a3c2bd3253c61856df4bf13198109b0d51486d", + "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", "is_secret": false, "is_verified": false, - "line_number": 1202, - "type": "Hex High Entropy String", + "line_number": 82, + "type": "Basic Auth Credentials", "verified_result": null }, { - "hashed_secret": "802fb7ee4a23f1061a4a099e6518b448f5dea8d3", + "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", "is_secret": false, "is_verified": false, - "line_number": 1212, - "type": "Hex High Entropy String", + "line_number": 100, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "a4cf867673699f5dc3d18038b5a87c2a7bf4dfd3", + "hashed_secret": "d6b66ddd9ea7dbe760114bfe9a97352a5e139134", "is_secret": false, "is_verified": false, - "line_number": 1225, - "type": "Hex High Entropy String", + "line_number": 106, + "type": "JSON Web Token", "verified_result": null }, { - "hashed_secret": "a02ff67f8e504e59af8eff72afb26fda27037a42", + "hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8", "is_secret": false, "is_verified": false, - "line_number": 1231, - "type": "Hex High Entropy String", + "line_number": 196, + "type": "Basic Auth Credentials", "verified_result": null }, { - "hashed_secret": "d978e33d84309bf97aa5af7e47486be164c0787e", + "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", "is_secret": false, "is_verified": false, - "line_number": 1237, - "type": "Hex High Entropy String", + "line_number": 210, + "type": "Basic Auth Credentials", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/services/test_token_storage_service.py": [ { - "hashed_secret": "f2ceefa876785b2d7376d2af71e760c71ec0109e", + "hashed_secret": "f7cfcf33b07a640de88cca1a41a681d98213a43c", "is_secret": false, "is_verified": false, - "line_number": 1243, - "type": "Hex High Entropy String", + "line_number": 25, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "ea76a6819e6c7b642a58ff2639e061e2303fc28d", + "hashed_secret": "920a25ef686c4f7ca6ad23dd109d3ad653161832", "is_secret": false, "is_verified": false, - "line_number": 1249, - "type": "Hex High Entropy String", + "line_number": 255, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "20b3b413826d61a82664d755aaaa4a180123d161", + "hashed_secret": "48004e013423b89217e65eca07df9574fcd092a6", "is_secret": false, "is_verified": false, - "line_number": 1255, - "type": "Hex High Entropy String", + "line_number": 322, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/services/test_tool_service.py": [ { - "hashed_secret": "56397d9a520742406189d71ac27362811f4eb3f4", + "hashed_secret": "d63b39580934e062f89aae63426d2f2c77c3e258", "is_secret": false, "is_verified": false, - "line_number": 1264, - "type": "Hex High Entropy String", + "line_number": 503, + "type": "Base64 High Entropy String", "verified_result": null }, { - "hashed_secret": "978bcf06dc12b74b1579b446ae082265db1358f6", + "hashed_secret": "586a55a9b8b97f0cd88e24ce8279ebc955949688", "is_secret": false, "is_verified": false, - "line_number": 1270, - "type": "Hex High Entropy String", + "line_number": 504, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "8cc69a0306a6b8f4c32428f44505f11eaebb3ebf", + "hashed_secret": "00cafd126182e8a9e7c01bb2f0dfd00496be724f", "is_secret": false, "is_verified": false, - "line_number": 1279, - "type": "Hex High Entropy String", + "line_number": 520, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "6538f9a3e2204f918d5719f17a23367f4d2105c7", + "hashed_secret": "7b1552c7c7ffb8bd70b5666e5997c8e017630aab", "is_secret": false, "is_verified": false, - "line_number": 1289, - "type": "Hex High Entropy String", + "line_number": 1935, + "type": "Base64 High Entropy String", "verified_result": null }, { - "hashed_secret": "ff70e2022b5f305d9d30b5604b3d3fef49158de5", + "hashed_secret": "9fb7fe1217aed442b04c0f5e43b5d5a7d3287097", "is_secret": false, "is_verified": false, - "line_number": 1298, - "type": "Hex High Entropy String", + "line_number": 2703, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "b4259ea178dd7cb0249df5d8feb7d13a2d6fd820", + "hashed_secret": "72cb70dbbafe97e5ea13ad88acd65d08389439b0", "is_secret": false, "is_verified": false, - "line_number": 1312, - "type": "Hex High Entropy String", + "line_number": 3331, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "fb82e6c6c8d3ed2486ef8249d7e59336b5c86610", + "hashed_secret": "ee977806d7286510da8b9a7492ba58e2484c0ecc", "is_secret": false, "is_verified": false, - "line_number": 1321, - "type": "Hex High Entropy String", + "line_number": 5610, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "ba44f293d24fbbcbe1ef98385355f7fed4f8419d", + "hashed_secret": "f2e7745f43b0ef0e2c2faf61d6c6a28be2965750", "is_secret": false, "is_verified": false, - "line_number": 1327, - "type": "Hex High Entropy String", + "line_number": 6102, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "bfdcf12c00b303dde9a57ba3e08927a60a89c6b8", + "hashed_secret": "4a249743d4d2241bd2ae085b4fe654d089488295", "is_secret": false, "is_verified": false, - "line_number": 1333, - "type": "Hex High Entropy String", + "line_number": 7347, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "8be0eab839d1133fc403dcc2790b84d4dfff3381", + "hashed_secret": "0c8d051d3c7eada5d31b53d9936fce6bcc232ae2", "is_secret": false, "is_verified": false, - "line_number": 1343, - "type": "Hex High Entropy String", + "line_number": 7485, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "891ddfb5f57f136198961b98f8014e3c7bc9e602", + "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", "is_secret": false, "is_verified": false, - "line_number": 1353, - "type": "Hex High Entropy String", + "line_number": 7830, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/services/test_tool_service_coverage.py": [ { - "hashed_secret": "19c50b4f711d39a10ac1890b58e609934e3e7ff6", + "hashed_secret": "2dd2850bcc4ae4e74154aed99ee5f68292ecfec5", "is_secret": false, "is_verified": false, - "line_number": 1362, - "type": "Hex High Entropy String", + "line_number": 2234, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "74ac836ae646fd72c7c162ace0d10d50d81af86b", + "hashed_secret": "d92342976d720ff38cf5dcb329be41959ab1ba6c", "is_secret": false, "is_verified": false, - "line_number": 1371, - "type": "Hex High Entropy String", + "line_number": 2844, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "852251ba1e8919a04b1d4678fbf9ba2a73e82a19", + "hashed_secret": "f93640458964af294a485bc6d597694cf3308e1f", "is_secret": false, "is_verified": false, - "line_number": 1380, - "type": "Hex High Entropy String", + "line_number": 2853, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "0675b70f52618de044d1d1703b6c8849b407c83a", + "hashed_secret": "0857abc2d90c5d9821229fc0a880f1c38ffb0e04", "is_secret": false, "is_verified": false, - "line_number": 1391, + "line_number": 3244, "type": "Hex High Entropy String", "verified_result": null }, { - "hashed_secret": "c63addae2503bfe26923f0460158bb2fc674b928", + "hashed_secret": "99834bc4eff3f1e1c1e4692d2476b593b501d045", "is_secret": false, "is_verified": false, - "line_number": 1403, - "type": "Hex High Entropy String", + "line_number": 5529, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "9f7a7461ae8d5e27458e899b4e84416483680a51", + "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", "is_secret": false, "is_verified": false, - "line_number": 1414, - "type": "Hex High Entropy String", + "line_number": 5547, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "5b35b10ce4ffd9e769dc5edeb91e3717ca35a110", + "hashed_secret": "b8cec023ad982a1355abb9e7c7700e42d7e6fac3", "is_secret": false, "is_verified": false, - "line_number": 1420, - "type": "Hex High Entropy String", + "line_number": 5571, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "de655789d424d5a5631a8000892feda124ff3528", + "hashed_secret": "0614eb27c6e0d2f4ed9a1cce2a5bcbbbc17aa556", "is_secret": false, "is_verified": false, - "line_number": 1460, - "type": "Hex High Entropy String", + "line_number": 5635, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "bbf2c697167fb0954886a726005d59a7548b75e6", + "hashed_secret": "a38fea79a043f0c9a62f851659d459dc3b716ea9", "is_secret": false, "is_verified": false, - "line_number": 1474, - "type": "Hex High Entropy String", + "line_number": 5646, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "639bd69cb045a98edcce33e331b9ac85691c5fda", + "hashed_secret": "a50da1fb101595ac5158f2c9394a65a84061b56c", "is_secret": false, "is_verified": false, - "line_number": 1498, - "type": "Hex High Entropy String", + "line_number": 5687, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "1ac59a9207d43c177051b356ba21af36e0c5e1c3", + "hashed_secret": "ed3e0017cb8e4b06a59af1a441f62cbe58d2ef59", "is_secret": false, "is_verified": false, - "line_number": 1511, - "type": "Hex High Entropy String", + "line_number": 5693, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/test_admin.py": [ { - "hashed_secret": "63571b418c02815c6322eb603852d5e166f6810d", + "hashed_secret": "206c80413b9a96c1312cc346b7d2517b84463edd", "is_secret": false, "is_verified": false, - "line_number": 1524, - "type": "Hex High Entropy String", + "line_number": 1600, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "b64689f9c223de81042e74dbcfbe5a73a2c1544c", + "hashed_secret": "5cbd0bf2db07a8f50fa9bbcc5ac720b1911c6380", "is_secret": false, "is_verified": false, - "line_number": 1537, - "type": "Hex High Entropy String", + "line_number": 1772, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "f4ebc8e2e9f67a0dc31342a70179c4d44b292a99", + "hashed_secret": "a10b98d7340036e9c8c301704f623eddd733cc1a", "is_secret": false, "is_verified": false, - "line_number": 1546, + "line_number": 2736, "type": "Hex High Entropy String", "verified_result": null }, { - "hashed_secret": "9daac1b266d3875ecff4a4b7932e73d63f51718f", + "hashed_secret": "3acfb2c2b433c0ea7ff107e33df91b18e52f960f", "is_secret": false, "is_verified": false, - "line_number": 1557, - "type": "Hex High Entropy String", + "line_number": 5148, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "60a673ff967b996194d288e551490d960a18d4e2", + "hashed_secret": "fe1bae27cb7c1fb823f496f286e78f1d2ae87734", "is_secret": false, "is_verified": false, - "line_number": 1563, - "type": "Hex High Entropy String", + "line_number": 5795, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "8a1b53508b16a129f35039f353671356a8281e8b", + "hashed_secret": "2878cbdbbcfa6feafc04b8889f5ecc8c470ba32e", "is_secret": false, "is_verified": false, - "line_number": 1569, - "type": "Hex High Entropy String", + "line_number": 5859, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "58f0106d7117389d48944ab0421c44ca96dd7992", + "hashed_secret": "a0281cd072cea8e80e7866b05dc124815760b6c9", "is_secret": false, "is_verified": false, - "line_number": 1578, - "type": "Hex High Entropy String", + "line_number": 6111, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "bc37fdf97c85de38881099f997b799f0ce27c830", + "hashed_secret": "a0f4ea7d91495df92bbac2e2149dfb850fe81396", "is_secret": false, "is_verified": false, - "line_number": 1592, - "type": "Hex High Entropy String", + "line_number": 9086, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "190341c4e045ee5d256b052fa97f59b1d066169f", + "hashed_secret": "a75a7c7b31474f3f04f3a395228ded8d61ee1ae3", "is_secret": false, "is_verified": false, - "line_number": 1604, - "type": "Hex High Entropy String", + "line_number": 9135, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "3632f3823bb444d1697daae3d3c00f8c1472fbac", + "hashed_secret": "02c593fd9af8254b859d426a76b6cd42847fbec1", "is_secret": false, "is_verified": false, - "line_number": 1610, - "type": "Hex High Entropy String", + "line_number": 9174, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "a3f550383fa80c876ecec52c42b21b375aec47a3", + "hashed_secret": "1ded3053d0363079a4e681a3b700435d6d880290", "is_secret": false, "is_verified": false, - "line_number": 1623, - "type": "Hex High Entropy String", + "line_number": 9231, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "ba8ac4ac3810916dd75cdf9bb539b045d8f828f4", + "hashed_secret": "c00dbbc9dadfbe1e232e93a729dd4752fade0abf", "is_secret": false, "is_verified": false, - "line_number": 1633, - "type": "Hex High Entropy String", + "line_number": 14001, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "dd30e69b06fc9e7153b91d6152b47abee06bc76e", + "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", "is_secret": false, "is_verified": false, - "line_number": 1639, - "type": "Hex High Entropy String", + "line_number": 16758, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "edea2c54c8d390fdfff6535d3da4fd1fdf17b62a", + "hashed_secret": "a4b48a81cdab1e1a5dd37907d6c85ca1c61ddc7c", "is_secret": false, "is_verified": false, - "line_number": 1649, - "type": "Hex High Entropy String", + "line_number": 16777, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "bfb212002ccc72caf73acbfb0a5bd6c1c2acaa42", + "hashed_secret": "dc8002865f92070749b264e76045b04fa3b8de71", "is_secret": false, "is_verified": false, - "line_number": 1658, - "type": "Hex High Entropy String", + "line_number": 20332, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/test_admin_catalog_htmx.py": [ { - "hashed_secret": "044ff8fe1e6d9bb34b8e097fa053256f451e1d65", + "hashed_secret": "dff6d4ff5dc357cf451d1855ab9cbda562645c9f", "is_secret": false, "is_verified": false, - "line_number": 1669, - "type": "Hex High Entropy String", + "line_number": 244, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/test_admin_module.py": [ { - "hashed_secret": "93658223830f17449a3b81a759279f8db378716a", + "hashed_secret": "1a91d62f7ca67399625a4368a6ab5d4a3baa6073", "is_secret": false, "is_verified": false, - "line_number": 1680, - "type": "Hex High Entropy String", + "line_number": 453, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "6d252f2c2f14196d2164788e909d288b535a7730", + "hashed_secret": "c00dbbc9dadfbe1e232e93a729dd4752fade0abf", "is_secret": false, "is_verified": false, - "line_number": 1693, - "type": "Hex High Entropy String", + "line_number": 1106, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/test_auth.py": [ { - "hashed_secret": "f4fd806c07aaf73c1529b88212a826fadcae43ae", + "hashed_secret": "b5920aa41ccde1341077fdb2636afc16d6da0e1a", "is_secret": false, "is_verified": false, - "line_number": 1702, - "type": "Hex High Entropy String", + "line_number": 180, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "dafd46fad0421f3f00574f5c736ec0847f53d212", + "hashed_secret": "467be688b21f5d43370f5dc983fe5ed58dea4b00", "is_secret": false, "is_verified": false, - "line_number": 1714, - "type": "Hex High Entropy String", + "line_number": 206, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "8f48b5146d88920187ca1c8d1d52cd0b008f12de", + "hashed_secret": "d955debea8e960a293ecbb257048680898cc9d94", "is_secret": false, "is_verified": false, - "line_number": 1723, - "type": "Hex High Entropy String", + "line_number": 221, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "083e28bdae3cd5b3af135e50a5758bd732e076c4", + "hashed_secret": "4ee9e7b8482f6e0282b0117eb81651c98b537589", "is_secret": false, "is_verified": false, - "line_number": 1729, - "type": "Hex High Entropy String", + "line_number": 255, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "630ac66c09261f06d21e712acac6287e3d573cc9", + "hashed_secret": "500886a66d19ebfe2d5c0ee86cf4612f9458ee4f", "is_secret": false, "is_verified": false, - "line_number": 1739, - "type": "Hex High Entropy String", + "line_number": 328, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "1eeb415c50a8d2b04a705ebf09786adf00d8cf59", + "hashed_secret": "f2169f1a808382eb7b521206debaef01c91f1b60", "is_secret": false, "is_verified": false, - "line_number": 1745, - "type": "Hex High Entropy String", + "line_number": 367, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "14b703e941e97ca4115fbfc7afc9d19920aa9c6c", + "hashed_secret": "cd61d9f689e9992aec40aeec7210b45a681309f9", "is_secret": false, "is_verified": false, - "line_number": 1751, - "type": "Hex High Entropy String", + "line_number": 401, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "f711a3e8a220ceb3c9b360745772e4ed8659f46c", + "hashed_secret": "ae97d9e9201a9bc1806069d80e5809ab745a91e8", "is_secret": false, "is_verified": false, - "line_number": 1757, - "type": "Hex High Entropy String", + "line_number": 436, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "1180001a3c0dcef8ab3a4427e6aab1011a1fc56d", + "hashed_secret": "eb2344b0e2d6529ced74f8b09be362a460defa15", "is_secret": false, "is_verified": false, - "line_number": 1767, - "type": "Hex High Entropy String", + "line_number": 473, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "7d64118ef4ee08c6e431da22c00cc6db06fc8495", + "hashed_secret": "89be7e8e88bc7b0cdb5ddaa01dab1cd1728541e2", "is_secret": false, "is_verified": false, - "line_number": 1776, - "type": "Hex High Entropy String", + "line_number": 537, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "fc643b8317813d4055f095a72f90dfc83ac98a52", + "hashed_secret": "5a539b200edec3aaa5167e1cde5d8fd0e8cf4aee", "is_secret": false, "is_verified": false, - "line_number": 1782, - "type": "Hex High Entropy String", + "line_number": 550, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "09b70bdc3f035c1579508c2a4a7a626d980c5e0c", + "hashed_secret": "8be6db326ca95f74590bd09c9c328fc9a3db3b6c", "is_secret": false, "is_verified": false, - "line_number": 1788, - "type": "Hex High Entropy String", + "line_number": 688, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "fed671d6a24626d6cf16f91b98f2b11ba736d095", + "hashed_secret": "2368055dc74fca43f2f9c849dbbaaa04616670cf", "is_secret": false, "is_verified": false, - "line_number": 1794, - "type": "Hex High Entropy String", + "line_number": 737, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "33eb20252a41cba23b834a062ead51beccf29376", + "hashed_secret": "8ce647089e0614d3b924f4a0b8fb78268562ec7c", "is_secret": false, "is_verified": false, - "line_number": 1805, - "type": "Hex High Entropy String", + "line_number": 1426, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "24c6c50199abdbdf01f5bfc41c06d323bb57ac6b", + "hashed_secret": "dc6f6cc49f767ae38db110a9e6c1adf5c651ecbf", "is_secret": false, "is_verified": false, - "line_number": 1814, - "type": "Hex High Entropy String", + "line_number": 1470, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "611820b81f8ac5cb3dca1a7fb85e67110bc551c5", + "hashed_secret": "7939ea1cdfc6d73115b0d736fa04012be2b25b18", "is_secret": false, "is_verified": false, - "line_number": 1825, - "type": "Hex High Entropy String", + "line_number": 1561, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "f57b63e67ed485c5ffe3dec0888657ac4c281ca5", + "hashed_secret": "91014bcbf183cf77807b4bd24f2e8652f67b56a0", "is_secret": false, "is_verified": false, - "line_number": 1838, - "type": "Hex High Entropy String", + "line_number": 2069, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "824096ad190248d40cef63862048a64d18e73093", + "hashed_secret": "61ac75f7e6677b79c219ce819d954e05f61b5d2c", "is_secret": false, "is_verified": false, - "line_number": 1847, - "type": "Hex High Entropy String", + "line_number": 2087, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "67eb96441e8f50509300c66e81b167f6e3f0f21d", + "hashed_secret": "8223fafd35f399e55c3cf611c3d6ebbf43d68d71", "is_secret": false, "is_verified": false, - "line_number": 1858, - "type": "Hex High Entropy String", + "line_number": 2733, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "09057af5054a5ad648e142f3d8f20650c1961474", + "hashed_secret": "dfd99b5f25f839608a3c275c0f8ceb363f8f0bc0", "is_secret": false, "is_verified": false, - "line_number": 1867, - "type": "Hex High Entropy String", + "line_number": 3510, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "b62ab1173e770de947a50fe2b3ae83805000809b", + "hashed_secret": "5038e18712161fca54e52805726d3c70b296eff6", "is_secret": false, "is_verified": false, - "line_number": 1882, - "type": "Hex High Entropy String", + "line_number": 3619, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "de47997a6ccac36e55eafe4f8112bbe269455d65", + "hashed_secret": "f5cd573c18bf811f82a886ceb6866b05d5ef02cd", "is_secret": false, "is_verified": false, - "line_number": 1888, - "type": "Hex High Entropy String", + "line_number": 3697, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "86efcec63d7f5e5c3cf34e1acfd29fc257f3c382", + "hashed_secret": "79bead8e6d65862a00cffaa12ccde1189ec34d29", "is_secret": false, "is_verified": false, - "line_number": 1898, - "type": "Hex High Entropy String", + "line_number": 4080, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/test_cli_export_import_coverage.py": [ { - "hashed_secret": "8e11a5e472d1d4d98e2bdc00b90448cca073dd97", + "hashed_secret": "5ef3af6cd9b5aa907fa618fac829baaec6763b42", "is_secret": false, "is_verified": false, - "line_number": 1908, - "type": "Hex High Entropy String", + "line_number": 685, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/test_config.py": [ { - "hashed_secret": "a5ea7e9c1ce564f30e9551f8200f29d90aa6ec5e", + "hashed_secret": "7952d9e777d5fa3933a6132f35493b970e1c8828", "is_secret": false, "is_verified": false, - "line_number": 1925, - "type": "Hex High Entropy String", + "line_number": 163, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "7794fee4eaeafc705f8eeba3b4d69cba7869dab0", + "hashed_secret": "78466ed9a08daa9faf88434c1dd6bb8761e98a61", "is_secret": false, "is_verified": false, - "line_number": 1936, - "type": "Hex High Entropy String", + "line_number": 460, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "8aecd4c13a7b746e2dd4cfe9762061501833943a", + "hashed_secret": "51beb0ccb2d6f1365ed1278c636dabcd8797db95", "is_secret": false, "is_verified": false, - "line_number": 1946, - "type": "Hex High Entropy String", + "line_number": 473, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "e5720b6d98058e32a6194a43416caa780707a3de", + "hashed_secret": "95bb8a28fc0d18320a4d8deae3bd9c043709a22f", "is_secret": false, "is_verified": false, - "line_number": 1956, - "type": "Hex High Entropy String", + "line_number": 479, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "dcb719d0fd8853d6851ce72bfe162430bd5541c1", + "hashed_secret": "cbfdac6008f9cab4083784cbd1874f76618d2a97", "is_secret": false, "is_verified": false, - "line_number": 1967, - "type": "Hex High Entropy String", + "line_number": 548, + "type": "Basic Auth Credentials", "verified_result": null }, { - "hashed_secret": "5a9e3d91c47e5f9099917f07fd7130195338b4ca", + "hashed_secret": "0f0ab1d14970dea160a53133a1b2487ba464fda3", "is_secret": false, "is_verified": false, - "line_number": 1978, - "type": "Hex High Entropy String", + "line_number": 608, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "8736cc66191df4fc4b9ed7736fec891760767e62", + "hashed_secret": "516b9783fca517eecbd1d064da2d165310b19759", "is_secret": false, "is_verified": false, - "line_number": 1991, - "type": "Hex High Entropy String", + "line_number": 973, + "type": "Basic Auth Credentials", "verified_result": null }, { - "hashed_secret": "65198a7292403ca29166dbaadb980fa1296fd56e", + "hashed_secret": "ef4eb24299c517306652ffee61e05934f2224914", "is_secret": false, "is_verified": false, - "line_number": 2006, - "type": "Hex High Entropy String", + "line_number": 1192, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/test_db.py": [ { - "hashed_secret": "e9932a2f581ce1c412466aba20ce5938f4ce4b6f", + "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", "is_secret": false, "is_verified": false, - "line_number": 2015, - "type": "Hex High Entropy String", + "line_number": 2421, + "type": "Basic Auth Credentials", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/test_db_isready.py": [ { - "hashed_secret": "544a059b6156aa5094965cbf44cacd5232cdc84f", + "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", "is_secret": false, "is_verified": false, - "line_number": 2024, - "type": "Hex High Entropy String", + "line_number": 72, + "type": "Basic Auth Credentials", "verified_result": null }, { - "hashed_secret": "79a0a8be10240450c72322f433eb92948903500f", + "hashed_secret": "1a91d62f7ca67399625a4368a6ab5d4a3baa6073", "is_secret": false, "is_verified": false, - "line_number": 2030, - "type": "Hex High Entropy String", + "line_number": 115, + "type": "Basic Auth Credentials", "verified_result": null }, { - "hashed_secret": "102dfadad7f84fbd0da59ec7a4360651e8018702", + "hashed_secret": "516b9783fca517eecbd1d064da2d165310b19759", "is_secret": false, "is_verified": false, - "line_number": 2045, - "type": "Hex High Entropy String", + "line_number": 224, + "type": "Basic Auth Credentials", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/test_display_name_uuid_features.py": [ { - "hashed_secret": "2bb2fe1e57f60f9972d8dc8aafecc14ea140b262", + "hashed_secret": "c377074d6473f35a91001981355da793dc808ffd", "is_secret": false, "is_verified": false, - "line_number": 2063, + "line_number": 620, "type": "Hex High Entropy String", "verified_result": null }, { - "hashed_secret": "d93f2cd8895569df797eb6a57011aa1489604dee", + "hashed_secret": "99c962e8c62296bdc9a17f5caf91ce9bb4c7e0e6", "is_secret": false, "is_verified": false, - "line_number": 2069, + "line_number": 621, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/test_final_coverage_push.py": [ { - "hashed_secret": "3f85aa33de16e014a62133be00e2fae3fee16a15", + "hashed_secret": "5ef3af6cd9b5aa907fa618fac829baaec6763b42", "is_secret": false, "is_verified": false, - "line_number": 2075, - "type": "Hex High Entropy String", + "line_number": 222, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/test_llm_schemas.py": [ { - "hashed_secret": "07bedfcb80ecb1e20c6167094b9715b4c25e3d28", + "hashed_secret": "3acfb2c2b433c0ea7ff107e33df91b18e52f960f", "is_secret": false, "is_verified": false, - "line_number": 2086, - "type": "Hex High Entropy String", + "line_number": 67, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/test_main.py": [ { - "hashed_secret": "319d3f52ed81b3c7c6a69e0ddb313dba40f94795", + "hashed_secret": "cd024c09e5784e941e833bd8fabf1dcfc3fb6cd8", "is_secret": false, "is_verified": false, - "line_number": 2098, - "type": "Hex High Entropy String", + "line_number": 53, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "99bce2688418d3adf96547726401effde2101407", + "hashed_secret": "718cbcc5a4207c0d5f38e3a309bdba17cb0074b7", "is_secret": false, "is_verified": false, - "line_number": 2109, + "line_number": 137, "type": "Hex High Entropy String", "verified_result": null }, { - "hashed_secret": "611ee95376df586616103d908ee6f0e50de5ce6f", + "hashed_secret": "a10b98d7340036e9c8c301704f623eddd733cc1a", "is_secret": false, "is_verified": false, - "line_number": 2119, + "line_number": 150, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/test_main_error_handlers.py": [ { - "hashed_secret": "8792035d236f541da7fcdacf04647f5050fd4f37", + "hashed_secret": "cd024c09e5784e941e833bd8fabf1dcfc3fb6cd8", "is_secret": false, "is_verified": false, - "line_number": 2130, - "type": "Hex High Entropy String", + "line_number": 30, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/test_main_extended.py": [ { - "hashed_secret": "1c7d5dc44e70c25f3c80013f6b1fcf41995ed951", + "hashed_secret": "6745fa570d36be08400efa1cbc2f057bb001290e", "is_secret": false, "is_verified": false, - "line_number": 2148, + "line_number": 2278, "type": "Hex High Entropy String", "verified_result": null }, { - "hashed_secret": "d62f1b254d7ecac7effa42e8a2e02d3baf03bd1b", + "hashed_secret": "4f13f134744a2fadbbe2d624687246347d12fa63", "is_secret": false, "is_verified": false, - "line_number": 2159, + "line_number": 2565, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/test_multi_auth_headers.py": [ { - "hashed_secret": "2bba312da1074a195e616dd95b0d9970dcbf623b", + "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", "is_secret": false, "is_verified": false, - "line_number": 2169, - "type": "Hex High Entropy String", + "line_number": 298, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/test_oauth_manager.py": [ { - "hashed_secret": "2cec7afe8f0f8f21d28f274299142cbbe949023c", + "hashed_secret": "9fb7fe1217aed442b04c0f5e43b5d5a7d3287097", "is_secret": false, "is_verified": false, - "line_number": 2175, - "type": "Hex High Entropy String", + "line_number": 71, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "370f2dab232e01715bb1463fe9a85a8a0e3b41ed", + "hashed_secret": "00942f4668670f34c5943cf52c7ef3139fe2b8d6", "is_secret": false, "is_verified": false, - "line_number": 2181, - "type": "Hex High Entropy String", + "line_number": 1225, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "c461b9dd39d7217a96ec3c56280634a76454701d", + "hashed_secret": "233243ef95e736679cb1d5664a4c71ba89c10664", "is_secret": false, "is_verified": false, - "line_number": 2187, - "type": "Hex High Entropy String", + "line_number": 1361, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "d8f1d314d800de9172c39e761f105816ae4af3e8", + "hashed_secret": "72cb70dbbafe97e5ea13ad88acd65d08389439b0", "is_secret": false, "is_verified": false, - "line_number": 2193, - "type": "Hex High Entropy String", + "line_number": 1829, + "type": "Secret Keyword", "verified_result": null + } + ], + "tests/unit/mcpgateway/test_observability.py": [ + { + "hashed_secret": "8f1b63868b5d31deb06a0ff740b192aa490e8950", + "is_verified": false, + "line_number": 326, + "type": "Secret Keyword", + "verified_result": null, + "is_secret": false }, { - "hashed_secret": "a0ccf26d3e5e73d1fea4cbd7e882f8a4269b51e6", + "hashed_secret": "1fb844731bf1e5928ae606915b54e21264da4768", "is_secret": false, "is_verified": false, - "line_number": 2205, - "type": "Hex High Entropy String", + "line_number": 755, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/test_postgresql_schema_config.py": [ { - "hashed_secret": "83719d2d8284b30dee2823349c2d7f600e237d8c", + "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", "is_secret": false, "is_verified": false, - "line_number": 2211, - "type": "Hex High Entropy String", + "line_number": 167, + "type": "Basic Auth Credentials", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/test_schemas.py": [ { - "hashed_secret": "788b34ba07c35e639f6e3e82f608189791e3239f", + "hashed_secret": "22fcf027f5caee316ca4f2963c475f62e87e9b76", "is_secret": false, "is_verified": false, - "line_number": 2217, + "line_number": 829, "type": "Hex High Entropy String", "verified_result": null }, { - "hashed_secret": "a2de2067076eb603a022b1b629bba6167194fcaa", + "hashed_secret": "6af3c121ed4a752936c297cddfb7b00394eabf10", "is_secret": false, "is_verified": false, - "line_number": 2223, - "type": "Hex High Entropy String", + "line_number": 1081, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/test_schemas_auth_validation.py": [ { - "hashed_secret": "831b1f0f0af506c6b1b5c2de459b9b4b4f3ac633", + "hashed_secret": "40461afdef055768f498c9f11ca7d42beaf667b6", "is_secret": false, "is_verified": false, - "line_number": 2229, - "type": "Hex High Entropy String", + "line_number": 333, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "d1837f7e31117d99b0cbffd82e7eabcea851a94c", + "hashed_secret": "364d84ed2a75ef4c9a3cba031109db88e504511b", "is_secret": false, "is_verified": false, - "line_number": 2238, - "type": "Hex High Entropy String", + "line_number": 352, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "040278ead73fc50f601174bf24cfe3552915a67e", + "hashed_secret": "42d7bc4801cb72f2559bd9f6c9af9a2707ca321e", "is_secret": false, "is_verified": false, - "line_number": 2244, - "type": "Hex High Entropy String", + "line_number": 371, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "b85b391225b20c3137322bcc23c7e2c31320a83e", + "hashed_secret": "379b59bcc5d7088a11af63318381a83709402009", "is_secret": false, "is_verified": false, - "line_number": 2253, - "type": "Hex High Entropy String", + "line_number": 382, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/test_schemas_validators_extra.py": [ { - "hashed_secret": "604edf3bd2202bebf89e23895a6ef68088f57e42", + "hashed_secret": "277fd76456880437641f76de1bfa6d7ef61ae861", "is_secret": false, "is_verified": false, - "line_number": 2262, - "type": "Hex High Entropy String", + "line_number": 775, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "a98f930ed9e869e7534be3e4b8ca121b2f46b3cc", + "hashed_secret": "4ea8d2335b430796cf3f500368c5b0f5b1dc90f5", "is_secret": false, "is_verified": false, - "line_number": 2275, - "type": "Hex High Entropy String", + "line_number": 956, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "7167df607c246fe7c20de1a7b7abc394c7026cd0", + "hashed_secret": "57b2ad99044d337197c0c39fd3823568ff81e48a", "is_secret": false, "is_verified": false, - "line_number": 2289, - "type": "Hex High Entropy String", + "line_number": 961, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "f0f817643483baa005cf31ad44d972f65508675e", + "hashed_secret": "3d12349760de61e8070eea4704d2c471731bdd15", "is_secret": false, "is_verified": false, - "line_number": 2299, - "type": "Hex High Entropy String", + "line_number": 987, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "64c9975a7ab1b5bcd2439c334f2370a65d4aae82", + "hashed_secret": "640d87e741e6aa4c669a82a4cd304787960513ab", "is_secret": false, "is_verified": false, - "line_number": 2312, - "type": "Hex High Entropy String", + "line_number": 1005, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "d3858567b9054a3163ab6dfd94a79713a72f7cec", + "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", "is_secret": false, "is_verified": false, - "line_number": 2321, - "type": "Hex High Entropy String", + "line_number": 1218, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "042d13670968f7c52be4bc75a67035bdc4128e45", + "hashed_secret": "8db15e8ba2f571ba5d630b4edf3a52d265668f0a", "is_secret": false, "is_verified": false, - "line_number": 2331, - "type": "Hex High Entropy String", + "line_number": 1334, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/test_team_governance_flags.py": [ { - "hashed_secret": "af4aec7dea6087b343019e88153e61a650d6e6dc", + "hashed_secret": "8ac21c6ecda35ffb18d58264aeb43ca800b3d758", "is_secret": false, "is_verified": false, - "line_number": 2343, - "type": "Hex High Entropy String", + "line_number": 580, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/test_toolops_utils.py": [ { - "hashed_secret": "35f86be9fc56fed00caa9d04d07259deee6e5713", + "hashed_secret": "a62f2225bf70bfaccbc7f1ef2a397836717377de", "is_secret": false, "is_verified": false, - "line_number": 2356, - "type": "Hex High Entropy String", + "line_number": 135, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/test_translate_stdio_endpoint.py": [ { - "hashed_secret": "7493d3c7ad45735197216f5195a3a4b2f0dc05f8", + "hashed_secret": "528ceffb33609fca3a5646ec64a032d4362d301b", "is_secret": false, "is_verified": false, - "line_number": 2368, - "type": "Hex High Entropy String", + "line_number": 277, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/test_version.py": [ { - "hashed_secret": "6d88ddb7d21a6a9b690f32635580a07f04713de5", + "hashed_secret": "516b9783fca517eecbd1d064da2d165310b19759", "is_secret": false, "is_verified": false, - "line_number": 2378, - "type": "Hex High Entropy String", + "line_number": 183, + "type": "Basic Auth Credentials", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/tools/builder/test_common.py": [ { - "hashed_secret": "0a3b77952502a84e59c4ab58fbcbe92b5df6e5e9", + "hashed_secret": "206c80413b9a96c1312cc346b7d2517b84463edd", "is_secret": false, "is_verified": false, - "line_number": 2390, - "type": "Hex High Entropy String", + "line_number": 668, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "4388f4a1071af57b8f44733a4b590114701c8263", + "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", "is_secret": false, "is_verified": false, - "line_number": 2399, - "type": "Hex High Entropy String", + "line_number": 946, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/tools/builder/test_schema.py": [ { - "hashed_secret": "e41371014435a1b6db1f2d6b30c59f17f356077c", + "hashed_secret": "fd858ef1e811e28a823d60908b38df56e9e359e7", "is_secret": false, "is_verified": false, - "line_number": 2412, - "type": "Hex High Entropy String", + "line_number": 201, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/transports/test_streamablehttp_transport.py": [ { - "hashed_secret": "c635956e53a2624afd87d3ac8a41701e377f00e7", + "hashed_secret": "b4c9248600a42f8c38c01b632f392dbcb4c7b19a", "is_secret": false, "is_verified": false, - "line_number": 2423, + "line_number": 11072, "type": "Hex High Entropy String", "verified_result": null }, { - "hashed_secret": "5747d8af9684661644b7df2fe00e51dc16f6843d", + "hashed_secret": "90bd1b48e958257948487b90bee080ba5ed00caa", "is_secret": false, "is_verified": false, - "line_number": 2434, + "line_number": 12209, "type": "Hex High Entropy String", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/utils/test_create_jwt_token.py": [ { - "hashed_secret": "a5f5903bd227b9d811f037da91086231b40422a7", + "hashed_secret": "cd024c09e5784e941e833bd8fabf1dcfc3fb6cd8", "is_secret": false, "is_verified": false, - "line_number": 2445, - "type": "Hex High Entropy String", + "line_number": 42, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/utils/test_db_isready.py": [ { - "hashed_secret": "8bcb6f0287067ce172f04cf87f61722d4bf477c4", + "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", "is_secret": false, "is_verified": false, - "line_number": 2451, - "type": "Hex High Entropy String", + "line_number": 75, + "type": "Basic Auth Credentials", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/utils/test_generate_keys.py": [ { - "hashed_secret": "34cfa36d78842de5ac91d80bd7a8996d903abee8", + "hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9", "is_secret": false, "is_verified": false, - "line_number": 2461, - "type": "Hex High Entropy String", + "line_number": 32, + "type": "Private Key", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/utils/test_proxy_auth.py": [ { - "hashed_secret": "6be5b6b2b339ebb0359e498fb3752087d91f5ea2", + "hashed_secret": "b2d898cda25886738c0cd002eb080745b09e2332", "is_secret": false, "is_verified": false, - "line_number": 2470, - "type": "Hex High Entropy String", + "line_number": 23, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/utils/test_sso_bootstrap.py": [ { - "hashed_secret": "84195544d364bd618a1ff2934f7820b59206d21a", + "hashed_secret": "fe1bae27cb7c1fb823f496f286e78f1d2ae87734", "is_secret": false, "is_verified": false, - "line_number": 2479, - "type": "Hex High Entropy String", + "line_number": 370, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "f9de3f6e836fa1b0a50f4e0e06154a422c0ed705", + "hashed_secret": "945db841c03e42eef2f3d0a4ff310e2f3b3e59ec", "is_secret": false, "is_verified": false, - "line_number": 2488, - "type": "Hex High Entropy String", + "line_number": 454, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/utils/test_trace_redaction.py": [ { - "hashed_secret": "822e00a0f89ac349cbf702531f4f033260828e21", - "is_secret": false, + "hashed_secret": "36c3eaa0e1e290f41e2810bae8d9502c785e92d9", "is_verified": false, - "line_number": 2497, - "type": "Hex High Entropy String", - "verified_result": null - }, + "line_number": 50, + "type": "Secret Keyword", + "verified_result": null, + "is_secret": false + } + ], + "tests/unit/mcpgateway/utils/test_url_auth.py": [ { - "hashed_secret": "74d93378d470bc4f789d3d428d036fa2d628f593", + "hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511", "is_secret": false, "is_verified": false, - "line_number": 2513, - "type": "Hex High Entropy String", + "line_number": 40, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "0fdd9df07d0f5a4b49a81d92bbb086763150fbc0", + "hashed_secret": "e48c47f1431afd3bb030deea266b4309e2228c5b", "is_secret": false, "is_verified": false, - "line_number": 2522, - "type": "Hex High Entropy String", + "line_number": 47, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "faa0ddbbe89ccdee24b39e7fb6cd924d84b9f95c", + "hashed_secret": "6367c48dd193d56ea7b0baad25b19455e529f5ee", "is_secret": false, "is_verified": false, - "line_number": 2528, - "type": "Hex High Entropy String", + "line_number": 57, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "729601340d94890d89e69f03762589a8cbf89cec", + "hashed_secret": "fd0ef1fb9f5dbc367c4ec061500002a22d109bab", "is_secret": false, "is_verified": false, - "line_number": 2534, - "type": "Hex High Entropy String", + "line_number": 65, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "ea581e0c5da35c1c464cd32062801ed7632d833c", + "hashed_secret": "de9f9d75dc3dd5f8f16d484e569b75fd2e69c86f", "is_secret": false, "is_verified": false, - "line_number": 2540, - "type": "Hex High Entropy String", + "line_number": 80, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "a9df4b3b5d643994b2d1015d8de874ffe74eebd6", + "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", "is_secret": false, "is_verified": false, - "line_number": 2546, - "type": "Hex High Entropy String", + "line_number": 153, + "type": "Basic Auth Credentials", "verified_result": null }, { - "hashed_secret": "15b35fb3118b66a879fd0b785ea79a11a3b69040", + "hashed_secret": "b02dff0ec9d24823e77c27c281a852247896b86d", "is_secret": false, "is_verified": false, - "line_number": 2552, - "type": "Hex High Entropy String", + "line_number": 155, + "type": "Basic Auth Credentials", "verified_result": null }, { - "hashed_secret": "fbe26dbbaa1b01b1932e5a13b04b7ffb88ef2d21", + "hashed_secret": "9bebe8d61ea4965d1205e9e0f8f0c6bf5262880f", "is_secret": false, "is_verified": false, - "line_number": 2558, - "type": "Hex High Entropy String", + "line_number": 215, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/utils/test_verify_credentials.py": [ { - "hashed_secret": "cf45dbb800154fd3ee6025a606eec2d87171f1ba", + "hashed_secret": "cd024c09e5784e941e833bd8fabf1dcfc3fb6cd8", "is_secret": false, "is_verified": false, - "line_number": 2564, - "type": "Hex High Entropy String", + "line_number": 53, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "e67b0b7e7387fa4b492322f78c37bbae56f8aede", + "hashed_secret": "29534257453ead25854695b1b7c95e3857a7238d", "is_secret": false, "is_verified": false, - "line_number": 2570, - "type": "Hex High Entropy String", + "line_number": 114, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "0253824dccde843d47fa65b99bb29912e5ec3a0a", + "hashed_secret": "a4b48a81cdab1e1a5dd37907d6c85ca1c61ddc7c", "is_secret": false, "is_verified": false, - "line_number": 2576, - "type": "Hex High Entropy String", + "line_number": 1042, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "d8a8d970a479e89ac4678dabd4216dfa8b22e040", + "hashed_secret": "ee977806d7286510da8b9a7492ba58e2484c0ecc", "is_secret": false, "is_verified": false, - "line_number": 2585, - "type": "Hex High Entropy String", + "line_number": 1191, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/mcpgateway/validation/test_validators_advanced.py": [ { - "hashed_secret": "d4e3e4ca451b86ca4ea86a0b2db6ec3ff7c01c22", + "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", "is_secret": false, "is_verified": false, - "line_number": 2596, - "type": "Hex High Entropy String", + "line_number": 1142, + "type": "Basic Auth Credentials", "verified_result": null - }, + } + ], + "tests/unit/plugins/test_encoded_exfil_detector.py": [ { - "hashed_secret": "84469e279d411898bc4109bd5c351e99cec1a050", + "hashed_secret": "55d2534ed6ad4f269b428160428fa2f6f541ba7b", "is_secret": false, "is_verified": false, - "line_number": 2612, - "type": "Hex High Entropy String", + "line_number": 156, + "type": "Base64 High Entropy String", "verified_result": null }, { - "hashed_secret": "8f9c265c3866a77dcb45fd46fa0b83dc78acf82d", - "is_secret": false, + "hashed_secret": "cf743b3a58a4d0f91c1d7f5825c0b1b5f7758174", "is_verified": false, - "line_number": 2627, - "type": "Hex High Entropy String", - "verified_result": null + "line_number": 538, + "type": "Base64 High Entropy String", + "verified_result": null, + "is_secret": false }, { - "hashed_secret": "4eee70986dc22664f0597495a6d0dd22fc5422c4", - "is_secret": false, + "hashed_secret": "8e42b03e460b2cf358ffbcf4da3bc5d14a22c86e", "is_verified": false, - "line_number": 2646, - "type": "Hex High Entropy String", - "verified_result": null + "line_number": 582, + "type": "Base64 High Entropy String", + "verified_result": null, + "is_secret": false }, { - "hashed_secret": "54f3ab20cd8669d07e74b7b8767b7c2d2be98e84", - "is_secret": false, + "hashed_secret": "2093dd9cf307518cfe1d2fa5a3985d6fec4e995e", "is_verified": false, - "line_number": 2664, - "type": "Hex High Entropy String", - "verified_result": null + "line_number": 595, + "type": "Base64 High Entropy String", + "verified_result": null, + "is_secret": false }, { - "hashed_secret": "ed1ba03fabc77d3aa81f08b87029efbd7d9a1340", - "is_secret": false, + "hashed_secret": "caa924f200b35ceb6f0e33878faff75203bdccb4", "is_verified": false, - "line_number": 2670, - "type": "Hex High Entropy String", - "verified_result": null + "line_number": 971, + "type": "Secret Keyword", + "verified_result": null, + "is_secret": false }, { - "hashed_secret": "3a49d16dc29026d169224f8bc14e6828a62128c2", - "is_secret": false, + "hashed_secret": "f16da2820437f3c703ff5b95c813f310ce8e67a4", "is_verified": false, - "line_number": 2681, - "type": "Hex High Entropy String", - "verified_result": null - }, + "line_number": 1288, + "type": "Secret Keyword", + "verified_result": null, + "is_secret": false + } + ], + "tests/unit/plugins/test_jwt_claims_extraction.py": [ { - "hashed_secret": "f2031cef7184ff2655c2795f8f53c0451d4ea648", + "hashed_secret": "dccfe30496fb85f5bb560ea18b582b9aa50372bb", "is_secret": false, "is_verified": false, - "line_number": 2693, - "type": "Hex High Entropy String", + "line_number": 153, + "type": "Secret Keyword", "verified_result": null - }, + } + ], + "tests/unit/plugins/test_secrets_detection.py": [ { - "hashed_secret": "fb3ffd7b8e0ef97a3b404c2db1b91868d28b0298", + "hashed_secret": "86de8c52637ec530fe39b0a8471da9b8764d5242", "is_secret": false, "is_verified": false, - "line_number": 2702, - "type": "Hex High Entropy String", + "line_number": 466, + "type": "AWS Access Key", "verified_result": null - }, + } + ], + "tests/unit/test_conc_01_helpers.py": [ { - "hashed_secret": "a58d51dc68f673a049f4d82bdf01e61cf73b93b2", + "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", "is_secret": false, "is_verified": false, - "line_number": 2713, - "type": "Hex High Entropy String", + "line_number": 160, + "type": "Basic Auth Credentials", "verified_result": null - }, + } + ], + "tools_rust/mcp_runtime/src/lib.rs": [ { - "hashed_secret": "86abb8da6326f0620dc9f0951186d370cad910b9", + "hashed_secret": "df1431b489758b92c84bdec3c9283b96066a44b8", "is_secret": false, "is_verified": false, - "line_number": 2722, - "type": "Hex High Entropy String", + "line_number": 90, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "3b7aac6c5358eda96f36b1d1a7df21dd571dec15", + "hashed_secret": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684", "is_secret": false, "is_verified": false, - "line_number": 2734, - "type": "Hex High Entropy String", + "line_number": 10450, + "type": "Basic Auth Credentials", "verified_result": null - }, + } + ], + "tools_rust/mcp_runtime/src/observability.rs": [ { - "hashed_secret": "fa35aa28d81cf737ac8af26aae31b0b195eef3af", - "is_secret": false, + "hashed_secret": "b7dd0ec3dc49487982011219e66db3716b6669c6", "is_verified": false, - "line_number": 2740, - "type": "Hex High Entropy String", - "verified_result": null + "line_number": 596, + "type": "Secret Keyword", + "verified_result": null, + "is_secret": false }, { - "hashed_secret": "6274c331e3520248957d3a2497e2edab2c3aaee0", + "hashed_secret": "d2b1620ca7a5314280e595624e8b13c185f8a2e1", + "is_verified": false, + "line_number": 785, + "type": "Secret Keyword", + "verified_result": null, + "is_secret": false + } + ], + "tools_rust/mcp_runtime/tests/runtime.rs": [ + { + "hashed_secret": "5b204323030835cdda5d258742d1452e812988de", "is_secret": false, "is_verified": false, - "line_number": 2751, - "type": "Hex High Entropy String", + "line_number": 1611, + "type": "Secret Keyword", "verified_result": null }, { - "hashed_secret": "27ab5e55f6ba7afa0e99492446d43050c7b719fa", + "hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3", "is_secret": false, "is_verified": false, - "line_number": 2762, - "type": "Hex High Entropy String", + "line_number": 4425, + "type": "Basic Auth Credentials", "verified_result": null }, { - "hashed_secret": "94014c04a964970c30d309870f97ea5d8ea5bfd8", + "hashed_secret": "d6c1622f5e897dac7dcc4fab2cded03cb8240caa", "is_secret": false, "is_verified": false, - "line_number": 2773, - "type": "Hex High Entropy String", + "line_number": 5836, + "type": "Secret Keyword", "verified_result": null } ], diff --git a/Makefile b/Makefile index 41889c2ae7..b643231269 100644 --- a/Makefile +++ b/Makefile @@ -2575,7 +2575,15 @@ load-test-agentgateway-mcp-server-time: ## Load test external MCP server (loc # help: load-test-mcp-protocol-heavy - MCP-only protocol heavy test (500 users, 5min) MCP_PROTOCOL_LOCUSTFILE ?= tests/loadtest/locustfile_mcp_protocol.py -MCP_RATE_LIMITER_LOCUSTFILE ?= tests/loadtest/locustfile_rate_limiter.py +MCP_RATE_LIMITER_LOCUSTFILE ?= tests/loadtest/locustfile_rate_limiter_backend_correctness.py +MCP_RATE_LIMITER_SCALE_LOCUSTFILE ?= tests/loadtest/locustfile_rate_limiter_scale.py +MCP_RATE_LIMITER_REDIS_CAPACITY_LOCUSTFILE ?= tests/loadtest/locustfile_rate_limiter_redis_capacity.py +RL_ALGORITHM ?= fixed_window +RL_USERS ?= 100 +RL_SPAWN_RATE ?= 10 +RL_REQS_PER_SECOND ?= 0.25 +RL_PROMPT_ID ?= +RATE_LIMITER_FORCE_PYTHON ?= MCP_PROTOCOL_HOST ?= http://localhost:4444 MCP_BENCHMARK_HOST ?= http://localhost:8080 MCP_BENCHMARK_SERVER_ID ?= 9779b6698cbd4b4995ee04a4fab38737 @@ -2692,6 +2700,81 @@ benchmark-rate-limiter: ## Rate limiter correctness test (1 --only-summary \ RateLimitedUser || true' + +# help: benchmark-rate-limiter-scale - Multi-user scale test showing Redis memory divergence across algorithms +.PHONY: benchmark-rate-limiter-scale +RL_RUN_TIME ?= 300s +benchmark-rate-limiter-scale: ## Scale test: RL_USERS unique users (default 100), Redis memory timeline per algorithm + @echo "📈 Running rate limiter scale test (resource divergence)..." + @echo " Algorithm: $(RL_ALGORITHM) (must match plugins/config.yaml)" + @echo " Users: $(RL_USERS) unique identities (each creates own Redis key)" + @echo " Spawn: $(RL_SPAWN_RATE) users/s" + @echo " Limit: $(RL_LIMIT_PER_MIN) req/min per user" + @echo " Duration: $(RL_RUN_TIME) (includes ~40s bootstrap for user registration)" + @echo "" + @echo " Redis memory diverges between algorithms as users ramp up:" + @echo " fixed_window: ~0.1-0.3 KiB/key (single integer)" + @echo " sliding_window: ~1-3 KiB/key (sorted set, $(RL_LIMIT_PER_MIN) entries)" + @echo " token_bucket: ~0.2 KiB/key (hash: tokens + last_refill)" + @test -d "$(VENV_DIR)" || $(MAKE) venv + @/bin/bash -eu -o pipefail -c 'source $(VENV_DIR)/bin/activate && \ + LOCUST_LOG_LEVEL=ERROR \ + RL_ALGORITHM=$(RL_ALGORITHM) \ + RL_LIMIT_PER_MIN=$(RL_LIMIT_PER_MIN) \ + RL_USERS=$(RL_USERS) \ + RL_SPAWN_RATE=$(RL_SPAWN_RATE) \ + RL_RUN_TIME=$(RL_RUN_TIME) \ + MCP_SERVER_ID=$(MCP_BENCHMARK_SERVER_ID) \ + locust -f $(MCP_RATE_LIMITER_SCALE_LOCUSTFILE) \ + --host=$(MCP_BENCHMARK_HOST) \ + --users=$(RL_USERS) \ + --spawn-rate=$(RL_SPAWN_RATE) \ + --run-time=$(RL_RUN_TIME) \ + --headless \ + --only-summary \ + ScaleComparisonUser || true' + + +# help: benchmark-rate-limiter-redis-capacity - Multi-instance prompt-path concurrency benchmark for Redis rate limiting +.PHONY: benchmark-rate-limiter-redis-capacity +benchmark-rate-limiter-redis-capacity: ## Capacity test: 3 gateways + Redis on prompt_pre_fetch path + @echo "🚀 Running rate limiter Redis capacity test..." + @echo " Host: $(MCP_BENCHMARK_HOST)" + @echo " Topology: nginx -> 3 gateways -> shared Redis" + @echo " Path: REST /prompts/{id} (prompt_pre_fetch)" + @echo " Users: $(RL_USERS)" + @echo " Spawn rate: $(RL_SPAWN_RATE)/s" + @echo " Pace: $(RL_REQS_PER_SECOND) req/s per user" + @echo " Duration: $(RL_RUN_TIME)" + @test -d "$(VENV_DIR)" || $(MAKE) venv + @/bin/bash -eu -o pipefail -c 'source $(VENV_DIR)/bin/activate && \ + LOCUST_LOG_LEVEL=ERROR \ + RATE_LIMITER_FORCE_PYTHON=$(RATE_LIMITER_FORCE_PYTHON) \ + RL_USERS=$(RL_USERS) \ + RL_SPAWN_RATE=$(RL_SPAWN_RATE) \ + RL_RUN_TIME=$(RL_RUN_TIME) \ + RL_REQS_PER_SECOND=$(RL_REQS_PER_SECOND) \ + RL_LIMIT_PER_MIN=$(RL_LIMIT_PER_MIN) \ + RL_PROMPT_ID=$(RL_PROMPT_ID) \ + locust -f $(MCP_RATE_LIMITER_REDIS_CAPACITY_LOCUSTFILE) \ + --host=$(MCP_BENCHMARK_HOST) \ + --users=$(RL_USERS) \ + --spawn-rate=$(RL_SPAWN_RATE) \ + --run-time=$(RL_RUN_TIME) \ + --headless \ + --only-summary \ + CapacityPromptUser || true' + +# help: benchmark-rate-limiter-capacity-rust - Capacity test with Rust engine enabled (default) +.PHONY: benchmark-rate-limiter-capacity-rust +benchmark-rate-limiter-capacity-rust: ## Capacity test with Rust engine + RATE_LIMITER_FORCE_PYTHON=0 $(MAKE) benchmark-rate-limiter-redis-capacity + +# help: benchmark-rate-limiter-capacity-python - Capacity test with Python fallback (forced) +.PHONY: benchmark-rate-limiter-capacity-python +benchmark-rate-limiter-capacity-python: ## Capacity test with Python fallback + RATE_LIMITER_FORCE_PYTHON=1 $(MAKE) benchmark-rate-limiter-redis-capacity + .PHONY: benchmark-mcp-mixed-300 benchmark-mcp-mixed-300: ## Distributed 300-user mixed MCP benchmark @echo "📊 Running distributed mixed MCP benchmark..." diff --git a/docs/docs/using/plugins/plugins.md b/docs/docs/using/plugins/plugins.md index 25af6fd723..21ca30b39c 100644 --- a/docs/docs/using/plugins/plugins.md +++ b/docs/docs/using/plugins/plugins.md @@ -40,7 +40,7 @@ Plugins for improving system reliability, performance, and resource management. |--------|------|-------------| | [Circuit Breaker](https://github.com/IBM/mcp-context-forge/tree/main/plugins/circuit_breaker) | Native | Trips per-tool breaker on high error rates or consecutive failures and blocks during cooldown | | [Watchdog](https://github.com/IBM/mcp-context-forge/tree/main/plugins/watchdog) | Native | Enforces maximum runtime for tools with warn or block actions on threshold violations | -| [Rate Limiter](https://github.com/IBM/mcp-context-forge/tree/main/plugins/rate_limiter) | Native | Fixed-window in-memory rate limiting by user, tenant, or tool | +| [Rate Limiter](https://github.com/IBM/mcp-context-forge/tree/main/plugins/rate_limiter) | Native | Per-user, tenant, and tool rate limiting with selectable algorithms (fixed_window, sliding_window, token_bucket) and memory or Redis backends | | [Cached Tool Result](https://github.com/IBM/mcp-context-forge/tree/main/plugins/cached_tool_result) | Native | Caches idempotent tool results in-memory with configurable TTL and key fields | | [Response Cache by Prompt](https://github.com/IBM/mcp-context-forge/tree/main/plugins/response_cache_by_prompt) | Native | Advisory response cache using cosine similarity over prompt/input fields with configurable threshold | | [Retry with Backoff](https://github.com/IBM/mcp-context-forge/tree/main/plugins/retry_with_backoff) | Native | Annotates retry/backoff policy in metadata with exponential backoff on specific HTTP status codes | diff --git a/mcpgateway/auth.py b/mcpgateway/auth.py index 02bf0e8d27..a3d879855e 100644 --- a/mcpgateway/auth.py +++ b/mcpgateway/auth.py @@ -1188,11 +1188,12 @@ def _set_trace_for_user(user_obj: EmailUser, *, teams: Any = _UNSET, auth_method # Get plugin contexts from request state if available global_context = getattr(request.state, "plugin_global_context", None) if request else None if not global_context: - # Create global context + # Propagate team_id → tenant_id for by_tenant rate limiting + team_id = getattr(getattr(request, "state", None), "team_id", None) if request else None global_context = GlobalContext( request_id=request_id, server_id=None, - tenant_id=None, + tenant_id=team_id, ) context_table = getattr(request.state, "plugin_context_table", None) if request else None @@ -1249,6 +1250,7 @@ def _set_trace_for_user(user_obj: EmailUser, *, teams: Any = _UNSET, auth_method if plugin_manager and plugin_manager.config.plugin_settings.include_user_info: _inject_userinfo_instate(request, user) + _propagate_tenant_id(request) _set_trace_for_user(user) return user @@ -1378,6 +1380,7 @@ def _set_trace_for_user(user_obj: EmailUser, *, teams: Any = _UNSET, auth_method if plugin_manager and plugin_manager.config.plugin_settings.include_user_info: _inject_userinfo_instate(request, _user_from_cached_dict(cached_ctx.user)) + _propagate_tenant_id(request) cached_user = _user_from_cached_dict(cached_ctx.user) _set_trace_for_user( @@ -1516,6 +1519,7 @@ def _set_trace_for_user(user_obj: EmailUser, *, teams: Any = _UNSET, auth_method if plugin_manager and plugin_manager.config.plugin_settings.include_user_info: _inject_userinfo_instate(request, _batched_user) + _propagate_tenant_id(request) _set_trace_for_user( _batched_user, @@ -1697,12 +1701,35 @@ def _set_trace_for_user(user_obj: EmailUser, *, teams: Any = _UNSET, auth_method if plugin_manager and plugin_manager.config.plugin_settings.include_user_info: _inject_userinfo_instate(request, user) + _propagate_tenant_id(request) trace_teams = getattr(request.state, "token_teams", _UNSET) if request else _UNSET _set_trace_for_user(user, teams=trace_teams, team_name=getattr(request.state, "trace_team_name", None) if request else None) return user +def _propagate_tenant_id(request: Optional[object] = None) -> None: + """Propagate request.state.team_id into GlobalContext.tenant_id for rate limiting. + + Called unconditionally at every return path in get_current_user() — unlike + _inject_userinfo_instate() which is gated by include_user_info. This + ensures by_tenant rate limiting works even when include_user_info is False + (the default) and the middleware has already created plugin_global_context. + + Only writes when tenant_id is still None (no overwrite of plugin-set values). + + Args: + request: The incoming request object, or ``None`` if unavailable. + """ + if not request: + return + global_context = getattr(getattr(request, "state", None), "plugin_global_context", None) + if global_context and global_context.tenant_id is None: + team_id = getattr(getattr(request, "state", None), "team_id", None) + if team_id: + global_context.tenant_id = team_id + + def _inject_userinfo_instate(request: Optional[object] = None, user: Optional[EmailUser] = None) -> None: """This function injects user related information into the plugin_global_context, if the config has include_user_info key set as true. diff --git a/plugins/config.yaml b/plugins/config.yaml index 10e28f072d..a06d941138 100644 --- a/plugins/config.yaml +++ b/plugins/config.yaml @@ -212,7 +212,7 @@ plugins: author: "Mihai Criveti" hooks: ["prompt_pre_fetch", "tool_pre_invoke"] tags: ["limits", "throttle"] - mode: "permissive" + mode: "disabled" priority: 20 conditions: [] config: @@ -220,6 +220,7 @@ plugins: redis_url: "redis://redis:6379/0" redis_key_prefix: "rl" redis_fallback: true + algorithm: "fixed_window" by_user: "30/m" by_tenant: "3000/m" by_tool: {} diff --git a/plugins/rate_limiter/README.md b/plugins/rate_limiter/README.md index fb32e9edc4..b4d3ffb00a 100644 --- a/plugins/rate_limiter/README.md +++ b/plugins/rate_limiter/README.md @@ -3,7 +3,7 @@ > Author: Mihai Criveti > Version: 0.1.0 -Enforces fixed-window rate limits per user, tenant, and tool across `tool_pre_invoke` and `prompt_pre_fetch` hooks. Supports an in-process memory backend (single-instance) and a Redis backend (shared across all gateway instances). +Enforces rate limits per user, tenant, and tool across `tool_pre_invoke` and `prompt_pre_fetch` hooks. Supports pluggable counting algorithms (fixed window, sliding window, token bucket), an in-process memory backend (single-instance), and a Redis backend (shared across all gateway instances). ## Hooks @@ -32,6 +32,9 @@ If any configured dimension is exceeded, the plugin returns a violation with HTT search: "10/m" summarise: "5/m" + # Algorithm — choose one (default: fixed_window) + algorithm: "fixed_window" # fixed_window | sliding_window | token_bucket + # Backend — choose one backend: "memory" # default: single-process, resets on restart # backend: "redis" # shared across all gateway instances @@ -49,6 +52,7 @@ If any configured dimension is exceeded, the plugin returns a violation with HTT | `by_user` | string | `null` | Per-user rate limit, e.g. `"60/m"` | | `by_tenant` | string | `null` | Per-tenant rate limit, e.g. `"600/m"` | | `by_tool` | dict | `{}` | Per-tool overrides, e.g. `{"search": "10/m"}` | +| `algorithm` | string | `"fixed_window"` | Counting algorithm: `"fixed_window"`, `"sliding_window"`, or `"token_bucket"` | | `backend` | string | `"memory"` | `"memory"` or `"redis"` | | `redis_url` | string | `null` | Redis connection URL (required when `backend: redis`) | | `redis_key_prefix` | string | `"rl"` | Prefix for all Redis keys | @@ -69,24 +73,50 @@ Every request (allowed or blocked) includes: | `X-RateLimit-Reset` | Unix timestamp when the current window resets | | `Retry-After` | Seconds until the window resets (blocked requests only) | +## Algorithms + +Three counting algorithms are available, selected via the `algorithm` config field. + +| Algorithm | Config value | Best for | Trade-off | +|---|---|---|---| +| Fixed window | `fixed_window` | General use, lowest overhead | Up to 2× the limit at window boundaries | +| Sliding window | `sliding_window` | Smooth enforcement, no boundary burst | Higher memory: stores one timestamp per request per key | +| Token bucket | `token_bucket` | Bursty workloads — allows short spikes up to capacity | Slightly higher Redis overhead: stores `{tokens, last_refill}` hash per key | + +### Fixed window (default) + +Counts requests in a fixed time slot (e.g. "minute 14:03"). Resets at the slot boundary. Simple and fast. The 2× burst at a boundary (N requests at the end of slot T, N requests at the start of T+1) is a known trade-off; use `by_user` with headroom if this matters. + +### Sliding window + +Stores a timestamp for every request in the current window. At each check, expired timestamps are discarded and the remaining count is compared against the limit. Prevents boundary bursts entirely. Memory usage grows with request volume — roughly one float per request per active key. + +### Token bucket + +Each identity (user, tenant, tool) has a bucket that holds up to `count` tokens. Tokens refill at a steady rate of `count/window`. A request consumes one token. Bursts up to the bucket capacity are allowed; sustained rate above `count/window` is rejected. Useful for APIs where short spikes are acceptable but sustained overload is not. + +**Redis support:** `token_bucket` with `backend: redis` is fully supported. The plugin stores `{tokens, last_refill}` in a Redis hash per key and uses an atomic Lua script to refill and consume tokens in a single round-trip — the same pattern as the other two algorithms. This means `token_bucket` enforces a true cluster-wide limit in multi-instance deployments. + ## Backends -### Memory backend (default) +### Memory backend (default, single-instance only) - Counters are stored in a process-local dict (`_store`) - An `asyncio.Lock` serialises all counter reads and writes — safe under concurrent asyncio tasks -- A background sweep task evicts expired windows every 0.5s — memory is bounded to active windows only +- A background sweep task evicts expired windows every 0.5s — for `fixed_window` and `token_bucket`, expired entries are removed promptly; for `sliding_window`, keys with fully stale timestamps are evicted by the sweep - **Limitation:** state is not shared across processes or hosts. In a multi-instance deployment (e.g. 3 gateway instances behind nginx), each instance tracks its own counter — the effective limit is `N × configured_limit` ### Redis backend -- Counters are stored in Redis using an atomic Lua `INCR`+`EXPIRE` script — a single Redis call per check with no race condition +- `fixed_window`: atomic Lua `INCR`+`EXPIRE` — one Redis round-trip per check, no race condition +- `sliding_window`: atomic Lua `ZADD`+`ZREMRANGEBYSCORE`+`ZCARD`+`EXPIRE` — one round-trip, no race condition +- `token_bucket`: atomic Lua script — reads `{tokens, last_refill}` hash, refills proportionally, consumes 1 token, writes back — one round-trip, no race condition - All gateway instances share the same counter — the configured limit is the true cluster-wide limit - Requires `redis_url` to be set - If `redis_fallback: true` (default) and Redis is unavailable, the plugin falls back to the in-process `MemoryBackend` automatically — requests are never blocked due to Redis downtime - If `redis_fallback: false` and Redis is unavailable, the exception is caught and the request is allowed through (fail-open) -**Multi-instance deployment:** use `backend: redis`. The Redis service is already included in the default Docker Compose stack at `redis://redis:6379/0`. +**Multi-instance deployment (important):** The `memory` backend is local to a single gateway instance — rate limit counters are not shared across replicas. For multi-instance deployments (e.g., behind nginx or on OpenShift with multiple gateway pods), always use `backend: redis` to ensure rate limits are enforced correctly across all instances. The default production configuration (`plugins/config.yaml`) already sets `backend: redis`. ## Examples @@ -111,6 +141,34 @@ config: search: "10/m" ``` +### Sliding window (no boundary bursts) + +```yaml +config: + algorithm: "sliding_window" + by_user: "30/m" + by_tenant: "300/m" +``` + +### Token bucket — memory backend (default) + +```yaml +config: + algorithm: "token_bucket" + by_user: "30/m" # bucket holds 30 tokens, refills at 30/min +``` + +### Token bucket — Redis backend (multi-instance) + +```yaml +config: + algorithm: "token_bucket" + backend: "redis" + redis_url: "redis://redis:6379/0" + redis_fallback: true + by_user: "30/m" +``` + ### Permissive mode (observe without blocking) ```yaml @@ -126,7 +184,9 @@ In `permissive` mode the plugin records violations and emits `X-RateLimit-*` hea | Limitation | Severity | Status | |---|---|---| | Memory backend not shared across processes | HIGH | Use Redis backend for multi-instance deployments | -| Fixed window allows up to 2× limit at window boundary | LOW | Deferred — use `by_user` with headroom as a workaround | +| Fixed window allows up to 2× limit at window boundary | LOW | Use `sliding_window` algorithm, or use `by_user` with headroom | +| `by_tool` matching is case-sensitive | LOW | Fixed — tool names are now normalised with `.strip().lower()` at init | +| Whitespace-only user identity bypasses anonymous bucket | LOW | Documented gap; strip identities before passing to hooks | | No per-server limits (`server_id` dimension missing) | LOW | Not implemented | | No config hot-reload — rate string changes require restart | LOW | Not implemented | | Memory backend not safe under threaded workers (gunicorn `--threads`) | LOW | asyncio.Lock is loop-safe; use async workers (`-k uvicorn`) | diff --git a/plugins/rate_limiter/rate_limiter.py b/plugins/rate_limiter/rate_limiter.py index 90f69c5c0c..42a8e52658 100644 --- a/plugins/rate_limiter/rate_limiter.py +++ b/plugins/rate_limiter/rate_limiter.py @@ -5,8 +5,23 @@ Authors: Mihai Criveti Rate Limiter Plugin. -Enforces simple in-memory rate limits by user, tenant, and/or tool. -Uses a fixed window keyed by second for simplicity and determinism. +Enforces rate limits by user, tenant, and/or tool using a pluggable algorithm: + - fixed_window : simple counter per time bucket (default) + - sliding_window: rolling timestamp log, prevents burst at window boundary + - token_bucket : token refill model, allows short controlled bursts + +All three algorithms support both memory and Redis backends with identical +semantics. The Redis backend uses atomic Lua scripts for each algorithm — +one round-trip per check with no race conditions. + +Security contract — fail-open on error: + Both hook methods (prompt_pre_fetch, tool_pre_invoke) catch all unexpected + exceptions and allow the request through. This is a deliberate design + choice: an internal engine failure (Rust panic, Redis timeout, config bug) + must never block legitimate traffic. The trade-off is that a sustained + engine failure silently disables rate limiting until the error is resolved. + Operators should monitor for rate-limiter error logs and treat them as + high-priority alerts. """ # Future @@ -16,8 +31,12 @@ import asyncio from dataclasses import dataclass import logging +import math +import os +import threading import time -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional, Tuple +import uuid # Third-Party from pydantic import BaseModel, Field @@ -36,6 +55,116 @@ logger = logging.getLogger(__name__) +# --------------------------------------------------------------------------- +# Optional Rust engine — Python backend is the fallback when unavailable +# --------------------------------------------------------------------------- + +_RATE_LIMITER_FORCE_PYTHON = os.environ.get("RATE_LIMITER_FORCE_PYTHON", "").strip().lower() in ("1", "true", "yes") +_RateLimiterEngine: Any = None # Assigned below when the Rust extension is available. + +if _RATE_LIMITER_FORCE_PYTHON: + _RUST_AVAILABLE = False +else: + try: + # Third-Party + from rate_limiter_rust.rate_limiter_rust import RateLimiterEngine as _RateLimiterEngine # type: ignore[import] + + _RUST_AVAILABLE = True + except ImportError: + _RUST_AVAILABLE = False + + +class RustRateLimiterEngine: + """Thin Python wrapper around the PyO3 RateLimiterEngine. + + Exposes evaluate_many() / evaluate_many_async() as pure-Python methods so + tests can patch them with unittest.mock (PyO3 C extension methods are + read-only and cannot be patched directly). Pattern mirrors RustPIIDetector + in plugins/pii_filter/pii_filter_rust.py. + """ + + def __init__(self, config: dict) -> None: + """Initialise the Rust engine with the given config dict. + + Args: + config: Engine configuration dict with keys ``by_user``, ``by_tenant``, + ``by_tool``, ``algorithm``, ``backend``, and optionally ``redis_url`` + and ``redis_key_prefix``. + """ + self._engine = _RateLimiterEngine(config) + + def evaluate_many(self, checks: List[Tuple[str, int, int]], now_unix: int) -> Any: + """Delegate to the PyO3 engine (ARCH-01: single call per hook). + + Args: + checks: List of ``(key, limit_count, window_nanos)`` tuples. + now_unix: Current Unix timestamp in whole seconds. + + Returns: + An ``EvalResult`` with the most restrictive outcome across all dimensions. + """ + return self._engine.evaluate_many(checks, now_unix) + + async def evaluate_many_async(self, checks: List[Tuple[str, int, int]], now_unix: int) -> Any: + """Delegate to the PyO3 async engine for Redis-backed calls. + + Args: + checks: List of ``(key, limit_count, window_nanos)`` tuples. + now_unix: Current Unix timestamp in whole seconds. + + Returns: + An ``EvalResult`` with the most restrictive outcome across all dimensions. + """ + return await self._engine.evaluate_many_async(checks, now_unix) + + def check(self, user: str, tenant: Optional[str], tool: str, now_unix: int, include_retry_after: bool) -> Tuple[bool, dict, dict]: + """High-level check: returns (allowed, headers_dict, meta_dict). + + Builds dimension keys internally, evaluates, and returns pre-built + dicts — eliminates per-attribute PyO3 boundary crossings. + + Args: + user: Normalised user identity string. + tenant: Tenant identifier, or ``None`` to skip the tenant dimension. + tool: Lowercased tool or prompt name. + now_unix: Current Unix timestamp in whole seconds. + include_retry_after: Whether to include ``Retry-After`` in headers. + + Returns: + Tuple of ``(allowed, headers_dict, meta_dict)``. + """ + return self._engine.check(user, tenant, tool, now_unix, include_retry_after) + + async def check_async(self, user: str, tenant: Optional[str], tool: str, now_unix: int, include_retry_after: bool) -> Tuple[bool, dict, dict]: + """Async variant of check() for Redis-backed deployments. + + Args: + user: Normalised user identity string. + tenant: Tenant identifier, or ``None`` to skip the tenant dimension. + tool: Lowercased tool or prompt name. + now_unix: Current Unix timestamp in whole seconds. + include_retry_after: Whether to include ``Retry-After`` in headers. + + Returns: + Tuple of ``(allowed, headers_dict, meta_dict)``. + """ + return await self._engine.check_async(user, tenant, tool, now_unix, include_retry_after) + + +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- + +ALGORITHM_FIXED_WINDOW = "fixed_window" +ALGORITHM_SLIDING_WINDOW = "sliding_window" +ALGORITHM_TOKEN_BUCKET = "token_bucket" # nosec B105 +VALID_ALGORITHMS = (ALGORITHM_FIXED_WINDOW, ALGORITHM_SLIDING_WINDOW, ALGORITHM_TOKEN_BUCKET) + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + def _parse_rate(rate: str) -> tuple[int, int]: """Parse rate like '60/m', '10/s', '100/h' -> (count, window_seconds). @@ -47,10 +176,15 @@ def _parse_rate(rate: str) -> tuple[int, int]: Tuple of (count, window_seconds) for the rate limit. Raises: - ValueError: If the rate unit is not supported. + ValueError: If the rate string is malformed or the unit is not supported. """ - count_str, per = rate.split("/") - count = int(count_str) + try: + count_str, per = rate.split("/", maxsplit=1) + count = int(count_str) + except (ValueError, AttributeError): + raise ValueError(f"Invalid rate string {rate!r}: expected '/' e.g. '60/m'") + if count <= 0: + raise ValueError(f"Invalid rate string {rate!r}: count must be > 0, got {count}") per = per.strip().lower() if per in ("s", "sec", "second"): return count, 1 @@ -58,199 +192,796 @@ def _parse_rate(rate: str) -> tuple[int, int]: return count, 60 if per in ("h", "hr", "hour"): return count, 3600 - raise ValueError(f"Unsupported rate unit: {per}") + raise ValueError(f"Invalid rate string {rate!r}: unsupported unit {per!r}, expected s/m/h") -class RateLimiterConfig(BaseModel): - """Configuration for the rate limiter plugin. +def _make_headers(limit: int, remaining: int, reset_timestamp: int, retry_after: int, include_retry_after: bool = True) -> dict[str, str]: + """Create RFC-compliant rate limit headers. - Attributes: - by_user: Rate limit per user (e.g., '60/m'). - by_tenant: Rate limit per tenant (e.g., '600/m'). - by_tool: Per-tool rate limits (e.g., {'search': '10/m'}). - backend: Storage backend — 'memory' (default, single-process) or 'redis' (shared). - redis_url: Redis connection URL, required when backend='redis'. - redis_key_prefix: Prefix for all Redis keys (default 'rl'). - redis_fallback: Fall back to in-process memory if Redis is unavailable (default True). + Args: + limit: The rate limit count. + remaining: Number of requests remaining in the current window. + reset_timestamp: Unix timestamp when the window resets. + retry_after: Seconds until the window resets (for Retry-After header). + include_retry_after: Whether to include Retry-After header (only for violations). + + Returns: + Dictionary of HTTP headers for rate limiting. """ + headers = { + "X-RateLimit-Limit": str(limit), + "X-RateLimit-Remaining": str(remaining), + "X-RateLimit-Reset": str(reset_timestamp), + } + if include_retry_after: + headers["Retry-After"] = str(retry_after) + return headers - by_user: Optional[str] = Field(default=None, description="e.g. '60/m'") - by_tenant: Optional[str] = Field(default=None, description="e.g. '600/m'") - by_tool: Optional[Dict[str, str]] = Field(default=None, description="per-tool rates, e.g. {'search': '10/m'}") - backend: str = Field(default="memory", description="'memory' or 'redis'") - redis_url: Optional[str] = Field(default=None, description="Redis URL, e.g. 'redis://localhost:6379/0'") - redis_key_prefix: str = Field(default="rl", description="Prefix for Redis keys") - redis_fallback: bool = Field(default=True, description="Fall back to memory if Redis is unavailable") +def _extract_user_identity(user: Any) -> str: + """Return a stable, normalised string identity from a user context value. -@dataclass -class _Window: - """Internal rate limiting window tracking. + Handles three cases: + - dict (production JWT context): extract ``email`` → ``id`` → ``sub`` fallback + - string: strip whitespace; empty/whitespace-only falls back to 'anonymous' + - None / falsy: 'anonymous' - Attributes: - window_start: Timestamp when the current window started. - count: Number of requests in the current window. + Args: + user: Raw user context value from ``PluginContext.global_context.user``. + + Returns: + Normalised identity string with colons replaced by underscores. """ + if isinstance(user, dict): + identity = user.get("email") or user.get("id") or user.get("sub") or "" + identity = str(identity).strip() + elif user is None: + identity = "" + else: + identity = str(user).strip() + identity = identity if identity else "anonymous" + # Replace colons to prevent collision with namespace delimiters (user:/tenant:/tool:). + return identity.replace(":", "_") + + +def _select_most_restrictive(results: list[tuple[bool, int, int, dict[str, Any]]]) -> tuple[bool, int, int, int, dict[str, Any]]: + """Select the most restrictive rate limit from multiple dimensions. + + Multi-dimension aggregation contract: + - Any blocked dimension → overall result is blocked. + - Among blocked dimensions: the one with the **lowest** retry_after + (soonest unblock) determines the Retry-After header. This signals + the next state change — the caller learns when at least one dimension + will re-open, even if other dimensions remain blocked longer. An + alternative (max) would guarantee success on retry but delays the + first attempt and hides which dimension unblocked. This is a + deliberate product-level choice shared by both the Python and Rust + implementations. + - Among allowed dimensions: the one with the fewest remaining requests + determines the header values (closest to exhaustion). + + Args: + results: List of (allowed, limit, reset_timestamp, metadata) tuples. + + Returns: + Tuple of (allowed, limit, remaining, reset_timestamp, metadata). + """ + limited_results = [(allowed, limit, reset_ts, meta) for allowed, limit, reset_ts, meta in results if limit > 0] + + if not limited_results: + return True, 0, 0, 0, {"limited": False} + + violated = [(allowed, limit, reset_ts, meta) for allowed, limit, reset_ts, meta in limited_results if not allowed] + allowed_dims = [(allowed, limit, reset_ts, meta) for allowed, limit, reset_ts, meta in limited_results if allowed] + + if violated: + # Pick the violated dimension that will unblock soonest — its reset_in is the + # Retry-After value the client should use to know when to retry. + soonest_reset = min(violated, key=lambda x: x[3].get("reset_in", float("inf"))) + _, limit, reset_ts, meta = soonest_reset + remaining = meta.get("remaining", 0) + retry_after = meta.get("reset_in", 0) + aggregated_meta = { + "limited": True, + "remaining": remaining, + "reset_in": retry_after, + "dimensions": { + "violated": [m for _, _, _, m in violated], + "allowed": [m for _, _, _, m in allowed_dims], + }, + } + return False, limit, remaining, reset_ts, aggregated_meta + + # All dimensions are within limit — surface the tightest one (fewest remaining + # requests) so headers reflect the dimension the caller is closest to exhausting. + tightest = min(allowed_dims, key=lambda x: x[3].get("remaining", float("inf"))) + _, limit, reset_ts, meta = tightest + remaining = meta.get("remaining", 0) + retry_after = meta.get("reset_in", 0) + # "limited" means rate limits are *configured and evaluated*, not that + # the request was blocked. Matches the Rust engine (engine.rs build_meta_dict). + aggregated_meta = { + "limited": True, + "remaining": remaining, + "reset_in": retry_after, + "dimensions": {"allowed": [m for _, _, _, m in allowed_dims]}, + } + return True, limit, remaining, reset_ts, aggregated_meta + + +# --------------------------------------------------------------------------- +# Algorithm strategies — each owns its own store and counting logic +# --------------------------------------------------------------------------- + + +@dataclass +class _Window: + """Fixed window state: when the window started and how many requests so far.""" window_start: int count: int + window_seconds: int = 0 + + +@dataclass +class _Bucket: + """Token bucket state: current token count and when tokens were last refilled.""" + + tokens: float + last_refill: float + window: int = 3600 # window in seconds, used by sweep for eviction threshold + + +class FixedWindowAlgorithm: + """Fixed-window counter. + + Time is divided into fixed slots of `window_seconds`. A counter resets at + each slot boundary. Simple and cheap — O(1) memory per key — but allows + up to 2× the limit when requests straddle a window boundary. + """ + + def __init__(self) -> None: + """Initialise with an empty window store.""" + self._store: Dict[str, _Window] = {} + + async def allow(self, lock: asyncio.Lock, key: str, count: int, window: int) -> Tuple[bool, int, int, Dict[str, Any]]: + """Check and increment the fixed-window counter for *key*. + + Args: + lock: Async lock serialising access to the window store. + key: Rate-limit dimension key (e.g. ``"user:alice"``). + count: Maximum allowed requests per window. + window: Window duration in seconds. + + Returns: + Tuple of ``(allowed, limit, reset_timestamp, metadata)``. + """ + now = int(time.time()) + win_key = f"{key}:{window}" + + async with lock: + wnd = self._store.get(win_key) + + if not wnd or now - wnd.window_start >= window: + reset_timestamp = now + window + self._store[win_key] = _Window(window_start=now, count=1, window_seconds=window) + return True, count, reset_timestamp, {"limited": True, "remaining": count - 1, "reset_in": window} + + reset_timestamp = wnd.window_start + window + reset_in = window - (now - wnd.window_start) + + if wnd.count < count: + wnd.count += 1 + return True, count, reset_timestamp, {"limited": True, "remaining": count - wnd.count, "reset_in": reset_in} + + return False, count, reset_timestamp, {"limited": True, "remaining": 0, "reset_in": reset_in} + + async def sweep(self, lock: asyncio.Lock) -> None: + """Evict all fixed windows whose duration has elapsed. + + Args: + lock: Async lock serialising access to the window store. + """ + now = int(time.time()) + async with lock: + expired = [k for k, w in self._store.items() if now - w.window_start >= w.window_seconds] + for k in expired: + del self._store[k] + + +class SlidingWindowAlgorithm: + """Sliding-window log. + + Stores a list of request timestamps per key. On each request, timestamps + older than `window_seconds` are dropped and the remaining count is checked + against the limit. Prevents burst at window boundaries at the cost of + O(requests-in-window) memory per key. + """ + + def __init__(self) -> None: + """Initialise with an empty timestamp store.""" + self._store: Dict[str, Tuple[List[float], int]] = {} + + async def allow(self, lock: asyncio.Lock, key: str, count: int, window: int) -> Tuple[bool, int, int, Dict[str, Any]]: + """Check the sliding-window log for *key* and record the request if allowed. + + Args: + lock: Async lock serialising access to the timestamp store. + key: Rate-limit dimension key. + count: Maximum allowed requests per window. + window: Window duration in seconds. + + Returns: + Tuple of ``(allowed, limit, reset_timestamp, metadata)``. + """ + now = time.time() + cutoff = now - window + win_key = f"{key}:{window}" + + async with lock: + entry = self._store.get(win_key) + timestamps = entry[0] if entry else [] + # Drop timestamps outside the current window + timestamps = [t for t in timestamps if t > cutoff] + + current = len(timestamps) + reset_timestamp = int(timestamps[0] + window) if timestamps else int(now + window) + reset_in = max(0, int(reset_timestamp - now)) + + if current >= count: + self._store[win_key] = (timestamps, window) + # Ensure Retry-After is at least 1 so clients do not retry immediately + # when the oldest timestamp + window truncates to int(now). + return False, count, reset_timestamp, {"limited": True, "remaining": 0, "reset_in": max(1, reset_in)} + + timestamps.append(now) + self._store[win_key] = (timestamps, window) + remaining = count - len(timestamps) + return True, count, reset_timestamp, {"limited": True, "remaining": remaining, "reset_in": reset_in} + + async def sweep(self, lock: asyncio.Lock) -> None: + """Evict keys whose entire timestamp list is outside the current window. + + Args: + lock: Async lock serialising access to the timestamp store. + """ + now = time.time() + async with lock: + stale = [k for k, (ts, window) in self._store.items() if not ts or all(t <= now - window for t in ts)] + for k in stale: + del self._store[k] + + +class TokenBucketAlgorithm: + """Token bucket. + + Each key starts with `count` tokens. Tokens refill at a steady rate of + `count / window_seconds` per second. Each request consumes one token. + If no token is available the request is blocked. + + Allows short controlled bursts (up to `count` tokens at once) while + enforcing the average rate over time. O(1) memory per key. + """ + + def __init__(self) -> None: + """Initialise with an empty bucket store.""" + self._store: Dict[str, _Bucket] = {} + + async def allow(self, lock: asyncio.Lock, key: str, count: int, window: int) -> Tuple[bool, int, int, Dict[str, Any]]: + """Consume one token from *key*'s bucket, refilling proportionally to elapsed time. + + Args: + lock: Async lock serialising access to the bucket store. + key: Rate-limit dimension key. + count: Bucket capacity (max tokens). + window: Refill period in seconds (tokens refill at ``count / window`` per second). + + Returns: + Tuple of ``(allowed, limit, reset_timestamp, metadata)``. + """ + now = time.time() + refill_rate = count / window # tokens per second + + async with lock: + bucket = self._store.get(key) + + if bucket is None: + # First request — start with a full bucket minus this request. + # Use tokens_needed / refill_rate for time_to_full — consistent + # with the subsequent-request path and the Redis Lua script. + self._store[key] = _Bucket(tokens=count - 1, last_refill=now, window=window) + tokens_needed = 1 # consumed 1 from a full bucket + time_to_full = max(1, int(tokens_needed / refill_rate)) if tokens_needed > 0 else 0 + reset_timestamp = int(now + time_to_full) + return True, count, reset_timestamp, {"limited": True, "remaining": count - 1, "reset_in": time_to_full} + + # Refill tokens based on elapsed time + elapsed = now - bucket.last_refill + bucket.tokens = min(count, bucket.tokens + elapsed * refill_rate) + bucket.last_refill = now + + if bucket.tokens >= 1.0: + bucket.tokens -= 1.0 + remaining = int(bucket.tokens) + # Time until bucket would be full again. + # Use max(1, ...) so sub-second refill times round up to a future + # integer timestamp — mirrors the same guard in the Redis path. + tokens_needed = count - bucket.tokens + time_to_full = max(1, int(tokens_needed / refill_rate)) if tokens_needed > 0 else 0 + reset_timestamp = int(now + time_to_full) + return True, count, reset_timestamp, {"limited": True, "remaining": remaining, "reset_in": time_to_full} + + # No tokens — calculate when next token arrives (ceiling division + # matches the Redis Lua path which uses math.ceil). + time_to_next = max(1, math.ceil((1.0 - bucket.tokens) / refill_rate)) + reset_timestamp = int(now + time_to_next) + return False, count, reset_timestamp, {"limited": True, "remaining": 0, "reset_in": time_to_next} + + async def sweep(self, lock: asyncio.Lock) -> None: + """Evict buckets that are full (no active limiting). + + Args: + lock: Async lock serialising access to the bucket store. + """ + async with lock: + now = time.time() + full = [] + for k, bucket in self._store.items(): + elapsed = now - bucket.last_refill + if elapsed > max(3600, 2 * bucket.window): # inactive beyond window or 1h + full.append(k) + for k in full: + del self._store[k] + + +def _make_algorithm(name: str) -> FixedWindowAlgorithm | SlidingWindowAlgorithm | TokenBucketAlgorithm: + """Instantiate the named algorithm strategy. + + Args: + name: Algorithm name (``fixed_window``, ``sliding_window``, or ``token_bucket``). + + Returns: + Algorithm instance for the requested algorithm. + + Raises: + ValueError: If *name* is not a recognised algorithm. + """ + if name == ALGORITHM_FIXED_WINDOW: + return FixedWindowAlgorithm() + if name == ALGORITHM_SLIDING_WINDOW: + return SlidingWindowAlgorithm() + if name == ALGORITHM_TOKEN_BUCKET: + return TokenBucketAlgorithm() + raise ValueError(f"Unknown algorithm {name!r}: expected one of {VALID_ALGORITHMS}") + + +# --------------------------------------------------------------------------- +# Backends — own the lock, sweep scheduler, and external connection +# --------------------------------------------------------------------------- class MemoryBackend: - """Thread-safe in-process rate limit store with TTL eviction. + """In-process rate limit backend. - Uses an asyncio.Lock to make counter increments atomic within the event loop - and a periodic background sweep to remove expired windows and bound memory growth. + Owns the asyncio.Lock and background sweep scheduler. Delegates all + counting logic to the injected Algorithm strategy. Attributes: - _store: Mapping of win_key -> _Window tracking active rate limit windows. - _lock: asyncio.Lock serialising reads and writes to _store. + _algorithm: The counting strategy (fixed_window, sliding_window, token_bucket). + _lock: asyncio.Lock serialising reads and writes to the algorithm's store. _sweep_interval: Seconds between background eviction sweeps. - _sweep_task: Running asyncio.Task for the background sweep loop (or None). + _sweep_task: Running asyncio.Task for the background sweep loop. """ - def __init__(self, sweep_interval: float = 0.5) -> None: - """Initialise the backend. + def __init__(self, algorithm: FixedWindowAlgorithm | SlidingWindowAlgorithm | TokenBucketAlgorithm, sweep_interval: float = 0.5) -> None: + """Initialise the backend with the given algorithm and sweep interval. Args: - sweep_interval: How often (in seconds) the background sweep removes expired windows. + algorithm: Counting strategy instance. + sweep_interval: Seconds between background eviction sweeps. """ - self._store: Dict[str, _Window] = {} - self._lock = asyncio.Lock() + self._algorithm = algorithm + self._lock: Optional[asyncio.Lock] = None self._sweep_interval = sweep_interval self._sweep_task: Optional[asyncio.Task] = None # type: ignore[type-arg] + self._parsed_cache: Dict[str, tuple[int, int]] = {} # rate_str → (count, window) + + def _ensure_lock(self) -> asyncio.Lock: + """Lazily create the asyncio.Lock on first use within a running event loop. + + This avoids binding the lock to the wrong loop on Python 3.11 when the + plugin is instantiated outside an async context. + + Returns: + The shared asyncio.Lock instance for this backend. + """ + if self._lock is None: + self._lock = asyncio.Lock() + return self._lock def _ensure_sweep_task(self) -> None: - """Start the background sweep task if no running task exists.""" + """Start the background sweep task if it is not already running.""" if self._sweep_task is None or self._sweep_task.done(): try: loop = asyncio.get_running_loop() self._sweep_task = loop.create_task(self._sweep_loop()) except RuntimeError: - pass # No running event loop yet (e.g. at module import time) + logger.warning("MemoryBackend: no running event loop; sweep task not started — expired entries will not be evicted") async def _sweep_loop(self) -> None: - """Periodically evict expired window entries.""" + """Periodically invoke the algorithm's sweep to evict expired entries.""" while True: await asyncio.sleep(self._sweep_interval) - await self._sweep() - - async def _sweep(self) -> None: - """Remove all entries whose fixed window has expired.""" - now = int(time.time()) - async with self._lock: - expired = [ - k for k, w in self._store.items() - if now - w.window_start >= int(k.rsplit(":", 1)[-1]) - ] - for k in expired: - del self._store[k] + await self._algorithm.sweep(self._ensure_lock()) async def allow(self, key: str, limit: Optional[str]) -> tuple[bool, int, int, dict[str, Any]]: - """Check and increment the rate limit counter for key. + """Check the rate limit for *key* against *limit* using the in-process algorithm. Args: - key: Composite key identifying the dimension (e.g. 'user:alice', 'tool:search'). - limit: Rate string (e.g. '60/m') or None for unlimited. + key: Rate-limit dimension key (e.g. ``"user:alice"``). + limit: Rate string (e.g. ``"60/m"``), or ``None`` to skip. Returns: - Tuple of (allowed, limit_count, reset_timestamp, metadata). + Tuple of ``(allowed, limit_count, reset_timestamp, metadata)``. """ self._ensure_sweep_task() - if not limit: return True, 0, 0, {"limited": False} + parsed = self._parsed_cache.get(limit) + if parsed is None: + parsed = _parse_rate(limit) + self._parsed_cache[limit] = parsed + count, window = parsed + return await self._algorithm.allow(self._ensure_lock(), key, count, window) - count, window_seconds = _parse_rate(limit) - now = int(time.time()) - win_key = f"{key}:{window_seconds}" - - async with self._lock: - wnd = self._store.get(win_key) - - if not wnd or now - wnd.window_start >= window_seconds: - # New window - reset_timestamp = now + window_seconds - self._store[win_key] = _Window(window_start=now, count=1) - return True, count, reset_timestamp, {"limited": True, "remaining": count - 1, "reset_in": window_seconds} - reset_timestamp = wnd.window_start + window_seconds - if wnd.count < count: - # Within limit - wnd.count += 1 - reset_in = window_seconds - (now - wnd.window_start) - return True, count, reset_timestamp, {"limited": True, "remaining": count - wnd.count, "reset_in": reset_in} +class RedisBackend: + """Shared rate limit backend backed by Redis. - # Exceeded - reset_in = window_seconds - (now - wnd.window_start) - return False, count, reset_timestamp, {"limited": True, "remaining": 0, "reset_in": reset_in} + Supports all three algorithms via atomic Lua scripts — one round-trip per + check with no race conditions. + .. important:: **Dual Lua-script invariant (rolling-upgrade compatibility)** -class RedisBackend: - """Shared rate limit store backed by Redis. + The Rust engine (``plugins_rust/rate_limiter/src/redis_backend.rs``) + contains its own copies of the batch Lua scripts and uses the same + Redis key format (``{prefix}:{dimension_key}:{window_seconds}``). + Both implementations **must** produce identical keys and compatible + counter semantics so that gateway instances running the Rust backend + and instances still on the Python fallback share the same Redis + counters during a rolling upgrade. - Uses an atomic Lua script (INCR + EXPIRE) so counter increments are race-free - even across multiple gateway processes or threads. Native Redis key TTLs replace - the in-process sweep, so memory is bounded automatically. + If you change a Lua script or the key format here, you **must** make + the corresponding change in the Rust backend (and vice-versa), and + validate with the ``test_redis_key_format_parity_*`` tests. Attributes: _url: Redis connection URL. - _prefix: Key namespace prefix (e.g. 'rl'). + _prefix: Key namespace prefix. + _algorithm_name: Which algorithm to use. _fallback: Optional MemoryBackend used when Redis is unavailable. - _client: Injected client (non-None overrides lazy init — used in tests). - _real_client: Lazily initialised production redis.asyncio client. """ - # Lua script: atomically increment the counter and set TTL on first call. - # Returns [current_count, ttl_remaining_seconds]. - _LUA = """ + # Fixed window: atomic INCR + EXPIRE. Returns [count, ttl]. + _LUA_FIXED = """ local current = redis.call('INCR', KEYS[1]) if current == 1 then redis.call('EXPIRE', KEYS[1], ARGV[1]) end local ttl = redis.call('TTL', KEYS[1]) return {current, ttl} +""" + + # Sliding window: remove expired entries, check count, ZADD only if allowed. + # ARGV: [now_float, window_seconds, limit_int, unique_member] + # Returns [allowed_int, current_count, oldest_timestamp_or_0]. + # Fix: check count before ZADD (blocked requests must not inflate the set). + # Fix: use a unique member (ARGV[4]) so simultaneous requests with identical + # timestamps do not collapse into a single sorted-set entry. + _LUA_SLIDING = """ +local now = tonumber(ARGV[1]) +local window = tonumber(ARGV[2]) +local limit = tonumber(ARGV[3]) +local member = ARGV[4] +local cutoff = now - window +redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', cutoff) +local count = tonumber(redis.call('ZCARD', KEYS[1])) +redis.call('EXPIRE', KEYS[1], window + 1) +local oldest = redis.call('ZRANGE', KEYS[1], 0, 0, 'WITHSCORES') +local oldest_ts = 0 +if #oldest > 0 then oldest_ts = tonumber(oldest[2]) end +if count >= limit then + return {0, count, oldest_ts} +end +redis.call('ZADD', KEYS[1], now, member) +count = count + 1 +oldest = redis.call('ZRANGE', KEYS[1], 0, 0, 'WITHSCORES') +oldest_ts = 0 +if #oldest > 0 then oldest_ts = tonumber(oldest[2]) end +return {1, count, oldest_ts} +""" + + # Token bucket: HMGET {tokens, last_refill}, refill proportionally, consume 1. + # ARGV: [capacity, refill_rate_per_sec, now_as_float] + # Returns [allowed_int, remaining_floor, time_to_next_token_seconds]. + # NOTE: Lua uses floating-point arithmetic for token refill (tokens + elapsed * rate), + # while the in-memory Rust backend uses integer milli-token math (u128). Under sustained + # high-frequency traffic the two may diverge by ±1 token due to float precision loss. + # This is acceptable for rate limiting — the behavioral contract is identical. + _LUA_TOKEN_BUCKET = """ +local data = redis.call('HMGET', KEYS[1], 'tokens', 'last_refill') +local capacity = tonumber(ARGV[1]) +local rate = tonumber(ARGV[2]) +local now = tonumber(ARGV[3]) + +local tokens = tonumber(data[1]) +local last_refill = tonumber(data[2]) + +if tokens == nil then + tokens = capacity - 1 + redis.call('HSET', KEYS[1], 'tokens', tokens, 'last_refill', now) + local ttl = math.ceil(capacity / rate) + 1 + redis.call('EXPIRE', KEYS[1], ttl) + return {1, math.floor(tokens), 0} +end + +local elapsed = now - last_refill +tokens = math.min(capacity, tokens + elapsed * rate) + +local allowed +local time_to_next = 0 +if tokens >= 1.0 then + tokens = tokens - 1.0 + allowed = 1 +else + allowed = 0 + time_to_next = math.ceil((1.0 - tokens) / rate) +end + +redis.call('HSET', KEYS[1], 'tokens', tokens, 'last_refill', now) +local ttl = math.ceil((capacity - tokens) / rate) + 1 +redis.call('EXPIRE', KEYS[1], ttl) + +return {allowed, math.floor(tokens), time_to_next} +""" + + # LIMITATION: Batch scripts pass multiple KEYS (one per dimension) in a + # single EVAL/EVALSHA call. In Redis Cluster, all keys in a single script + # must hash to the same slot. The key format `{prefix}:{dim}:{window}` + # does NOT use hash tags, so these scripts will fail on Redis Cluster. + # Use standalone Redis or Sentinel for multi-dimension batch evaluation. + + # Batch fixed window: N keys, N windows in ARGV. + # KEYS: [key1..keyN] ARGV: [window1..windowN] + # Returns: [[count1,ttl1], ..., [countN,ttlN]] + _LUA_BATCH_FIXED = """ +local results = {} +for i = 1, #KEYS do + local current = redis.call('INCR', KEYS[i]) + if current == 1 then + redis.call('EXPIRE', KEYS[i], ARGV[i]) + end + local ttl = redis.call('TTL', KEYS[i]) + results[i] = {current, ttl} +end +return results +""" + + # Batch sliding window: N keys. + # KEYS: [key1..keyN] ARGV: [now, window1, limit1, member1, window2, limit2, member2, ...] + # Returns: [[allowed,count,oldest_ts], ...] + _LUA_BATCH_SLIDING = """ +local now = tonumber(ARGV[1]) +local results = {} +for i = 1, #KEYS do + local base = 1 + (i-1)*3 + 1 + local window = tonumber(ARGV[base]) + local limit = tonumber(ARGV[base+1]) + local member = ARGV[base+2] + local cutoff = now - window + redis.call('ZREMRANGEBYSCORE', KEYS[i], '-inf', cutoff) + local count = tonumber(redis.call('ZCARD', KEYS[i])) + redis.call('EXPIRE', KEYS[i], window + 1) + if count >= limit then + local oldest = redis.call('ZRANGE', KEYS[i], 0, 0, 'WITHSCORES') + local oldest_ts = 0 + if #oldest > 0 then oldest_ts = tonumber(oldest[2]) end + results[i] = {0, count, oldest_ts} + else + redis.call('ZADD', KEYS[i], now, member) + count = count + 1 + local oldest = redis.call('ZRANGE', KEYS[i], 0, 0, 'WITHSCORES') + local oldest_ts = 0 + if #oldest > 0 then oldest_ts = tonumber(oldest[2]) end + results[i] = {1, count, oldest_ts} + end +end +return results +""" + + # Batch token bucket: N keys. + # KEYS: [key1..keyN] ARGV: [now, capacity1, rate1, capacity2, rate2, ...] + # Returns: [[allowed,remaining,time_to_next], ...] + _LUA_BATCH_TOKEN_BUCKET = """ +local now = tonumber(ARGV[1]) +local results = {} +for i = 1, #KEYS do + local base = 1 + (i-1)*2 + 1 + local capacity = tonumber(ARGV[base]) + local rate = tonumber(ARGV[base+1]) + local data = redis.call('HMGET', KEYS[i], 'tokens', 'last_refill') + local tokens = tonumber(data[1]) + local last_refill = tonumber(data[2]) + if tokens == nil then + tokens = capacity - 1 + redis.call('HSET', KEYS[i], 'tokens', tokens, 'last_refill', now) + local ttl = math.ceil(capacity / rate) + 1 + redis.call('EXPIRE', KEYS[i], ttl) + results[i] = {1, math.floor(tokens), 0} + else + local elapsed = now - last_refill + tokens = math.min(capacity, tokens + elapsed * rate) + local allowed, time_to_next + if tokens >= 1.0 then + tokens = tokens - 1.0 + allowed = 1 + time_to_next = 0 + else + allowed = 0 + time_to_next = math.ceil((1.0 - tokens) / rate) + end + redis.call('HSET', KEYS[i], 'tokens', tokens, 'last_refill', now) + local ttl = math.ceil((capacity - tokens) / rate) + 1 + redis.call('EXPIRE', KEYS[i], ttl) + results[i] = {allowed, math.floor(tokens), time_to_next} + end +end +return results """ def __init__( self, redis_url: str, key_prefix: str = "rl", + algorithm_name: str = ALGORITHM_FIXED_WINDOW, fallback: Optional[MemoryBackend] = None, _client: Any = None, ) -> None: - """Initialise the Redis backend. + """Initialise the Redis backend with connection URL, key prefix, algorithm, and optional fallback. Args: - redis_url: Redis connection URL (e.g. 'redis://localhost:6379/0'). + redis_url: Redis connection URL (e.g. ``"redis://localhost:6379/0"``). key_prefix: Namespace prefix for all Redis keys. - fallback: MemoryBackend to use when Redis is unreachable and redis_fallback=True. - _client: Pre-built client for testing; skips lazy initialisation when set. + algorithm_name: Counting algorithm name (``fixed_window``, ``sliding_window``, or ``token_bucket``). + fallback: Optional in-memory backend used when Redis is unavailable. + _client: Injected Redis client for testing; ``None`` for production. """ self._url = redis_url self._prefix = key_prefix + self._algorithm_name = algorithm_name self._fallback = fallback self._client = _client self._real_client: Any = None + # REDIS-02: SHA cache for EVALSHA — loaded once at first use, never on request path. + self._sha_fixed: Optional[str] = None + self._sha_sliding: Optional[str] = None + self._sha_token_bucket: Optional[str] = None + self._sha_batch_fixed: Optional[str] = None + self._sha_batch_sliding: Optional[str] = None + self._sha_batch_token_bucket: Optional[str] = None + self._scripts_loaded: bool = False + self._script_load_lock: Optional[asyncio.Lock] = None async def _get_client(self) -> Any: - """Return the Redis client, lazily initialising the real one if needed.""" + """Return the Redis client, lazily initialising a real connection if needed. + + Returns: + An async Redis client instance. + """ if self._client is not None: return self._client if self._real_client is None: + # Third-Party import redis.asyncio as aioredis # noqa: PLC0415 - self._real_client = aioredis.from_url(self._url, decode_responses=False) + + self._real_client = aioredis.from_url(self._url, decode_responses=True, max_connections=50, socket_timeout=5, socket_connect_timeout=5) return self._real_client + async def _ensure_scripts_loaded(self, client: Any) -> None: + """REDIS-02: Load all Lua scripts once via SCRIPT LOAD and cache their SHAs. + + Subsequent calls are no-ops once all SHAs are cached. EVALSHA is then used on + every request path instead of EVAL — O(1) SHA lookup vs. re-parsing the script. + Only caches the result when `script_load` returns a real string SHA (guards + against test mock clients that return Mock objects). + + Uses an asyncio.Lock to serialise the one-time loading and prevent + duplicate SCRIPT LOAD round-trips under concurrent coroutines. + + Args: + client: Async Redis client instance. + """ + if self._scripts_loaded: + return + if self._script_load_lock is None: + self._script_load_lock = asyncio.Lock() + async with self._script_load_lock: + if self._scripts_loaded: + return + pairs = ( + ("_sha_fixed", self._LUA_FIXED), + ("_sha_sliding", self._LUA_SLIDING), + ("_sha_token_bucket", self._LUA_TOKEN_BUCKET), + ("_sha_batch_fixed", self._LUA_BATCH_FIXED), + ("_sha_batch_sliding", self._LUA_BATCH_SLIDING), + ("_sha_batch_token_bucket", self._LUA_BATCH_TOKEN_BUCKET), + ) + for attr, script in pairs: + if getattr(self, attr) is None: + result = await client.script_load(script) + if isinstance(result, str): + setattr(self, attr, result) + self._scripts_loaded = True + + async def _evalsha(self, client: Any, sha: Optional[str], script: str, numkeys: int, *args: Any) -> Any: + """REDIS-02: Execute via EVALSHA when SHA is cached; fall back to EVAL otherwise. + + Falls back to EVAL when: + - sha is None (script not yet loaded — first call before Redis responds, or test mock) + - NOSCRIPT error (Redis restarted and flushed its script cache) + After a NOSCRIPT fallback, reloads the SHA so the next call uses EVALSHA again. + + Args: + client: Async Redis client instance. + sha: Cached script SHA, or ``None`` if not yet loaded. + script: Full Lua script text (used as EVAL fallback). + numkeys: Number of Redis keys passed to the script. + *args: Positional arguments passed as KEYS and ARGV to the script. + + Returns: + Raw result from the Redis EVALSHA or EVAL call. + + Raises: + Exception: Re-raised from Redis if the error is not a NOSCRIPT error. + """ + if sha is None: + return await client.eval(script, numkeys, *args) + try: + return await client.evalsha(sha, numkeys, *args) + except Exception as exc: + if "NOSCRIPT" in str(exc): + logger.warning("EVALSHA cache miss (NOSCRIPT); falling back to EVAL and reloading SHA") + # Allow _ensure_scripts_loaded to bulk-reload all SHAs next request. + self._scripts_loaded = False + result = await client.eval(script, numkeys, *args) + try: + new_sha = await client.script_load(script) + if isinstance(new_sha, str): + for attr, s in ( + ("_sha_fixed", self._LUA_FIXED), + ("_sha_sliding", self._LUA_SLIDING), + ("_sha_token_bucket", self._LUA_TOKEN_BUCKET), + ("_sha_batch_fixed", self._LUA_BATCH_FIXED), + ("_sha_batch_sliding", self._LUA_BATCH_SLIDING), + ("_sha_batch_token_bucket", self._LUA_BATCH_TOKEN_BUCKET), + ): + if s.strip() == script.strip(): + setattr(self, attr, new_sha) + break + except Exception: + logger.warning("EVALSHA SHA reload failed; subsequent calls will fall back to EVAL", exc_info=True) + return result + raise + async def allow(self, key: str, limit: Optional[str]) -> tuple[bool, int, int, dict[str, Any]]: - """Check and increment the rate limit counter in Redis. + """Check the rate limit for *key* against *limit* using an atomic Redis Lua script. Args: - key: Composite key identifying the dimension (e.g. 'user:alice'). - limit: Rate string (e.g. '60/m') or None for unlimited. + key: Rate-limit dimension key (e.g. ``"user:alice"``). + limit: Rate string (e.g. ``"60/m"``), or ``None`` to skip. Returns: - Tuple of (allowed, limit_count, reset_timestamp, metadata). + Tuple of ``(allowed, limit_count, reset_timestamp, metadata)``. """ if not limit: return True, 0, 0, {"limited": False} @@ -260,284 +991,679 @@ async def allow(self, key: str, limit: Optional[str]) -> tuple[bool, int, int, d try: client = await self._get_client() - result = await client.eval(self._LUA, 1, redis_key, window_seconds) - current_count = int(result[0]) - ttl = int(result[1]) - now = int(time.time()) - reset_timestamp = now + max(ttl, 0) - reset_in = max(ttl, 0) - remaining = max(0, count - current_count) + await self._ensure_scripts_loaded(client) - if current_count > count: - return False, count, reset_timestamp, {"limited": True, "remaining": 0, "reset_in": reset_in} - - return True, count, reset_timestamp, {"limited": True, "remaining": remaining, "reset_in": reset_in} + if self._algorithm_name == ALGORITHM_SLIDING_WINDOW: + return await self._allow_sliding(client, redis_key, count, window_seconds) + if self._algorithm_name == ALGORITHM_TOKEN_BUCKET: + return await self._allow_token_bucket(client, redis_key, count, window_seconds) + return await self._allow_fixed(client, redis_key, count, window_seconds) except Exception: logger.exception("RedisBackend.allow failed; %s", "falling back to memory" if self._fallback else "allowing request") if self._fallback is not None: return await self._fallback.allow(key, limit) - return True, 0, 0, {"limited": False} + return True, 0, 0, {"limited": False, "error": True} + async def _allow_fixed(self, client: Any, redis_key: str, count: int, window_seconds: int) -> tuple[bool, int, int, dict[str, Any]]: + """Run the fixed-window Lua script and return the allow/block decision. -# Module-level backend instance shared across all plugin instances on this process. -_backend = MemoryBackend() + Args: + client: Async Redis client instance. + redis_key: Fully-qualified Redis key for this dimension. + count: Maximum allowed requests per window. + window_seconds: Window duration in seconds. -# Expose _store at module level so tests can inspect and clear it directly. -_store: Dict[str, _Window] = _backend._store + Returns: + Tuple of ``(allowed, limit, reset_timestamp, metadata)``. + """ + result = await self._evalsha(client, self._sha_fixed, self._LUA_FIXED, 1, redis_key, window_seconds) + current_count = int(result[0]) + ttl = int(result[1]) + now = int(time.time()) + reset_timestamp = now + max(ttl, 0) + reset_in = max(ttl, 0) + remaining = max(0, count - current_count) + if current_count > count: + return False, count, reset_timestamp, {"limited": True, "remaining": 0, "reset_in": reset_in} + return True, count, reset_timestamp, {"limited": True, "remaining": remaining, "reset_in": reset_in} -def _make_headers(limit: int, remaining: int, reset_timestamp: int, retry_after: int, include_retry_after: bool = True) -> dict[str, str]: - """Create RFC-compliant rate limit headers. + async def _allow_sliding(self, client: Any, redis_key: str, count: int, window_seconds: int) -> tuple[bool, int, int, dict[str, Any]]: + """Run the sliding-window Lua script and return the allow/block decision. - Args: - limit: The rate limit count. - remaining: Number of requests remaining in the current window. - reset_timestamp: Unix timestamp when the window resets. - retry_after: Seconds until the window resets (for Retry-After header). - include_retry_after: Whether to include Retry-After header (only for violations). + Args: + client: Async Redis client instance. + redis_key: Fully-qualified Redis key for this dimension. + count: Maximum allowed requests per window. + window_seconds: Window duration in seconds. - Returns: - Dictionary of HTTP headers for rate limiting. - """ - headers = { - "X-RateLimit-Limit": str(limit), - "X-RateLimit-Remaining": str(remaining), - "X-RateLimit-Reset": str(reset_timestamp), - } - if include_retry_after: - headers["Retry-After"] = str(retry_after) - return headers + Returns: + Tuple of ``(allowed, limit, reset_timestamp, metadata)``. + """ + now = time.time() + unique_member = f"{now}:{uuid.uuid4().hex}" + result = await self._evalsha(client, self._sha_sliding, self._LUA_SLIDING, 1, redis_key, now, window_seconds, count, unique_member) + allowed_int = int(result[0]) + current_count = int(result[1]) + oldest_ts = float(result[2]) if result[2] else now + reset_timestamp = int(oldest_ts + window_seconds) + reset_in = max(0, int(reset_timestamp - now)) + remaining = max(0, count - current_count) + + if not allowed_int: + return False, count, reset_timestamp, {"limited": True, "remaining": 0, "reset_in": max(1, reset_in)} + return True, count, reset_timestamp, {"limited": True, "remaining": remaining, "reset_in": reset_in} + + async def _allow_token_bucket(self, client: Any, redis_key: str, count: int, window_seconds: int) -> tuple[bool, int, int, dict[str, Any]]: + """Run the token-bucket Lua script and return the allow/block decision. + Args: + client: Async Redis client instance. + redis_key: Fully-qualified Redis key for this dimension. + count: Bucket capacity (max tokens). + window_seconds: Refill period in seconds. -def _select_most_restrictive( - results: list[tuple[bool, int, int, dict[str, Any]]] -) -> tuple[bool, int, int, int, dict[str, Any]]: - """Select the most restrictive rate limit from multiple dimensions. + Returns: + Tuple of ``(allowed, limit, reset_timestamp, metadata)``. + """ + now = time.time() + refill_rate = count / window_seconds # tokens per second + result = await self._evalsha(client, self._sha_token_bucket, self._LUA_TOKEN_BUCKET, 1, redis_key, count, refill_rate, now) + allowed_int = int(result[0]) + remaining = int(result[1]) + time_to_next = int(result[2]) + + if not allowed_int: + reset_timestamp = int(now + time_to_next) + return False, count, reset_timestamp, {"limited": True, "remaining": 0, "reset_in": time_to_next} + + # Compute time-to-full consistent with the memory backend: tokens_needed / refill_rate. + # Use max(1, ...) so sub-second refill times round up to a future integer timestamp. + tokens_needed = count - remaining + time_to_full = max(1, int(tokens_needed / refill_rate)) if tokens_needed > 0 else 0 + reset_timestamp = int(now + time_to_full) + return True, count, reset_timestamp, {"limited": True, "remaining": remaining, "reset_in": time_to_full} + + async def allow_many(self, checks: List[Tuple[str, str]]) -> List[tuple[bool, int, int, dict[str, Any]]]: + """Batch all dimension checks into a single Redis eval call (REDIS-01, REDIS-03). - Args: - results: List of (allowed, limit, reset_timestamp, metadata) tuples from _allow(). - - allowed: True if the request is allowed - - limit_count: The rate limit count (0 if unlimited) - - reset_timestamp: Unix timestamp when the window resets (0 if unlimited) - - metadata: Additional rate limiting information + Args: + checks: List of (dimension_key, rate_str) pairs, e.g. [("user:alice", "10/s")]. - Returns: - Tuple of (allowed, limit, remaining, reset_timestamp, metadata) representing - the most restrictive limit. If any dimension is violated, allowed is False. - The metadata includes aggregated information from all dimensions. - """ - # Filter out unlimited results (limit == 0) - limited_results = [(allowed, limit, reset_ts, meta) for allowed, limit, reset_ts, meta in results if limit > 0] + Returns: + One (allowed, limit, reset_timestamp, metadata) tuple per input check. + """ + no_limit: tuple[bool, int, int, dict[str, Any]] = (True, 0, 0, {"limited": False}) + active_indices = [i for i, (_, limit) in enumerate(checks) if limit] + if not active_indices: + return [no_limit] * len(checks) - if not limited_results: - # All unlimited - return True, 0, 0, 0, {"limited": False} + active = [checks[i] for i in active_indices] + parsed: List[Tuple[str, int, int]] = [(key, *_parse_rate(limit)) for key, limit in active] # type: ignore[misc] + redis_keys = [f"{self._prefix}:{key}:{window}" for key, _count, window in parsed] - # Separate violated and allowed dimensions - violated = [(allowed, limit, reset_ts, meta) for allowed, limit, reset_ts, meta in limited_results if not allowed] - allowed_dims = [(allowed, limit, reset_ts, meta) for allowed, limit, reset_ts, meta in limited_results if allowed] + try: + client = await self._get_client() + await self._ensure_scripts_loaded(client) + if self._algorithm_name == ALGORITHM_SLIDING_WINDOW: + active_results = await self._allow_many_sliding(client, parsed, redis_keys) + elif self._algorithm_name == ALGORITHM_TOKEN_BUCKET: + active_results = await self._allow_many_token_bucket(client, parsed, redis_keys) + else: + active_results = await self._allow_many_fixed(client, parsed, redis_keys) - # If any dimension is violated, pick the one with shortest retry_after (resets soonest) - if violated: - most_restrictive = min(violated, key=lambda x: x[3].get("reset_in", float("inf"))) - _, limit, reset_ts, meta = most_restrictive - remaining = meta.get("remaining", 0) - retry_after = meta.get("reset_in", 0) + except Exception: + logger.exception("RedisBackend.allow_many failed; %s", "falling back to memory" if self._fallback else "allowing request") + if self._fallback is not None: + active_results = [await self._fallback.allow(key, limit) for key, limit in active] + else: + no_limit_error: tuple[bool, int, int, dict[str, Any]] = (True, 0, 0, {"limited": False, "error": True}) + active_results = [no_limit_error] * len(active) - # Aggregate metadata from all dimensions for observability - aggregated_meta = { - "limited": True, - "remaining": remaining, - "reset_in": retry_after, - "dimensions": { - "violated": [m for _, _, _, m in violated], - "allowed": [m for _, _, _, m in allowed_dims], - } - } - return False, limit, remaining, reset_ts, aggregated_meta + # Map active results back to the full input list. + results: List[tuple[bool, int, int, dict[str, Any]]] = [no_limit] * len(checks) + for idx, result in zip(active_indices, active_results): + results[idx] = result + return results - # All dimensions allowed - find the most restrictive (lowest remaining) - most_restrictive = min(allowed_dims, key=lambda x: x[3].get("remaining", float("inf"))) - _, limit, reset_ts, meta = most_restrictive - remaining = meta.get("remaining", 0) - retry_after = meta.get("reset_in", 0) + async def _allow_many_fixed(self, client: Any, parsed: List[Tuple[str, int, int]], redis_keys: List[str]) -> List[tuple[bool, int, int, dict[str, Any]]]: + """Batch fixed-window: one eval call for all N dimensions. - # Aggregate metadata from all dimensions - aggregated_meta = { - "limited": True, - "remaining": remaining, - "reset_in": retry_after, - "dimensions": {"allowed": [m for _, _, _, m in allowed_dims]}, - } - return True, limit, remaining, reset_ts, aggregated_meta + Args: + client: Async Redis client instance. + parsed: List of ``(dimension_key, count, window_seconds)`` tuples. + redis_keys: Pre-built Redis keys corresponding to *parsed*. + + Returns: + One ``(allowed, limit, reset_timestamp, metadata)`` tuple per dimension. + """ + argv = [str(window) for _, _, window in parsed] + raw = await self._evalsha(client, self._sha_batch_fixed, self._LUA_BATCH_FIXED, len(parsed), *redis_keys, *argv) + now = int(time.time()) + results = [] + for i, (_key, count, _window) in enumerate(parsed): + current_count = int(raw[i][0]) + ttl = int(raw[i][1]) + reset_timestamp = now + max(ttl, 0) + reset_in = max(ttl, 0) + remaining = max(0, count - current_count) + if current_count > count: + results.append((False, count, reset_timestamp, {"limited": True, "remaining": 0, "reset_in": reset_in})) + else: + results.append((True, count, reset_timestamp, {"limited": True, "remaining": remaining, "reset_in": reset_in})) + return results + + async def _allow_many_sliding(self, client: Any, parsed: List[Tuple[str, int, int]], redis_keys: List[str]) -> List[tuple[bool, int, int, dict[str, Any]]]: + """Batch sliding-window: one eval call for all N dimensions. + + Args: + client: Async Redis client instance. + parsed: List of ``(dimension_key, count, window_seconds)`` tuples. + redis_keys: Pre-built Redis keys corresponding to *parsed*. + + Returns: + One ``(allowed, limit, reset_timestamp, metadata)`` tuple per dimension. + """ + now = time.time() + argv: List[Any] = [now] + for _key, count, window in parsed: + argv += [window, count, f"{now}:{uuid.uuid4().hex}"] + raw = await self._evalsha(client, self._sha_batch_sliding, self._LUA_BATCH_SLIDING, len(parsed), *redis_keys, *argv) + results = [] + for i, (_key, count, window) in enumerate(parsed): + allowed_int = int(raw[i][0]) + current_count = int(raw[i][1]) + oldest_ts = float(raw[i][2]) if raw[i][2] else now + reset_timestamp = int(oldest_ts + window) + reset_in = max(0, int(reset_timestamp - now)) + remaining = max(0, count - current_count) + if not allowed_int: + results.append((False, count, reset_timestamp, {"limited": True, "remaining": 0, "reset_in": max(1, reset_in)})) + else: + results.append((True, count, reset_timestamp, {"limited": True, "remaining": remaining, "reset_in": reset_in})) + return results + + async def _allow_many_token_bucket(self, client: Any, parsed: List[Tuple[str, int, int]], redis_keys: List[str]) -> List[tuple[bool, int, int, dict[str, Any]]]: + """Batch token-bucket: one eval call for all N dimensions. + + Args: + client: Async Redis client instance. + parsed: List of ``(dimension_key, count, window_seconds)`` tuples. + redis_keys: Pre-built Redis keys corresponding to *parsed*. + + Returns: + One ``(allowed, limit, reset_timestamp, metadata)`` tuple per dimension. + """ + now = time.time() + argv: List[Any] = [now] + for _key, count, window in parsed: + refill_rate = count / window + argv += [count, refill_rate] + raw = await self._evalsha(client, self._sha_batch_token_bucket, self._LUA_BATCH_TOKEN_BUCKET, len(parsed), *redis_keys, *argv) + results = [] + for i, (_key, count, window) in enumerate(parsed): + refill_rate = count / window + allowed_int = int(raw[i][0]) + remaining = int(raw[i][1]) + time_to_next = int(raw[i][2]) + if not allowed_int: + reset_timestamp = int(now + time_to_next) + results.append((False, count, reset_timestamp, {"limited": True, "remaining": 0, "reset_in": time_to_next})) + else: + tokens_needed = count - remaining + time_to_full = max(1, int(tokens_needed / refill_rate)) if tokens_needed > 0 else 0 + reset_timestamp = int(now + time_to_full) + results.append((True, count, reset_timestamp, {"limited": True, "remaining": remaining, "reset_in": time_to_full})) + return results + + +# --------------------------------------------------------------------------- +# Config +# --------------------------------------------------------------------------- + + +class RateLimiterConfig(BaseModel): + """Configuration for the rate limiter plugin. + + Attributes: + by_user: Rate limit per user (e.g., '60/m'). + by_tenant: Rate limit per tenant (e.g., '600/m'). + by_tool: Per-tool rate limits (e.g., {'search': '10/m'}). + algorithm: Counting algorithm — 'fixed_window', 'sliding_window', or 'token_bucket'. + backend: Storage backend — 'memory' (default) or 'redis'. + redis_url: Redis connection URL, required when backend='redis'. + redis_key_prefix: Prefix for all Redis keys (default 'rl'). + redis_fallback: Fall back to in-process memory if Redis is unavailable (default True). + """ + + by_user: Optional[str] = Field(default=None, description="e.g. '60/m'") + by_tenant: Optional[str] = Field(default=None, description="e.g. '600/m'") + by_tool: Optional[Dict[str, str]] = Field(default=None, description="per-tool rates, e.g. {'search': '10/m'}") + algorithm: str = Field(default=ALGORITHM_FIXED_WINDOW, description="'fixed_window', 'sliding_window', or 'token_bucket'") + backend: str = Field(default="memory", description="'memory' or 'redis'") + redis_url: Optional[str] = Field(default=None, description="Redis URL, e.g. 'redis://localhost:6379/0'") + redis_key_prefix: str = Field(default="rl", description="Prefix for Redis keys") + redis_fallback: bool = Field(default=True, description="Fall back to memory if Redis is unavailable") + + +# --------------------------------------------------------------------------- +# Plugin +# --------------------------------------------------------------------------- class RateLimiterPlugin(Plugin): - """Simple fixed-window rate limiter with per-user/tenant/tool buckets.""" + """Rate limiter with pluggable algorithm (fixed_window, sliding_window, token_bucket).""" def __init__(self, config: PluginConfig) -> None: - """Initialize the rate limiter plugin. + """Initialise the plugin, parse config, and set up the rate limiting backend. Args: - config: Plugin configuration containing rate limit settings. - - Raises: - ValueError: If any configured rate string is malformed or uses an unsupported unit. + config: Plugin configuration from the plugin framework. """ super().__init__(config) self._cfg = RateLimiterConfig(**(config.config or {})) + self._rust_consecutive_failures: int = 0 + self._rust_failure_lock = threading.Lock() + self._rust_disabled_at: Optional[float] = None # monotonic time when engine was disabled + self._rust_recovery_interval: float = 60.0 # seconds before attempting re-enable + self._failopen_error_count: int = 0 # total fail-open events for observability self._validate_config() + + # Pre-compute normalised by_tool keys once — used on every hook call. + self._normalised_by_tool: Dict[str, str] = {k.strip().lower(): v for k, v in self._cfg.by_tool.items()} if self._cfg.by_tool else {} + + # Rust engine — handles both memory and Redis backends when available. + # For Redis: Rust owns the connection and fires batch Lua scripts directly, + # keeping the shared counter semantics required for multi-instance deployments. + # Pre-parse limits here so the hot path never does string parsing (IFACE-01). + self._rust_engine: Optional[Any] = None + if _RUST_AVAILABLE: + try: + rust_config: Dict[str, Any] = { + "by_user": self._cfg.by_user, + "by_tenant": self._cfg.by_tenant, + "by_tool": self._cfg.by_tool or {}, + "algorithm": self._cfg.algorithm, + "backend": self._cfg.backend, + } + if self._cfg.backend == "redis": + rust_config["redis_url"] = self._cfg.redis_url + rust_config["redis_key_prefix"] = self._cfg.redis_key_prefix + self._rust_engine = RustRateLimiterEngine(rust_config) + self._rust_config = rust_config # kept for recovery re-init + # Pre-parsed (count, window_nanos) for each dimension — used to build + # the checks list passed to evaluate_many() on every hook call. + self._rust_by_user: Optional[Tuple[int, int]] = self._parse_rate_nanos(self._cfg.by_user) if self._cfg.by_user else None + self._rust_by_tenant: Optional[Tuple[int, int]] = self._parse_rate_nanos(self._cfg.by_tenant) if self._cfg.by_tenant else None + self._rust_by_tool: Dict[str, Tuple[int, int]] = {k.strip().lower(): self._parse_rate_nanos(v) for k, v in (self._cfg.by_tool or {}).items()} + logger.debug("Rate limiter using Rust engine (backend=%s, algorithm=%s)", self._cfg.backend, self._cfg.algorithm) + except Exception: + logger.error("Failed to initialise Rust rate limiter engine; falling back to Python backend", exc_info=True) + self._rust_engine = None + + algorithm = _make_algorithm(self._cfg.algorithm) + if self._cfg.backend == "redis": - fallback = _backend if self._cfg.redis_fallback else None + fallback_backend = MemoryBackend(_make_algorithm(self._cfg.algorithm)) if self._cfg.redis_fallback else None self._rate_backend: MemoryBackend | RedisBackend = RedisBackend( - redis_url=self._cfg.redis_url or "redis://localhost:6379/0", + redis_url=self._cfg.redis_url, key_prefix=self._cfg.redis_key_prefix, - fallback=fallback, + algorithm_name=self._cfg.algorithm, + fallback=fallback_backend, ) else: - self._rate_backend = _backend + self._rate_backend = MemoryBackend(algorithm) def _validate_config(self) -> None: - """Validate all configured rate strings and backend at startup. - - Parses every rate string (by_user, by_tenant, and all by_tool entries) so that - malformed or unsupported values raise immediately at plugin initialisation rather - than propagating a ValueError to callers at request time. + """Validate rate strings and algorithm/backend settings; raise ValueError on error. Raises: - ValueError: Collected error message listing every invalid rate string found. + ValueError: If any rate string is malformed or settings are invalid. """ errors: list[str] = [] + if self._cfg.algorithm not in VALID_ALGORITHMS: + errors.append(f"algorithm={self._cfg.algorithm!r}: must be one of {VALID_ALGORITHMS}") + if self._cfg.backend not in ("memory", "redis"): errors.append(f"backend={self._cfg.backend!r}: must be 'memory' or 'redis'") - for field, value in [("by_user", self._cfg.by_user), ("by_tenant", self._cfg.by_tenant)]: + if self._cfg.backend == "redis" and not self._cfg.redis_url: + errors.append("redis_url is required when backend='redis'") + + for field_name, value in [("by_user", self._cfg.by_user), ("by_tenant", self._cfg.by_tenant)]: if value is not None: try: _parse_rate(value) except ValueError as exc: - errors.append(f"{field}={value!r}: {exc}") + errors.append(f"{field_name}={value!r}: {exc}") if self._cfg.by_tool: + normalised_keys: set[str] = set() for tool_name, rate in self._cfg.by_tool.items(): try: _parse_rate(rate) except ValueError as exc: errors.append(f"by_tool[{tool_name!r}]={rate!r}: {exc}") + norm_key = tool_name.strip().lower() + if norm_key in normalised_keys: + errors.append(f"by_tool has duplicate key after normalisation: {tool_name!r} -> {norm_key!r}") + normalised_keys.add(norm_key) if errors: raise ValueError("RateLimiterPlugin config errors: " + "; ".join(errors)) - async def prompt_pre_fetch(self, payload: PromptPrehookPayload, context: PluginContext) -> PromptPrehookResult: - """Check rate limits before fetching a prompt. + @staticmethod + def _parse_rate_nanos(rate: str) -> Tuple[int, int]: + """Parse a rate string and return (count, window_nanos). Args: - payload: The prompt pre-fetch payload. - context: Plugin execution context containing user and tenant information. + rate: Rate string (e.g. ``"60/m"``). Returns: - PromptPrehookResult indicating whether to continue or block due to rate limit. + Tuple of ``(count, window_nanos)``. """ - try: - prompt = payload.prompt_id - user = context.global_context.user or "anonymous" - tenant = context.global_context.tenant_id or "default" + count, window_secs = _parse_rate(rate) + return count, window_secs * 1_000_000_000 - # Check all dimensions - results = [ - await self._rate_backend.allow(f"user:{user}", self._cfg.by_user), - await self._rate_backend.allow(f"tenant:{tenant}", self._cfg.by_tenant), - ] + def _build_rust_checks(self, user: str, tenant: Optional[str], tool: str) -> List[Tuple[str, int, int]]: + """Build the checks list for evaluate_many() from the current request context. - # Check per-prompt/tool limit if configured (keyed by prompt_id) - by_tool_config = self._cfg.by_tool - if by_tool_config and prompt in by_tool_config: # pylint: disable=unsupported-membership-test - results.append(await self._rate_backend.allow(f"tool:{prompt}", by_tool_config[prompt])) + Python extracts context; Rust engine does all rate math (ARCH-03). + None tenant is excluded — no check added (CORR-04). - # Select most restrictive - allowed, limit, remaining, reset_ts, meta = _select_most_restrictive(results) - retry_after = meta.get("reset_in", 0) + Args: + user: Normalised user identity string. + tenant: Tenant identifier, or ``None`` to skip the tenant dimension. + tool: Lowercased tool or prompt name. - if not allowed: - # Rate limit exceeded - include Retry-After header - headers = _make_headers(limit, remaining, reset_ts, retry_after, include_retry_after=True) - return PromptPrehookResult( - continue_processing=False, - violation=PluginViolation( - reason="Rate limit exceeded", - description=f"Rate limit exceeded for prompt '{prompt}'", - code="RATE_LIMIT", - details=meta, - http_status_code=429, - http_headers=headers, - ), - ) + Returns: + List of ``(key, limit_count, window_nanos)`` tuples for active dimensions. + """ + checks: List[Tuple[str, int, int]] = [] + if self._rust_by_user: + count, window_nanos = self._rust_by_user + checks.append((f"user:{user}", count, window_nanos)) + if tenant and self._rust_by_tenant: + count, window_nanos = self._rust_by_tenant + checks.append((f"tenant:{tenant}", count, window_nanos)) + if tool in self._rust_by_tool: + count, window_nanos = self._rust_by_tool[tool] + checks.append((f"tool:{tool}", count, window_nanos)) + return checks + + def _rust_to_plugin_headers(self, result: Any, include_retry_after: bool) -> dict[str, str]: + """Convert an EvalResult to HTTP rate-limit headers (CORR-02). - # Success - include informational headers (without Retry-After) - if limit > 0: - headers = _make_headers(limit, remaining, reset_ts, retry_after, include_retry_after=False) - return PromptPrehookResult(metadata=meta, http_headers=headers) + Args: + result: Rust ``EvalResult`` instance. + include_retry_after: Whether to include ``Retry-After`` in the headers. - return PromptPrehookResult(metadata=meta) + Returns: + Dictionary of HTTP rate-limit headers. + """ + retry_after = result.retry_after if result.retry_after is not None else 0 + return _make_headers(result.limit, result.remaining, result.reset_timestamp, retry_after, include_retry_after) - except Exception: - logger.exception("RateLimiterPlugin.prompt_pre_fetch encountered an unexpected error; allowing request") - return PromptPrehookResult() + def _rust_to_plugin_meta(self, result: Any) -> dict[str, Any]: + """Convert a Rust EvalResult into the same metadata shape as the Python path. - async def tool_pre_invoke(self, payload: ToolPreInvokePayload, context: PluginContext) -> ToolPreInvokeResult: - """Check rate limits before invoking a tool. + Args: + result: Rust ``EvalResult`` instance. + + Returns: + Plugin metadata dict with ``limited``, ``remaining``, ``reset_in``, and ``dimensions``. + """ + + def _dimension_meta(dim: Any) -> dict[str, Any]: + """Convert a single Rust dimension result into Python plugin metadata. + + Args: + dim: Rust ``EvalDimension`` instance. + + Returns: + Metadata dict for a single dimension. + """ + reset_in = dim.retry_after if dim.retry_after is not None else max(0, int(dim.reset_timestamp) - int(time.time())) + return { + "limited": True, + "remaining": int(dim.remaining), + "reset_in": reset_in, + } + + reset_in = result.retry_after if result.retry_after is not None else max(0, int(result.reset_timestamp) - int(time.time())) + meta: dict[str, Any] = { + "limited": True, + "remaining": int(result.remaining), + "reset_in": reset_in, + } + if not result.allowed: + meta["dimensions"] = { + "violated": [_dimension_meta(dim) for dim in result.violated_dimensions], + "allowed": [_dimension_meta(dim) for dim in result.allowed_dimensions], + } + elif result.allowed_dimensions: + meta["dimensions"] = { + "allowed": [_dimension_meta(dim) for dim in result.allowed_dimensions], + } + return meta + + def _should_fallback_to_python_redis(self) -> bool: + """Return True when Redis-backed Rust errors should drop to Python fallback. + + Returns: + Whether the Python Redis backend is available as a fallback. + """ + return self._cfg.backend == "redis" and self._cfg.redis_fallback and isinstance(self._rate_backend, RedisBackend) + + def _should_use_async_rust_redis(self) -> bool: + """Return True when the Rust Redis fast path should use the async bridge. + + Returns: + Whether the backend is Redis (requiring the async code path). + """ + return self._cfg.backend == "redis" + + async def _check_rust_fast_path(self, user: str, tenant: Optional[str], entity: str, hook_name: str) -> Optional[Tuple[bool, Optional[Dict[str, str]], Dict[str, Any]]]: + """Attempt rate evaluation via the Rust engine (ARCH-01). Args: - payload: The tool pre-invoke payload containing tool name and arguments. - context: Plugin execution context containing user and tenant information. + user: Normalised user identity string. + tenant: Tenant identifier, or ``None`` to skip the tenant dimension. + entity: Lowercased tool or prompt name. + hook_name: Hook identifier for logging. Returns: - ToolPreInvokeResult indicating whether to continue or block due to rate limit. + The ``(allowed, headers, meta)`` tuple on success, or ``None`` to + fall through to the Python path. """ try: - tool = payload.name - user = context.global_context.user or "anonymous" - tenant = context.global_context.tenant_id or "default" + now_unix = int(time.time()) + if self._should_use_async_rust_redis(): + allowed, headers, meta = await self._rust_engine.check_async(user, tenant, entity, now_unix, True) + else: + allowed, headers, meta = self._rust_engine.check(user, tenant, entity, now_unix, True) + except Exception: + with self._rust_failure_lock: + self._rust_consecutive_failures += 1 + failures = self._rust_consecutive_failures + if failures >= 10: + logger.error( + "Rust rate limiter disabled after %d consecutive failures during %s; will attempt recovery in %.0fs", + failures, + hook_name, + self._rust_recovery_interval, + exc_info=True, + ) + self._rust_engine = None + self._rust_disabled_at = time.monotonic() + else: + logger.warning( + "Rust rate limiter failed during %s (%d/%d before disable); %s", + hook_name, + failures, + 10, + "falling back to Python Redis backend" if self._should_fallback_to_python_redis() else "falling through to Python path", + exc_info=True, + ) + return None + + with self._rust_failure_lock: + self._rust_consecutive_failures = 0 + if meta.get("limited") is False: + return True, None, meta + if not allowed: + return False, headers, meta + headers.pop("Retry-After", None) + return True, headers, meta + + def _maybe_recover_rust_engine(self) -> None: + """Attempt to re-initialise the Rust engine after a timed backoff.""" + if self._rust_disabled_at is None or not _RUST_AVAILABLE: + return + if time.monotonic() - self._rust_disabled_at < self._rust_recovery_interval: + return + try: + self._rust_engine = RustRateLimiterEngine(self._rust_config) + with self._rust_failure_lock: + self._rust_consecutive_failures = 0 + self._rust_disabled_at = None + logger.info("Rust rate limiter engine recovered after backoff") + except Exception: + # Push the next recovery attempt out by another interval. + self._rust_disabled_at = time.monotonic() + logger.warning("Rust rate limiter recovery failed; will retry in %.0fs", self._rust_recovery_interval, exc_info=True) + + async def _check_python_fallback(self, user: str, tenant: Optional[str], entity: str) -> Tuple[bool, Optional[Dict[str, str]], Dict[str, Any]]: + """Rate evaluation via the Python backend (ARCH-05: fallback). + + Args: + user: Normalised user identity string. + tenant: Tenant identifier, or ``None`` to skip the tenant dimension. + entity: Lowercased tool or prompt name. + + Returns: + Tuple of ``(allowed, headers, meta)``. + """ + checks: List[Tuple[str, str]] = [] + if self._cfg.by_user: + checks.append((f"user:{user}", self._cfg.by_user)) + if tenant and self._cfg.by_tenant: + checks.append((f"tenant:{tenant}", self._cfg.by_tenant)) + if self._normalised_by_tool and entity in self._normalised_by_tool: + checks.append((f"tool:{entity}", self._normalised_by_tool[entity])) + + if not checks: + return True, None, {"limited": False} + + if isinstance(self._rate_backend, RedisBackend): + results = await self._rate_backend.allow_many(checks) + else: + results = [await self._rate_backend.allow(key, limit) for key, limit in checks] + + allowed, limit, remaining, reset_ts, meta = _select_most_restrictive(results) + retry_after = meta.get("reset_in", 0) + + if not allowed: + headers = _make_headers(limit, remaining, reset_ts, retry_after, include_retry_after=True) + return False, headers, meta - # Check all dimensions - results = [ - await self._rate_backend.allow(f"user:{user}", self._cfg.by_user), - await self._rate_backend.allow(f"tenant:{tenant}", self._cfg.by_tenant), - ] + if limit > 0: + headers = _make_headers(limit, remaining, reset_ts, retry_after, include_retry_after=False) + return True, headers, meta - # Check per-tool limit if configured - by_tool_config = self._cfg.by_tool - if by_tool_config and tool in by_tool_config: # pylint: disable=unsupported-membership-test - results.append(await self._rate_backend.allow(f"tool:{tool}", by_tool_config[tool])) + return True, None, meta - # Select most restrictive - allowed, limit, remaining, reset_ts, meta = _select_most_restrictive(results) - retry_after = meta.get("reset_in", 0) + async def _check_rate_limit(self, user: str, tenant: Optional[str], entity: str, hook_name: str) -> Tuple[bool, Optional[Dict[str, str]], Dict[str, Any]]: + """Core rate-limit evaluation shared by prompt_pre_fetch and tool_pre_invoke. + + Args: + user: Normalised user identity string. + tenant: Tenant identifier, or ``None`` to skip the tenant dimension. + entity: Lowercased tool or prompt name. + hook_name: Hook identifier for logging (e.g. ``"tool_pre_invoke"``). + + Returns: + Tuple of ``(allowed, headers, meta)`` where *headers* is ``None`` + when no limits are configured and includes ``Retry-After`` only when blocked. + """ + if self._rust_engine is None and self._rust_disabled_at is not None: + self._maybe_recover_rust_engine() + + if self._rust_engine is not None: + result = await self._check_rust_fast_path(user, tenant, entity, hook_name) + if result is not None: + return result + + return await self._check_python_fallback(user, tenant, entity) + + async def _dispatch_hook(self, entity: str, context: PluginContext, hook_name: str, entity_label: str, result_cls: type) -> Any: + """Shared rate-limit dispatch for both hook methods. + + Extracts user/tenant from *context*, evaluates limits for *entity*, + and returns the appropriate *result_cls* instance. Fail-open on any + unexpected error (see module docstring "Security contract"). + + Args: + entity: Lowercased tool or prompt name being rate-limited. + context: Plugin context carrying user, tenant, and request state. + hook_name: Hook identifier for logging (e.g. ``"tool_pre_invoke"``). + entity_label: Human-readable label for error messages (``"tool"`` or ``"prompt"``). + result_cls: Result class to instantiate (``ToolPreInvokeResult`` or ``PromptPrehookResult``). + + Returns: + An instance of *result_cls*, either allowing the request or containing a violation. + """ + try: + user = _extract_user_identity(context.global_context.user) + tenant = str(context.global_context.tenant_id).strip() if context.global_context.tenant_id else None + + allowed, headers, meta = await self._check_rate_limit(user, tenant, entity, hook_name) if not allowed: - # Rate limit exceeded - include Retry-After header - headers = _make_headers(limit, remaining, reset_ts, retry_after, include_retry_after=True) - return ToolPreInvokeResult( + return result_cls( continue_processing=False, violation=PluginViolation( reason="Rate limit exceeded", - description=f"Rate limit exceeded for tool '{tool}'", + description=f"Rate limit exceeded for {entity_label} '{entity}'", code="RATE_LIMIT", details=meta, http_status_code=429, http_headers=headers, ), ) + if headers: + return result_cls(metadata=meta, http_headers=headers) + return result_cls(metadata=meta) - # Success - include informational headers (without Retry-After) - if limit > 0: - headers = _make_headers(limit, remaining, reset_ts, retry_after, include_retry_after=False) - return ToolPreInvokeResult(metadata=meta, http_headers=headers) + except Exception: + # Deliberate fail-open: engine errors must not block legitimate traffic. + # See module docstring "Security contract — fail-open on error". + self._failopen_error_count += 1 + logger.exception("RateLimiterPlugin.%s encountered an unexpected error; allowing request (failopen_errors=%d)", hook_name, self._failopen_error_count) + return result_cls() - return ToolPreInvokeResult(metadata=meta) + async def prompt_pre_fetch(self, payload: PromptPrehookPayload, context: PluginContext) -> PromptPrehookResult: + """Enforce rate limits before a prompt is fetched. - except Exception: - logger.exception("RateLimiterPlugin.tool_pre_invoke encountered an unexpected error; allowing request") - return ToolPreInvokeResult() + Args: + payload: Prompt prehook payload containing the prompt identifier. + context: Plugin context carrying user, tenant, and request state. + + Returns: + Result allowing the request or containing a rate-limit violation. + """ + return await self._dispatch_hook(payload.prompt_id.strip().lower(), context, "prompt_pre_fetch", "prompt", PromptPrehookResult) + + async def tool_pre_invoke(self, payload: ToolPreInvokePayload, context: PluginContext) -> ToolPreInvokeResult: + """Enforce rate limits before a tool is invoked. + + Args: + payload: Tool pre-invoke payload containing the tool name. + context: Plugin context carrying user, tenant, and request state. + + Returns: + Result allowing the request or containing a rate-limit violation. + """ + return await self._dispatch_hook(payload.name.strip().lower(), context, "tool_pre_invoke", "tool", ToolPreInvokeResult) diff --git a/plugins_rust/rate_limiter/Cargo.lock b/plugins_rust/rate_limiter/Cargo.lock new file mode 100644 index 0000000000..917a9c8f5b --- /dev/null +++ b/plugins_rust/rate_limiter/Cargo.lock @@ -0,0 +1,2027 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloca" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" +dependencies = [ + "cc", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arc-swap" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a07d1f37ff60921c83bdfc7407723bdefe89b44b98a9b772f225c8f9d67141a6" +dependencies = [ + "rustversion", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.2.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "criterion" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" +dependencies = [ + "alloca", + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools 0.13.0", + "num-traits", + "oorandom", + "page_size", + "plotters", + "rayon", + "regex", + "serde", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" +dependencies = [ + "cast", + "itertools 0.13.0", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-macro", + "futures-sink", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inventory" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009ae045c87e7082cb72dab0ccd01ae075dd00141ddc108f43a0ea150a9e7227" +dependencies = [ + "rustversion", +] + +[[package]] +name = "is-macro" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57a3e447e24c22647738e4607f1df1e0ec6f72e16182c4cd199f647cdfb0e4" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lalrpop-util" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553" + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "matrixmultiply" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndarray" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "numpy" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778da78c64ddc928ebf5ad9df5edf0789410ff3bdbf3619aed51cd789a6af1e2" +dependencies = [ + "libc", + "ndarray", + "num-complex", + "num-integer", + "num-traits", + "pyo3", + "pyo3-build-config", + "rustc-hash 2.1.1", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "ordered-float" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4779c6901a562440c3786d08192c6fbda7c1c2060edd10006b05ee35d10f2d" +dependencies = [ + "num-traits", +] + +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pyo3" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf85e27e86080aafd5a22eae58a162e133a589551542b3e5cee4beb27e54f8e1" +dependencies = [ + "libc", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", +] + +[[package]] +name = "pyo3-async-runtimes" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e7364a95bf00e8377bbf9b0f09d7ff9715a29d8fcf93b47d1a967363b973178" +dependencies = [ + "futures-channel", + "futures-util", + "once_cell", + "pin-project-lite", + "pyo3", + "tokio", +] + +[[package]] +name = "pyo3-build-config" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf94ee265674bf76c09fa430b0e99c26e319c945d96ca0d5a8215f31bf81cf7" +dependencies = [ + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "491aa5fc66d8059dd44a75f4580a2962c1862a1c2945359db36f6c2818b748dc" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-log" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c2ec80932c5c3b2d4fbc578c9b56b2d4502098587edb8bef5b6bfcad43682e" +dependencies = [ + "arc-swap", + "log", + "pyo3", +] + +[[package]] +name = "pyo3-macros" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d671734e9d7a43449f8480f8b38115df67bef8d21f76837fa75ee7aaa5e52e" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22faaa1ce6c430a1f71658760497291065e6450d7b5dc2bcf254d49f66ee700a" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + +[[package]] +name = "pyo3-stub-gen" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b159f7704044f57d058f528a6f1f22a0a0a327dcb595c5fb38beae658e0338d6" +dependencies = [ + "anyhow", + "chrono", + "either", + "indexmap", + "inventory", + "itertools 0.14.0", + "log", + "maplit", + "num-complex", + "numpy", + "ordered-float", + "pyo3", + "pyo3-stub-gen-derive", + "rustpython-parser", + "serde", + "serde_json", + "time", + "toml", +] + +[[package]] +name = "pyo3-stub-gen-derive" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c79e7c5b1fcec7c39ab186594658a971c59911eb6fbab5a5932cf2318534be" +dependencies = [ + "heck", + "indexmap", + "proc-macro2", + "quote", + "rustpython-parser", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rate_limiter" +version = "0.1.0" +dependencies = [ + "criterion", + "log", + "parking_lot", + "pyo3", + "pyo3-async-runtimes", + "pyo3-log", + "pyo3-stub-gen", + "redis", + "thiserror", + "tokio", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redis" +version = "0.27.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d8f99a4090c89cc489a94833c901ead69bfbf3877b4867d5482e321ee875bc" +dependencies = [ + "arc-swap", + "async-trait", + "bytes", + "combine", + "futures-util", + "itertools 0.13.0", + "itoa", + "num-bigint", + "percent-encoding", + "pin-project-lite", + "ryu", + "sha1_smol", + "socket2 0.5.10", + "tokio", + "tokio-util", + "url", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustpython-ast" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cdaf8ee5c1473b993b398c174641d3aa9da847af36e8d5eb8291930b72f31a5" +dependencies = [ + "is-macro", + "num-bigint", + "rustpython-parser-core", + "static_assertions", +] + +[[package]] +name = "rustpython-parser" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "868f724daac0caf9bd36d38caf45819905193a901e8f1c983345a68e18fb2abb" +dependencies = [ + "anyhow", + "is-macro", + "itertools 0.11.0", + "lalrpop-util", + "log", + "num-bigint", + "num-traits", + "phf", + "phf_codegen", + "rustc-hash 1.1.0", + "rustpython-ast", + "rustpython-parser-core", + "tiny-keccak", + "unic-emoji-char", + "unic-ucd-ident", + "unicode_names2", +] + +[[package]] +name = "rustpython-parser-core" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4b6c12fa273825edc7bccd9a734f0ad5ba4b8a2f4da5ff7efe946f066d0f4ad" +dependencies = [ + "is-macro", + "memchr", + "rustpython-parser-vendored", +] + +[[package]] +name = "rustpython-parser-vendored" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04fcea49a4630a3a5d940f4d514dc4f575ed63c14c3e3ed07146634aed7f67a6" +dependencies = [ + "memchr", + "once_cell", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2 0.6.3", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "1.0.7+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd28d57d8a6f6e458bc0b8784f8fdcc4b99a437936056fa122cb234f18656a96" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.0.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.0.10+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.7+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17aaa1c6e3dc22b1da4b6bba97d066e354c7945cac2f7852d4e4e7ca7a6b56d" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-emoji-char" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b07221e68897210270a38bde4babb655869637af0f69407f96053a34f76494d" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode_names2" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1673eca9782c84de5f81b82e4109dcfb3611c8ba0d52930ec4a9478f547b2dd" +dependencies = [ + "phf", + "unicode_names2_generator", +] + +[[package]] +name = "unicode_names2_generator" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91e5b84611016120197efd7dc93ef76774f4e084cd73c9fb3ea4a86c570c56e" +dependencies = [ + "getopts", + "log", + "phf_codegen", + "rand", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/plugins_rust/rate_limiter/Cargo.toml b/plugins_rust/rate_limiter/Cargo.toml new file mode 100644 index 0000000000..960754b9c9 --- /dev/null +++ b/plugins_rust/rate_limiter/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "rate_limiter" +version = "0.1.0" +edition = "2024" +authors = ["ContextForge Contributors"] +license = "Apache-2.0" +repository = "https://github.com/IBM/mcp-context-forge" +description = "High-performance rate limiter engine for MCP Gateway" + +[lib] +name = "rate_limiter_rust" +crate-type = ["cdylib", "rlib"] + +[[bin]] +name = "stub_gen" +path = "src/bin/stub_gen.rs" + +[dependencies] +pyo3 = { version = "0.28.2", features = ["abi3-py311"] } +pyo3-async-runtimes = { version = "0.28", features = ["tokio-runtime"] } +pyo3-stub-gen = "0.19" +pyo3-log = "0.13" +log = "0.4" +parking_lot = "0.12" +thiserror = "2.0" +redis = { version = "0.27", features = ["aio", "tokio-comp"] } +tokio = { version = "1", features = ["rt-multi-thread", "sync", "time"] } + +[dev-dependencies] +criterion = { version = "0.8", features = ["html_reports"] } + +[[bench]] +name = "rate_limiter" +harness = false + +[profile.release] +opt-level = 3 +lto = "fat" +codegen-units = 1 +strip = true + +[profile.bench] +inherits = "release" +debug = true diff --git a/plugins_rust/rate_limiter/Makefile b/plugins_rust/rate_limiter/Makefile new file mode 100644 index 0000000000..6af7c6d39b --- /dev/null +++ b/plugins_rust/rate_limiter/Makefile @@ -0,0 +1,161 @@ +# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +# Rate Limiter Engine - Makefile +# High-performance rate limiter engine (Rust + PyO3) +# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +# +# help: Rate Limiter Engine (Rust + Python extension build & automation) +# ───────────────────────────────────────────────────────────────────────── + +.PHONY: help +help: + @grep '^# help\:' $(firstword $(MAKEFILE_LIST)) | sed 's/^# help\: //' + +PACKAGE_NAME := rate_limiter +PROJECT_PYTHON := $(abspath ../../.venv/bin/python) + +GREEN := \033[0;32m +YELLOW := \033[0;33m +NC := \033[0m + +# ============================================================================= +# 🔍 LINTING & FORMAT +# ============================================================================= +# help: fmt - Format Rust code with rustfmt +# help: fmt-check - Check Rust code formatting (CI) +# help: clippy - Run clippy lints +.PHONY: fmt fmt-check clippy + +fmt: + cargo fmt + +fmt-check: + cargo fmt -- --check + +clippy: + cargo clippy -- -D warnings + +# ============================================================================= +# 🧪 TESTS +# ============================================================================= +# help: test - Run Rust unit tests +# help: test-verbose - Run Rust tests with verbose output +# help: test-python - Run Python wrapper tests +# help: test-all - Run both Rust and Python tests +.PHONY: test test-verbose test-python test-all test-integration + +test: + @echo "$(GREEN)Running rate_limiter Rust tests...$(NC)" + cargo test + +test-verbose: + @echo "$(GREEN)Running rate_limiter Rust tests (verbose)...$(NC)" + cargo test -- --nocapture + +test-python: + @echo "$(GREEN)Running Python wrapper tests...$(NC)" + cd ../.. && uv run pytest -k rate_limiter -v + +test-integration: + @echo "$(GREEN)Running integration tests (requires Redis)...$(NC)" + cd ../.. && uv run pytest tests/integration/test_rate_limiter.py -v + +test-all: test test-python + +# ============================================================================= +# 🛠 BUILD +# ============================================================================= +# help: stub-gen - Generate Python type stubs (.pyi files) +# help: build - Build release extension (no install) +# help: install - Build and install wheel into project venv +.PHONY: stub-gen build install uninstall + +stub-gen: + @echo "$(GREEN)Generating Python type stubs...$(NC)" + @PYO3_PYTHON="$(PROJECT_PYTHON)" cargo run --bin stub_gen + @echo "$(GREEN)Stubs generated$(NC)" + +build: stub-gen + @echo "$(GREEN)Building $(PACKAGE_NAME)...$(NC)" + @cd ../.. && uv run maturin build --release --manifest-path plugins_rust/rate_limiter/Cargo.toml + @echo "$(GREEN)Build complete$(NC)" + +install: stub-gen + @echo "$(GREEN)Installing $(PACKAGE_NAME)...$(NC)" + @cd ../.. && uv run maturin develop --release --manifest-path plugins_rust/rate_limiter/Cargo.toml + @echo "$(GREEN)Installation complete$(NC)" + +uninstall: + @echo "$(YELLOW)Uninstalling $(PACKAGE_NAME)...$(NC)" + @cd ../.. && uv pip uninstall -y mcpgateway-rate-limiter 2>/dev/null || true + +# ============================================================================= +# 📊 BENCHMARKS +# ============================================================================= +# help: bench - Run Criterion benchmarks +# help: bench-compare - Compare against saved baseline +# help: compare - Run Python vs Rust hook-path comparison +# help: compare-quick - Run a quick Python vs Rust comparison +# help: compare-detailed - Run a detailed Python vs Rust comparison +.PHONY: bench bench-baseline bench-compare compare compare-quick compare-detailed + +bench: + @echo "$(GREEN)Running benchmarks...$(NC)" + cargo bench + +bench-baseline: + cargo bench --bench rate_limiter -- --save-baseline main + +bench-compare: + cargo bench --bench rate_limiter -- --baseline main + +compare: install + @echo "$(GREEN)Running Python vs Rust hook-path comparison...$(NC)" + @cd ../.. && uv run python3 plugins_rust/rate_limiter/compare_performance.py + +compare-quick: install + @echo "$(GREEN)Running quick Python vs Rust hook-path comparison...$(NC)" + @cd ../.. && uv run python3 plugins_rust/rate_limiter/compare_performance.py --iterations 250 --warmup 25 + +compare-detailed: install + @echo "$(GREEN)Running detailed Python vs Rust hook-path comparison...$(NC)" + @cd ../.. && uv run python3 plugins_rust/rate_limiter/compare_performance.py --iterations 5000 --warmup 500 + +# ============================================================================= +# 🧹 CLEANUP +# ============================================================================= +.PHONY: clean clean-all + +clean: + cargo clean + rm -rf target/ coverage/ + find . -name "*.whl" -delete + +clean-all: clean + +# ============================================================================= +# 📚 DOCUMENTATION +# ============================================================================= +.PHONY: doc doc-open + +doc: + cargo doc --no-deps --document-private-items + +doc-open: doc + cargo doc --no-deps --document-private-items --open + +# ============================================================================= +# 🔧 DEVELOPMENT HELPERS +# ============================================================================= +# help: verify - Verify plugin installation +# help: check-all - Run fmt-check + clippy + test +.PHONY: verify check-all pre-commit + +verify: + @cd ../.. && uv run python -c "import rate_limiter_rust; print('✅ rate_limiter_rust available')" || echo "⚠️ rate_limiter_rust not installed — run: make install" + +check-all: fmt-check clippy test + @echo "$(GREEN)✔ All checks passed$(NC)" + +pre-commit: check-all + +.DEFAULT_GOAL := help diff --git a/plugins_rust/rate_limiter/benches/rate_limiter.rs b/plugins_rust/rate_limiter/benches/rate_limiter.rs new file mode 100644 index 0000000000..6d60a87ffe --- /dev/null +++ b/plugins_rust/rate_limiter/benches/rate_limiter.rs @@ -0,0 +1,244 @@ +// Copyright 2026 +// SPDX-License-Identifier: Apache-2.0 +// +// Criterion benchmarks for the rate limiter memory backend. +// PERF-01, MEM-02, MEM-03, MEM-04. +// +// These benchmarks test the raw MemoryStore performance (no PyO3 overhead) +// across various access patterns: single-key, multi-dim, hot-counter, +// blocked-path, many-keys, and multi-threaded contention. + +use std::hint::black_box; +use std::sync::Arc; + +use criterion::{Criterion, criterion_group, criterion_main}; +use rate_limiter_rust::{clock::FakeClock, config::Algorithm, memory::MemoryStore}; + +const T0_UNIX: i64 = 1_000_000; +const LIMIT: u64 = 100; +const WINDOW: u64 = 60_000_000_000; // 60s in nanos + +fn make_store_and_clock() -> (Arc, rate_limiter_rust::clock::FakeClockHandle) { + let (clock, handle) = FakeClock::new(T0_UNIX); + let _ = clock; // clock is only needed for engine; store uses explicit timestamps + (Arc::new(MemoryStore::new()), handle) +} + +// --------------------------------------------------------------------------- +// Original single-key benchmarks (direct MemoryStore, no Python required) +// --------------------------------------------------------------------------- + +fn bench_fixed_window(c: &mut Criterion) { + let (store, handle) = make_store_and_clock(); + c.bench_function("fixed_window/single_key", |b| { + b.iter(|| { + handle.advance_secs(61); + store.check_and_increment( + black_box("user:bench"), + LIMIT, + WINDOW, + Algorithm::FixedWindow, + handle.monotonic_nanos(), + handle.unix_secs(), + ) + }) + }); +} + +fn bench_token_bucket(c: &mut Criterion) { + let (store, handle) = make_store_and_clock(); + c.bench_function("token_bucket/single_key", |b| { + b.iter(|| { + handle.advance_secs(61); + store.check_and_increment( + black_box("user:bench"), + LIMIT, + WINDOW, + Algorithm::TokenBucket, + handle.monotonic_nanos(), + handle.unix_secs(), + ) + }) + }); +} + +fn bench_sliding_window(c: &mut Criterion) { + let (store, handle) = make_store_and_clock(); + c.bench_function("sliding_window/single_key", |b| { + b.iter(|| { + handle.advance_secs(61); + store.check_and_increment( + black_box("user:bench"), + LIMIT, + WINDOW, + Algorithm::SlidingWindow, + handle.monotonic_nanos(), + handle.unix_secs(), + ) + }) + }); +} + +fn bench_multi_dim(c: &mut Criterion) { + let (store, handle) = make_store_and_clock(); + c.bench_function("fixed_window/three_dims", |b| { + b.iter(|| { + handle.advance_secs(61); + let now_mono = handle.monotonic_nanos(); + let now_unix = handle.unix_secs(); + let _r1 = store.check_and_increment( + "user:alice", + LIMIT, + WINDOW, + Algorithm::FixedWindow, + now_mono, + now_unix, + ); + let _r2 = store.check_and_increment( + "tenant:acme", + LIMIT * 100, + WINDOW, + Algorithm::FixedWindow, + now_mono, + now_unix, + ); + let _r3 = store.check_and_increment( + "tool:search", + LIMIT / 10, + WINDOW, + Algorithm::FixedWindow, + now_mono, + now_unix, + ); + }) + }); +} + +// --------------------------------------------------------------------------- +// Hot-counter: counter at near-limit, no window reset between iterations +// --------------------------------------------------------------------------- + +fn bench_fixed_window_hot_counter(c: &mut Criterion) { + let (store, handle) = make_store_and_clock(); + let mut iteration = 0u64; + c.bench_function("fixed_window/hot_counter", |b| { + b.iter(|| { + iteration += 1; + // Reset the window every LIMIT iterations to prevent permanent blocking + if iteration.is_multiple_of(LIMIT) { + handle.advance_secs(61); + } + store.check_and_increment( + black_box("user:hot"), + LIMIT, + WINDOW, + Algorithm::FixedWindow, + handle.monotonic_nanos(), + handle.unix_secs(), + ) + }) + }); +} + +// --------------------------------------------------------------------------- +// Blocked-path: counter past limit, measures reject code path +// --------------------------------------------------------------------------- + +fn bench_fixed_window_blocked(c: &mut Criterion) { + let (store, handle) = make_store_and_clock(); + // Exhaust the limit once + let now_mono = handle.monotonic_nanos(); + let now_unix = handle.unix_secs(); + for _ in 0..LIMIT { + store.check_and_increment( + "user:blocked", + LIMIT, + WINDOW, + Algorithm::FixedWindow, + now_mono, + now_unix, + ); + } + // Now every call hits the blocked path + c.bench_function("fixed_window/blocked_path", |b| { + b.iter(|| { + store.check_and_increment( + black_box("user:blocked"), + LIMIT, + WINDOW, + Algorithm::FixedWindow, + handle.monotonic_nanos(), + handle.unix_secs(), + ) + }) + }); +} + +// --------------------------------------------------------------------------- +// Many-keys: tests HashMap scaling and cache behavior +// --------------------------------------------------------------------------- + +fn bench_fixed_window_many_keys(c: &mut Criterion) { + let (store, handle) = make_store_and_clock(); + let keys: Vec = (0..10_000).map(|i| format!("user:many{}", i)).collect(); + let mut key_idx = 0usize; + c.bench_function("fixed_window/many_keys_10k", |b| { + b.iter(|| { + key_idx = (key_idx + 1) % keys.len(); + store.check_and_increment( + black_box(&keys[key_idx]), + LIMIT, + WINDOW, + Algorithm::FixedWindow, + handle.monotonic_nanos(), + handle.unix_secs(), + ) + }) + }); +} + +// --------------------------------------------------------------------------- +// Multi-threaded: concurrent access from N threads (parking_lot contention) +// --------------------------------------------------------------------------- + +fn bench_fixed_window_concurrent(c: &mut Criterion) { + let (store, handle) = make_store_and_clock(); + + for threads in [2, 4, 8] { + c.bench_function(&format!("fixed_window/concurrent_{}t", threads), |b| { + b.iter(|| { + handle.advance_secs(61); + let now_mono = handle.monotonic_nanos(); + let now_unix = handle.unix_secs(); + std::thread::scope(|s| { + for t in 0..threads { + let store = &store; + s.spawn(move || { + store.check_and_increment( + &format!("user:thread{}", t), + LIMIT, + WINDOW, + Algorithm::FixedWindow, + now_mono, + now_unix, + ) + }); + } + }); + }) + }); + } +} + +criterion_group!( + benches, + bench_fixed_window, + bench_token_bucket, + bench_sliding_window, + bench_multi_dim, + bench_fixed_window_hot_counter, + bench_fixed_window_blocked, + bench_fixed_window_many_keys, + bench_fixed_window_concurrent, +); +criterion_main!(benches); diff --git a/plugins_rust/rate_limiter/compare_performance.py b/plugins_rust/rate_limiter/compare_performance.py new file mode 100755 index 0000000000..d42081d750 --- /dev/null +++ b/plugins_rust/rate_limiter/compare_performance.py @@ -0,0 +1,621 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Compare Python and Rust rate limiter hook performance. + +This benchmark measures the real plugin hook path, not just the raw Rust engine. +It mirrors the comparison style used by other Rust plugins in this repository by +reporting Python-vs-Rust timings in ms/iteration for the same hook inputs. + +Design choices for fairness: +- use fresh identities per iteration (latency mode) so counters do not + accumulate differently between implementations +- compare the same hook (`prompt_pre_fetch` / `tool_pre_invoke`) with the same + plugin config, only toggling whether the Rust engine is active +- use a dedicated Redis DB (default: /15) so the benchmark does not disturb the + running local stack + +Modes: +- latency (default): per-call latency comparison, sequential +- throughput: max ops/sec comparison at various concurrency levels using + ThreadPoolExecutor — demonstrates Rust's GIL-release advantage + +Options: +- --dimensions 1|3: number of rate limit dimensions (1=user only, 3=user+tenant+tool) +- --workload allow|mixed: allow-only or mixed allow/block +- --concurrency N: thread count for throughput mode +""" + +# Future +from __future__ import annotations + +# Standard +import argparse +import asyncio +from dataclasses import dataclass +from pathlib import Path +import statistics +import sys +import time +from typing import Any, Sequence +from uuid import uuid4 + +# Third-Party +from pydantic import BaseModel + +ROOT = Path(__file__).resolve().parents[2] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +# First-Party +from mcpgateway.plugins.framework import GlobalContext, PluginConfig, PluginContext, PromptPrehookPayload, ToolPreInvokePayload +from plugins.rate_limiter.rate_limiter import RateLimiterPlugin + +try: + # Third-Party + import redis.asyncio as aioredis +except ImportError: # pragma: no cover - dependency exists in repo venv + aioredis = None + + +class BenchmarkResult(BaseModel): + """One measured implementation result for a scenario.""" + + implementation: str + mean_ms: float + median_ms: float + p95_ms: float + + +class ThroughputResult(BaseModel): + """Throughput benchmark result for one concurrency level.""" + + implementation: str + threads: int + ops_per_sec: float + total_ops: int + duration_sec: float + + +@dataclass(frozen=True) +class Scenario: + """A benchmark scenario.""" + + algorithm: str + backend: str + hook: str + dimensions: int = 1 + workload: str = "allow" + + +def _percentile(values: Sequence[float], percentile: float) -> float: + """Return a simple percentile from a sorted float sequence.""" + if not values: + return 0.0 + ordered = sorted(values) + index = min(len(ordered) - 1, max(0, int(round((len(ordered) - 1) * percentile)))) + return ordered[index] + + +def _make_plugin_config( + algorithm: str, + backend: str, + redis_url: str, + redis_key_prefix: str, + dimensions: int = 1, + workload: str = "allow", +) -> PluginConfig: + """Create a plugin config for the benchmark. + + dimensions=1: by_user only + dimensions=3: by_user + by_tenant + by_tool (3-dimension batch) + + workload="allow": high limit so all requests are allowed + workload="mixed": low limit so some requests are blocked + """ + user_rate = "3/m" if workload == "mixed" else "600000/m" + config: dict[str, Any] = { + "algorithm": algorithm, + "backend": backend, + "redis_url": redis_url, + "redis_key_prefix": redis_key_prefix, + "redis_fallback": False, + } + if dimensions == 0: + # Baseline: no rate limits configured — plugin short-circuits immediately. + pass + else: + config["by_user"] = user_rate + if dimensions >= 3: + config["by_tenant"] = "6000000/m" if workload != "mixed" else "6/m" + config["by_tool"] = {"benchmark_tool": "3000000/m" if workload != "mixed" else "5/m"} + return PluginConfig( + name=f"rate-limiter-bench-{algorithm}-{backend}-d{dimensions}-{workload}", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=["prompt_pre_fetch", "tool_pre_invoke"], + config=config, + ) + + +def _build_plugin( + algorithm: str, + backend: str, + use_rust: bool, + redis_url: str, + redis_key_prefix: str, + dimensions: int = 1, + workload: str = "allow", +) -> RateLimiterPlugin: + """Instantiate a plugin and force the requested implementation path.""" + plugin = RateLimiterPlugin(_make_plugin_config(algorithm, backend, redis_url, redis_key_prefix, dimensions, workload)) + if not use_rust: + plugin._rust_engine = None + elif plugin._rust_engine is None: + raise RuntimeError("Rust rate limiter engine is not available. Run: make -C plugins_rust/rate_limiter install") + return plugin + + +def _build_prompt_contexts(count: int, dimensions: int = 1) -> list[PluginContext]: + """Build prompt benchmark contexts with fresh user identities.""" + if dimensions >= 3: + return [PluginContext(global_context=GlobalContext(request_id=f"prompt-{i}", user=f"prompt-user-{i}@example.com", tenant_id="bench-tenant")) for i in range(count)] + return [PluginContext(global_context=GlobalContext(request_id=f"prompt-{i}", user=f"prompt-user-{i}@example.com")) for i in range(count)] + + +def _build_tool_contexts(count: int, dimensions: int = 1) -> list[PluginContext]: + """Build tool benchmark contexts with fresh user identities.""" + if dimensions >= 3: + return [PluginContext(global_context=GlobalContext(request_id=f"tool-{i}", user=f"tool-user-{i}@example.com", tenant_id="bench-tenant")) for i in range(count)] + return [PluginContext(global_context=GlobalContext(request_id=f"tool-{i}", user=f"tool-user-{i}@example.com")) for i in range(count)] + + +async def _invoke_hook(plugin: RateLimiterPlugin, hook: str, payload: Any, context: PluginContext) -> Any: + """Invoke the selected plugin hook.""" + if hook == "prompt_pre_fetch": + return await plugin.prompt_pre_fetch(payload, context) + return await plugin.tool_pre_invoke(payload, context) + + +async def _cleanup_plugin(plugin: RateLimiterPlugin) -> None: + """Cancel any sweep task left behind by the memory backend.""" + rate_backend = getattr(plugin, "_rate_backend", None) + sweep_task = getattr(rate_backend, "_sweep_task", None) + if sweep_task is not None: + try: + sweep_task.cancel() + await sweep_task + except (asyncio.CancelledError, RuntimeError): + # RuntimeError: event loop is closed — happens when the task was + # created on a worker thread's event loop (throughput mode). + pass + except Exception: + pass + + +async def _flush_redis(redis_url: str) -> None: + """Flush the benchmark Redis DB for a clean run.""" + if aioredis is None: + return + client = aioredis.from_url(redis_url, decode_responses=False) + try: + await client.flushdb() + finally: + await client.aclose() + + +async def _redis_available(redis_url: str) -> bool: + """Check whether the benchmark Redis target is reachable.""" + if aioredis is None: + return False + client = aioredis.from_url(redis_url, decode_responses=False) + try: + return bool(await client.ping()) + except Exception: + return False + finally: + await client.aclose() + + +async def _parity_smoke_test(algorithm: str, backend: str, redis_url: str) -> None: + """Quick sanity-check that Python and Rust agree on an allow/block sequence.""" + redis_key_prefix = f"rlbench-parity-{algorithm}-{backend}-{uuid4().hex}" + if backend == "redis": + await _flush_redis(redis_url) + + plugin_python = RateLimiterPlugin( + PluginConfig( + name="rate-limiter-parity-python", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=["tool_pre_invoke"], + config={ + "algorithm": algorithm, + "backend": backend, + "by_user": "3/m", + "redis_url": redis_url, + "redis_key_prefix": redis_key_prefix, + "redis_fallback": False, + }, + ) + ) + plugin_python._rust_engine = None + + plugin_rust = RateLimiterPlugin( + PluginConfig( + name="rate-limiter-parity-rust", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=["tool_pre_invoke"], + config={ + "algorithm": algorithm, + "backend": backend, + "by_user": "3/m", + "redis_url": redis_url, + "redis_key_prefix": redis_key_prefix, + "redis_fallback": False, + }, + ) + ) + + if plugin_rust._rust_engine is None: + raise RuntimeError("Rust engine unavailable during parity check") + + payload = ToolPreInvokePayload(name="bench_tool", args={}) + python_sequence: list[bool] = [] + rust_sequence: list[bool] = [] + + for idx in range(4): + ctx_python = PluginContext(global_context=GlobalContext(request_id=f"parity-py-{idx}", user="same-user@example.com")) + ctx_rust = PluginContext(global_context=GlobalContext(request_id=f"parity-rs-{idx}", user="same-user@example.com")) + python_result = await plugin_python.tool_pre_invoke(payload, ctx_python) + rust_result = await plugin_rust.tool_pre_invoke(payload, ctx_rust) + python_sequence.append(python_result.continue_processing) + rust_sequence.append(rust_result.continue_processing) + + await _cleanup_plugin(plugin_python) + await _cleanup_plugin(plugin_rust) + + if python_sequence != rust_sequence: + raise AssertionError(f"Parity failed for {algorithm}/{backend}: python={python_sequence}, rust={rust_sequence}") + + +# --------------------------------------------------------------------------- +# Latency mode (original sequential benchmark) +# --------------------------------------------------------------------------- + + +async def _benchmark_scenario( + scenario: Scenario, + implementation: str, + iterations: int, + warmup: int, + redis_url: str, +) -> BenchmarkResult: + """Benchmark one scenario for either the Python or Rust path.""" + use_rust = implementation == "Rust" + redis_key_prefix = f"rlbench-{scenario.algorithm}-{scenario.backend}-{scenario.hook}-{implementation.lower()}-{uuid4().hex}" + + if scenario.backend == "redis": + await _flush_redis(redis_url) + + plugin = _build_plugin( + algorithm=scenario.algorithm, + backend=scenario.backend, + use_rust=use_rust, + redis_url=redis_url, + redis_key_prefix=redis_key_prefix, + dimensions=scenario.dimensions, + workload=scenario.workload, + ) + + total_calls = iterations + warmup + if scenario.hook == "prompt_pre_fetch": + payload = PromptPrehookPayload(prompt_id="benchmark_tool", args={}) + contexts = _build_prompt_contexts(total_calls, scenario.dimensions) + else: + payload = ToolPreInvokePayload(name="benchmark_tool", args={}) + contexts = _build_tool_contexts(total_calls, scenario.dimensions) + + # Warmup + for idx in range(warmup): + result = await _invoke_hook(plugin, scenario.hook, payload, contexts[idx]) + if scenario.workload == "allow" and not result.continue_processing: + raise AssertionError(f"Unexpected rate-limit during warmup for {scenario.algorithm}/{scenario.backend}/{scenario.hook}") + + times_ms: list[float] = [] + for idx in range(warmup, total_calls): + start = time.perf_counter() + await _invoke_hook(plugin, scenario.hook, payload, contexts[idx]) + elapsed_ms = (time.perf_counter() - start) * 1000 + times_ms.append(elapsed_ms) + + await _cleanup_plugin(plugin) + + return BenchmarkResult( + implementation=implementation, + mean_ms=statistics.mean(times_ms), + median_ms=statistics.median(times_ms), + p95_ms=_percentile(times_ms, 0.95), + ) + + +# --------------------------------------------------------------------------- +# Throughput mode (concurrent threads — demonstrates GIL-release advantage) +# --------------------------------------------------------------------------- + + +async def _run_concurrent_batch( + plugin: RateLimiterPlugin, + scenario: Scenario, + concurrency: int, + iterations_per_task: int, +) -> list[float]: + """Fire ``concurrency`` async tasks each running ``iterations_per_task`` calls. + + Returns a flat list of per-call times (ms). + """ + hook = scenario.hook + if hook == "prompt_pre_fetch": + payload = PromptPrehookPayload(prompt_id="benchmark_tool", args={}) + else: + payload = ToolPreInvokePayload(name="benchmark_tool", args={}) + + sem = asyncio.Semaphore(concurrency) + all_times: list[list[float]] = [[] for _ in range(concurrency)] + + async def _worker(worker_id: int) -> None: + for i in range(iterations_per_task): + async with sem: + ctx = PluginContext( + global_context=GlobalContext( + request_id=f"c-{worker_id}-{i}", + user=f"c-{worker_id}-{i}@bench.test", + tenant_id="bench-tenant" if scenario.dimensions >= 3 else None, + ) + ) + start = time.perf_counter() + await _invoke_hook(plugin, hook, payload, ctx) + all_times[worker_id].append((time.perf_counter() - start) * 1000) + + await asyncio.gather(*[_worker(w) for w in range(concurrency)]) + return [t for task_times in all_times for t in task_times] + + +async def _benchmark_throughput( + scenario: Scenario, + implementation: str, + concurrency: int, + iterations_per_task: int, + redis_url: str, +) -> ThroughputResult: + """Measure concurrent async throughput at a given concurrency level. + + Runs ``concurrency`` async tasks, each firing ``iterations_per_task`` + hook calls through the same plugin. This mirrors production uvicorn + usage where multiple request handlers share a plugin concurrently. + """ + use_rust = implementation == "Rust" + redis_key_prefix = f"rlbench-tp-{scenario.algorithm}-{implementation.lower()}-{uuid4().hex}" + + if scenario.backend == "redis": + await _flush_redis(redis_url) + + plugin = _build_plugin( + algorithm=scenario.algorithm, + backend=scenario.backend, + use_rust=use_rust, + redis_url=redis_url, + redis_key_prefix=redis_key_prefix, + dimensions=scenario.dimensions, + workload=scenario.workload, + ) + + start = time.monotonic() + times_ms = await _run_concurrent_batch(plugin, scenario, concurrency, iterations_per_task) + elapsed = time.monotonic() - start + total_ops = len(times_ms) + + await _cleanup_plugin(plugin) + + return ThroughputResult( + implementation=implementation, + threads=concurrency, + ops_per_sec=total_ops / elapsed if elapsed > 0 else 0, + total_ops=total_ops, + duration_sec=elapsed, + ) + + +# --------------------------------------------------------------------------- +# Run modes +# --------------------------------------------------------------------------- + + +async def _run_latency(args: argparse.Namespace, redis_enabled: bool) -> None: + """Run latency-mode benchmarks.""" + # --- Baseline: no rate limits configured --- + if args.baseline: + hook = args.hooks[0] + baseline_scenario = Scenario(algorithm="fixed_window", backend="memory", hook=hook, dimensions=0, workload="allow") + print("=" * 88) + print(f"BASELINE (no rate limits) / {hook}") + print("=" * 88) + baseline_result = await _benchmark_scenario(baseline_scenario, "Python", args.iterations, args.warmup, args.redis_url) + print(f" Baseline: mean {baseline_result.mean_ms:.4f} ms | median {baseline_result.median_ms:.4f} ms | p95 {baseline_result.p95_ms:.4f} ms") + print() + else: + baseline_result = None + + # --- Per-scenario benchmarks --- + scenarios = [ + Scenario(algorithm=algorithm, backend=backend, hook=hook, dimensions=args.dimensions, workload=args.workload) + for algorithm in ("fixed_window", "sliding_window", "token_bucket") + for backend in args.backends + for hook in args.hooks + ] + + for scenario in scenarios: + if scenario.backend == "redis" and not redis_enabled: + continue + print("=" * 88) + label = f"{scenario.algorithm} / {scenario.backend} / {scenario.hook}" + if scenario.dimensions > 1: + label += f" / {scenario.dimensions}d" + if scenario.workload != "allow": + label += f" / {scenario.workload}" + print(f"Scenario: {label}") + print("=" * 88) + python_result = await _benchmark_scenario(scenario, "Python", args.iterations, args.warmup, args.redis_url) + rust_result = await _benchmark_scenario(scenario, "Rust", args.iterations, args.warmup, args.redis_url) + speedup = python_result.mean_ms / rust_result.mean_ms if rust_result.mean_ms else 0.0 + print(f" Python: mean {python_result.mean_ms:.3f} ms | median {python_result.median_ms:.3f} ms | p95 {python_result.p95_ms:.3f} ms") + print(f" Rust: mean {rust_result.mean_ms:.3f} ms | median {rust_result.median_ms:.3f} ms | p95 {rust_result.p95_ms:.3f} ms") + print(f" Speedup: {speedup:.2f}x faster") + if baseline_result and baseline_result.mean_ms > 0: + py_overhead = python_result.mean_ms - baseline_result.mean_ms + rs_overhead = rust_result.mean_ms - baseline_result.mean_ms + print(f" Rate-limiter overhead: Python +{py_overhead:.3f} ms | Rust +{rs_overhead:.3f} ms") + print() + + +async def _run_throughput(args: argparse.Namespace, redis_enabled: bool) -> None: + """Run throughput-mode benchmarks at various concurrency levels. + + Uses asyncio.gather with a shared plugin to mirror production uvicorn + concurrency where multiple request handlers share the same plugin. + """ + concurrency_levels = [1, 4, 16, 64] + if args.concurrency: + concurrency_levels = [args.concurrency] + + iterations_per_task = max(100, args.iterations // 4) + + for algorithm in ("fixed_window",): # throughput mode uses one algorithm to keep output manageable + for backend in args.backends: + if backend == "redis" and not redis_enabled: + continue + hook = args.hooks[0] + scenario = Scenario(algorithm=algorithm, backend=backend, hook=hook, dimensions=args.dimensions, workload=args.workload) + + print("=" * 88) + label = f"THROUGHPUT: {algorithm} / {backend} / {hook}" + if scenario.dimensions > 1: + label += f" / {scenario.dimensions}d" + if scenario.workload != "allow": + label += f" / {scenario.workload}" + print(label) + print(f" ({iterations_per_task} iterations per task)") + print("=" * 88) + print(f" {'Tasks':>7} {'Python ops/s':>14} {'Rust ops/s':>14} {'Speedup':>8}") + print(f" {'-----':>7} {'-' * 14:>14} {'-' * 14:>14} {'--------':>8}") + + for concurrency in concurrency_levels: + py_result = await _benchmark_throughput(scenario, "Python", concurrency, iterations_per_task, args.redis_url) + rs_result = await _benchmark_throughput(scenario, "Rust", concurrency, iterations_per_task, args.redis_url) + speedup = rs_result.ops_per_sec / py_result.ops_per_sec if py_result.ops_per_sec else 0.0 + print(f" {concurrency:>7} {py_result.ops_per_sec:>14,.0f} {rs_result.ops_per_sec:>14,.0f} {speedup:>7.2f}x") + + print() + + +async def _run(args: argparse.Namespace) -> int: + """Run the benchmark suite.""" + redis_enabled = False + if "redis" in args.backends: + redis_enabled = await _redis_available(args.redis_url) + if not redis_enabled: + print(f" Redis unavailable at {args.redis_url}; skipping Redis scenarios") + + print("Rate Limiter Performance Comparison (Plugin Hook Path)") + print(f"Mode: {args.mode}") + print(f"Iterations: {args.iterations} (+ {args.warmup} warmup)") + print(f"Hooks: {', '.join(args.hooks)}") + print(f"Backends: {', '.join(args.backends)}") + print(f"Dimensions: {args.dimensions}") + print(f"Workload: {args.workload}") + if args.mode == "throughput": + print(f"Concurrency: {args.concurrency or '1,2,4,8'}") + print(f"Redis URL: {args.redis_url}") + print() + + # Parity checks + for algorithm in ("fixed_window", "sliding_window", "token_bucket"): + for backend in args.backends: + if backend == "redis" and not redis_enabled: + continue + await _parity_smoke_test(algorithm, backend, args.redis_url) + + print("Parity smoke checks: pass") + print() + + if args.mode == "latency": + await _run_latency(args, redis_enabled) + elif args.mode == "throughput": + await _run_throughput(args, redis_enabled) + + print("Comparison complete") + return 0 + + +def _parse_args() -> argparse.Namespace: + """Parse command-line flags.""" + parser = argparse.ArgumentParser(description="Rate limiter Python vs Rust hook-path benchmark") + parser.add_argument("--iterations", type=int, default=1000, help="Measured iterations per scenario (latency mode)") + parser.add_argument("--warmup", type=int, default=100, help="Warmup iterations per scenario (latency mode)") + parser.add_argument( + "--redis-url", + default="redis://localhost:6379/15", + help="Dedicated Redis URL for benchmark scenarios (defaults to DB 15)", + ) + parser.add_argument( + "--hooks", + nargs="+", + default=["prompt_pre_fetch", "tool_pre_invoke"], + choices=["prompt_pre_fetch", "tool_pre_invoke"], + help="Hooks to benchmark", + ) + parser.add_argument( + "--backends", + nargs="+", + default=["memory", "redis"], + choices=["memory", "redis"], + help="Backends to benchmark", + ) + parser.add_argument( + "--mode", + default="latency", + choices=["latency", "throughput"], + help="Benchmark mode: latency (sequential per-call) or throughput (concurrent ops/sec)", + ) + parser.add_argument( + "--dimensions", + type=int, + default=1, + choices=[1, 3], + help="Number of rate limit dimensions: 1 (user only) or 3 (user+tenant+tool)", + ) + parser.add_argument( + "--workload", + default="allow", + choices=["allow", "mixed"], + help="Workload type: allow (all requests pass) or mixed (some blocked)", + ) + parser.add_argument( + "--concurrency", + type=int, + default=None, + help="Thread count for throughput mode (default: sweep 1,2,4,8)", + ) + parser.add_argument( + "--baseline", + action="store_true", + default=False, + help="Include a baseline run (no rate limits) to measure plugin overhead", + ) + return parser.parse_args() + + +def main() -> int: + """Run the async benchmark entrypoint.""" + return asyncio.run(_run(_parse_args())) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/plugins_rust/rate_limiter/deny.toml b/plugins_rust/rate_limiter/deny.toml new file mode 100644 index 0000000000..142f5157ff --- /dev/null +++ b/plugins_rust/rate_limiter/deny.toml @@ -0,0 +1,27 @@ +# Cargo-deny config: license and policy checks for this crate. +# See https://embarkstudios.github.io/cargo-deny/ + +[licenses] +unused-allowed-license = "allow" +confidence-threshold = 0.95 +allow = [ + # Currently used across our Rust projects + "Apache-2.0", + "BSD-2-Clause", + "BSD-3-Clause", + "BSL-1.0", + "CC0-1.0", + "ISC", + "LGPL-2.1-or-later", + "MIT", + "MIT-0", + "OpenSSL", + "Unicode-3.0", + "Unicode-DFS-2016", + "Unlicense", + "Zlib", + # Common safe licenses in the Rust ecosystem + "0BSD", + "Apache-2.0 WITH LLVM-exception", + "Unicode-DFS-2015", +] diff --git a/plugins_rust/rate_limiter/pyproject.toml b/plugins_rust/rate_limiter/pyproject.toml new file mode 100644 index 0000000000..9265649248 --- /dev/null +++ b/plugins_rust/rate_limiter/pyproject.toml @@ -0,0 +1,22 @@ +[build-system] +requires = ["maturin>=1.4,<2.0"] +build-backend = "maturin" + +[project] +name = "mcpgateway-rate-limiter" +version = "0.1.0" +description = "High-performance rate limiter engine for MCP Gateway" +authors = [{ name = "ContextForge Contributors" }] +license = { text = "Apache-2.0" } +requires-python = ">=3.11" +classifiers = [ + "Programming Language :: Rust", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] + +[tool.maturin] +module-name = "rate_limiter_rust" +python-source = "python" +features = ["pyo3/extension-module"] diff --git a/plugins_rust/rate_limiter/python/rate_limiter_rust/__init__.pyi b/plugins_rust/rate_limiter/python/rate_limiter_rust/__init__.pyi new file mode 100644 index 0000000000..5082a9f06c --- /dev/null +++ b/plugins_rust/rate_limiter/python/rate_limiter_rust/__init__.pyi @@ -0,0 +1,140 @@ +# This file is automatically generated by pyo3_stub_gen +# ruff: noqa: E501, F401, F403, F405 + +import builtins +import typing +__all__ = [ + "EvalDimension", + "EvalResult", + "RateLimiterEngine", +] + +@typing.final +class EvalDimension: + r""" + The outcome of a single active dimension, exposed to Python for + per-dimension inspection (e.g. which dimension blocked the request). + """ + @property + def remaining(self) -> builtins.int: + r""" + Requests remaining for this active dimension. + """ + @property + def reset_timestamp(self) -> builtins.int: + r""" + Unix timestamp when this dimension resets or refills. + """ + @property + def retry_after(self) -> typing.Optional[builtins.int]: + r""" + Seconds until retry — populated only for blocked dimensions. + """ + +@typing.final +class EvalResult: + r""" + The aggregated result returned to Python via `evaluate_many()`. + + Contains the most restrictive outcome across all active dimensions + (min remaining, earliest unblock among blocked dimensions — matching + Python `_select_most_restrictive`). + """ + @property + def allowed(self) -> builtins.bool: + r""" + `True` if all active dimensions allow the request. + """ + @property + def limit(self) -> builtins.int: + r""" + Configured limit for the most restrictive active dimension. + """ + @property + def remaining(self) -> builtins.int: + r""" + Remaining requests for the most restrictive active dimension. + """ + @property + def reset_timestamp(self) -> builtins.int: + r""" + Unix timestamp when the most restrictive dimension resets. + """ + @property + def retry_after(self) -> typing.Optional[builtins.int]: + r""" + Seconds until reset — populated only when `allowed == False`. + """ + @property + def violated_dimensions(self) -> builtins.list[EvalDimension]: + r""" + Per-dimension outcomes that were blocked for this request. + """ + @property + def allowed_dimensions(self) -> builtins.list[EvalDimension]: + r""" + Per-dimension outcomes that still allowed this request. + """ + def __repr__(self) -> builtins.str: ... + +@typing.final +class RateLimiterEngine: + r""" + High-performance rate limiter engine. + + Construct once per plugin instance (`__init__`), then call + `check()` / `check_async()` on every hook invocation. + + Backend is selected at init time from the config dict: + - `backend: "memory"` (default) — in-process counting via `MemoryStore` + - `backend: "redis"` — Rust owns the Redis connection; same batch Lua + scripts as the Python `RedisBackend`, one EVAL per hook invocation + """ + def __new__(cls, config: dict) -> RateLimiterEngine: + r""" + Construct from the Python config dict. + + Parses all rate strings and normalises `by_tool` keys at init time — + never on the request path (IFACE-01, IFACE-05). + + Extra keys consumed here (not part of `EngineConfig`): + - `backend`: `"memory"` (default) or `"redis"` + - `redis_url`: required when `backend = "redis"` + - `redis_key_prefix`: key namespace prefix (default `"rl"`) + """ + def evaluate_many(self, checks: typing.Sequence[tuple[builtins.str, builtins.int, builtins.int]], now_unix: builtins.int) -> EvalResult: + r""" + Evaluate all active dimensions in a single call (ARCH-01, IFACE-02). + + `checks` is a list of `(key, limit_count, window_nanos)` tuples built + by the Python wrapper from the request context. + + `now_unix` is `int(time.time())` from Python — passing it here means + Python test mocks of `time.time()` propagate to header timestamps (CORR-02). + + Returns the most restrictive `EvalResult` across all dimensions (ARCH-02). + """ + def evaluate_many_async(self, checks: typing.Sequence[tuple[builtins.str, builtins.int, builtins.int]], now_unix: builtins.int) -> typing.Any: + r""" + Evaluate all active dimensions asynchronously. + + Intended for Redis-backed deployments so Python async hooks can await + the Rust Redis path without blocking the event loop. + """ + def check(self, user: builtins.str, tenant: typing.Optional[builtins.str], tool: builtins.str, now_unix: builtins.int, include_retry_after: builtins.bool) -> tuple[builtins.bool, dict, dict]: + r""" + High-level check: builds dimension keys internally, evaluates, and + returns pre-built Python dicts for headers and metadata. + + This eliminates all per-attribute PyO3 accesses on the Python side. + The Python wrapper calls this once per hook invocation instead of + `evaluate_many()` + `_rust_to_plugin_meta()` + `_rust_to_plugin_headers()`. + + Returns `(allowed, headers_dict, meta_dict)`. + """ + def check_async(self, user: builtins.str, tenant: typing.Optional[builtins.str], tool: builtins.str, now_unix: builtins.int, include_retry_after: builtins.bool) -> typing.Any: + r""" + Async variant of `check()` for Redis-backed deployments. + + Returns an awaitable that resolves to `(allowed, headers_dict, meta_dict)`. + """ diff --git a/plugins_rust/rate_limiter/src/bin/stub_gen.rs b/plugins_rust/rate_limiter/src/bin/stub_gen.rs new file mode 100644 index 0000000000..495186be9c --- /dev/null +++ b/plugins_rust/rate_limiter/src/bin/stub_gen.rs @@ -0,0 +1,13 @@ +// Copyright 2026 +// SPDX-License-Identifier: Apache-2.0 +// +// Generates Python type stubs (.pyi) for the rate_limiter_rust module. +// Run with: cargo run --bin stub_gen + +use rate_limiter_rust::stub_info; + +fn main() { + let stub_info = stub_info().expect("Failed to get stub info"); + stub_info.generate().expect("Failed to generate stub file"); + println!("✓ Generated stub files successfully"); +} diff --git a/plugins_rust/rate_limiter/src/clock.rs b/plugins_rust/rate_limiter/src/clock.rs new file mode 100644 index 0000000000..044e682fd7 --- /dev/null +++ b/plugins_rust/rate_limiter/src/clock.rs @@ -0,0 +1,193 @@ +// Copyright 2026 +// SPDX-License-Identifier: Apache-2.0 +// +// Clock abstraction for rate limiter engine. +// +// All internal rate math uses `Clock::now_monotonic()` (nanoseconds). +// Wall-clock Unix timestamps (for response headers) use `Clock::now_unix_secs()`. +// +// Tests inject `FakeClock` to make all timing-dependent assertions deterministic. + +/// Monotonic time in nanoseconds since an arbitrary epoch. +pub type Nanos = u64; + +/// Unix timestamp in whole seconds (for X-RateLimit-Reset headers). +pub type UnixSecs = i64; + +/// Clock abstraction injected into the engine at construction time. +pub trait Clock: Send + Sync + 'static { + /// Monotonic nanosecond counter — used for all rate math. + fn now_monotonic(&self) -> Nanos; + + /// Wall-clock Unix seconds — used only for header timestamps. + fn now_unix_secs(&self) -> UnixSecs; +} + +// --------------------------------------------------------------------------- +// Real clock — delegates to std::time +// --------------------------------------------------------------------------- + +/// Production clock backed by `std::time`. +#[derive(Debug, Clone, Default)] +pub struct SystemClock; + +impl Clock for SystemClock { + fn now_monotonic(&self) -> Nanos { + use std::sync::OnceLock; + use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + + // Instant is monotonic; we anchor it to a fixed start to get nanoseconds. + // We use a process-global anchor so monotonic values are comparable + // across threads — required because MemoryStore is shared via RwLock. + static ANCHOR: OnceLock<(Instant, u64)> = OnceLock::new(); + let (anchor_instant, anchor_nanos) = ANCHOR.get_or_init(|| { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or(Duration::ZERO) + .as_nanos() as u64; + (Instant::now(), nanos) + }); + let elapsed = anchor_instant.elapsed().as_nanos() as u64; + anchor_nanos + elapsed + } + + fn now_unix_secs(&self) -> UnixSecs { + use std::time::{Duration, SystemTime, UNIX_EPOCH}; + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or(Duration::ZERO) + .as_secs() as i64 + } +} + +// --------------------------------------------------------------------------- +// Fake clock — for deterministic tests +// --------------------------------------------------------------------------- + +use std::sync::Arc; +use std::sync::atomic::{AtomicI64, AtomicU64, Ordering}; + +/// Shareable handle to advance a `FakeClock` from test code. +#[derive(Clone, Debug)] +pub struct FakeClockHandle { + monotonic_nanos: Arc, + unix_secs: Arc, +} + +impl FakeClockHandle { + /// Advance the monotonic clock by `nanos` nanoseconds. + pub fn advance_nanos(&self, nanos: u64) { + self.monotonic_nanos.fetch_add(nanos, Ordering::SeqCst); + } + + /// Advance both clocks by `secs` seconds. + pub fn advance_secs(&self, secs: u64) { + self.monotonic_nanos + .fetch_add(secs * 1_000_000_000, Ordering::SeqCst); + self.unix_secs.fetch_add(secs as i64, Ordering::SeqCst); + } + + /// Set the Unix wall-clock to an absolute value (for header assertions). + pub fn set_unix_secs(&self, secs: i64) { + self.unix_secs.store(secs, Ordering::SeqCst); + } + + /// Read the current monotonic value. + pub fn monotonic_nanos(&self) -> u64 { + self.monotonic_nanos.load(Ordering::SeqCst) + } + + /// Read the current Unix seconds value. + pub fn unix_secs(&self) -> i64 { + self.unix_secs.load(Ordering::SeqCst) + } +} + +/// A `Clock` implementation driven by atomics — suitable for concurrent tests. +pub struct FakeClock { + monotonic_nanos: Arc, + unix_secs: Arc, +} + +impl FakeClock { + /// Create a `FakeClock` starting at the given Unix epoch and a matching + /// monotonic counter, returning both the clock and a control handle. + pub fn new(start_unix_secs: i64) -> (Self, FakeClockHandle) { + let mono = Arc::new(AtomicU64::new(start_unix_secs as u64 * 1_000_000_000)); + let wall = Arc::new(AtomicI64::new(start_unix_secs)); + let clock = FakeClock { + monotonic_nanos: Arc::clone(&mono), + unix_secs: Arc::clone(&wall), + }; + let handle = FakeClockHandle { + monotonic_nanos: mono, + unix_secs: wall, + }; + (clock, handle) + } +} + +impl Clock for FakeClock { + fn now_monotonic(&self) -> Nanos { + self.monotonic_nanos.load(Ordering::SeqCst) + } + + fn now_unix_secs(&self) -> UnixSecs { + self.unix_secs.load(Ordering::SeqCst) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn fake_clock_starts_at_given_epoch() { + let (clock, handle) = FakeClock::new(1_000_000); + assert_eq!(clock.now_unix_secs(), 1_000_000); + assert_eq!(clock.now_monotonic(), 1_000_000 * 1_000_000_000); + let _ = handle; + } + + #[test] + fn fake_clock_advances_in_sync() { + let (clock, handle) = FakeClock::new(1_000_000); + handle.advance_secs(60); + assert_eq!(clock.now_unix_secs(), 1_000_060); + assert_eq!(clock.now_monotonic(), (1_000_000 + 60) * 1_000_000_000); + } + + #[test] + fn fake_clock_advance_nanos_does_not_move_wall() { + let (clock, handle) = FakeClock::new(1_000_000); + handle.advance_nanos(500_000_000); // 0.5 s + assert_eq!(clock.now_unix_secs(), 1_000_000); // wall unchanged + assert_eq!( + clock.now_monotonic(), + 1_000_000 * 1_000_000_000 + 500_000_000 + ); + } + + #[test] + fn fake_clock_handle_clone_shares_state() { + let (clock, handle) = FakeClock::new(0); + let handle2 = handle.clone(); + handle2.advance_secs(10); + assert_eq!(clock.now_unix_secs(), 10); + assert_eq!(handle.unix_secs(), 10); + } + + #[test] + fn system_clock_monotonic_is_non_decreasing() { + let c = SystemClock; + let t1 = c.now_monotonic(); + let t2 = c.now_monotonic(); + assert!(t2 >= t1); + } + + #[test] + fn system_clock_unix_secs_is_positive() { + let c = SystemClock; + assert!(c.now_unix_secs() > 0); + } +} diff --git a/plugins_rust/rate_limiter/src/config.rs b/plugins_rust/rate_limiter/src/config.rs new file mode 100644 index 0000000000..0392b848ce --- /dev/null +++ b/plugins_rust/rate_limiter/src/config.rs @@ -0,0 +1,228 @@ +// Copyright 2026 +// SPDX-License-Identifier: Apache-2.0 +// +// Configuration types for the rate limiter engine. +// +// All rate strings are parsed once at engine init (`RateLimiterEngine::new`). +// The `by_tool` map is normalised (strip + lowercase) at init — never on the +// request path (IFACE-01, IFACE-05). + +use std::collections::HashMap; +use thiserror::Error; + +/// A parsed rate limit: `count` requests per `window_nanos` nanoseconds. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RateLimit { + pub count: u64, + pub window_nanos: u64, +} + +/// Errors that can occur while parsing config. +#[derive(Debug, Error)] +pub enum ConfigError { + #[error("invalid rate string {0:?}: expected \"/\" where unit is s/m/h")] + InvalidRateString(String), + #[error("rate count must be > 0, got {0}")] + ZeroCount(u64), + #[error( + "invalid algorithm {0:?}: expected \"fixed_window\", \"sliding_window\", or \"token_bucket\"" + )] + InvalidAlgorithm(String), +} + +/// Parse a rate string like `"30/m"`, `"100/s"`, `"1000/h"`. +/// +/// Accepted units (case-insensitive): `s`, `sec`, `second`, `m`, `min`, +/// `minute`, `h`, `hr`, `hour`. +pub fn parse_rate(s: &str) -> Result { + let s = s.trim(); + let (count_str, unit_str) = s + .split_once('/') + .ok_or_else(|| ConfigError::InvalidRateString(s.to_string()))?; + + let count: u64 = count_str + .trim() + .parse() + .map_err(|_| ConfigError::InvalidRateString(s.to_string()))?; + + if count == 0 { + return Err(ConfigError::ZeroCount(count)); + } + + let window_secs: u64 = match unit_str.trim().to_ascii_lowercase().as_str() { + "s" | "sec" | "second" => 1, + "m" | "min" | "minute" => 60, + "h" | "hr" | "hour" => 3600, + _ => return Err(ConfigError::InvalidRateString(s.to_string())), + }; + + Ok(RateLimit { + count, + window_nanos: window_secs * 1_000_000_000, + }) +} + +/// Which counting algorithm to use. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Algorithm { + FixedWindow, + SlidingWindow, + TokenBucket, +} + +impl Algorithm { + /// Parse an algorithm name from a string. + #[allow(clippy::should_implement_trait)] + pub fn from_str(s: &str) -> Option { + match s.trim().to_ascii_lowercase().as_str() { + "fixed_window" => Some(Self::FixedWindow), + "sliding_window" => Some(Self::SlidingWindow), + "token_bucket" => Some(Self::TokenBucket), + _ => None, + } + } +} + +/// Validated engine configuration, built from the raw Python dict. +#[derive(Debug, Clone)] +pub struct EngineConfig { + pub by_user: Option, + pub by_tenant: Option, + /// Normalised key → limit. Keys are already `.trim().to_lowercase()`. + pub by_tool: HashMap, + pub algorithm: Algorithm, +} + +impl EngineConfig { + /// Build from raw string fields (mirrors the Python `RateLimiterConfig` fields + /// that are relevant to the Rust engine — strict subset per IFACE-04). + pub fn new( + by_user: Option<&str>, + by_tenant: Option<&str>, + by_tool: HashMap, + algorithm: &str, + ) -> Result { + let by_user = by_user.map(parse_rate).transpose()?; + let by_tenant = by_tenant.map(parse_rate).transpose()?; + let by_tool = by_tool + .into_iter() + .map(|(k, v)| { + let normalised_key = k.trim().to_ascii_lowercase(); + parse_rate(&v).map(|limit| (normalised_key, limit)) + }) + .collect::, _>>()?; + let algorithm = Algorithm::from_str(algorithm) + .ok_or_else(|| ConfigError::InvalidAlgorithm(algorithm.to_string()))?; + Ok(Self { + by_user, + by_tenant, + by_tool, + algorithm, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // --- parse_rate --- + + #[test] + fn parse_rate_seconds_short() { + let r = parse_rate("10/s").unwrap(); + assert_eq!(r.count, 10); + assert_eq!(r.window_nanos, 1_000_000_000); + } + + #[test] + fn parse_rate_minutes_short() { + let r = parse_rate("30/m").unwrap(); + assert_eq!(r.count, 30); + assert_eq!(r.window_nanos, 60 * 1_000_000_000); + } + + #[test] + fn parse_rate_hours_long() { + let r = parse_rate("1000/hour").unwrap(); + assert_eq!(r.count, 1000); + assert_eq!(r.window_nanos, 3600 * 1_000_000_000); + } + + #[test] + fn parse_rate_whitespace_stripped() { + let r = parse_rate(" 5 / min ").unwrap(); + assert_eq!(r.count, 5); + } + + #[test] + fn parse_rate_unsupported_unit_errors() { + assert!(parse_rate("10/day").is_err()); + } + + #[test] + fn parse_rate_no_slash_errors() { + assert!(parse_rate("10m").is_err()); + } + + #[test] + fn parse_rate_zero_count_errors() { + assert!(parse_rate("0/s").is_err()); + } + + // --- Algorithm::from_str --- + + #[test] + fn algorithm_from_str_all_variants() { + assert_eq!( + Algorithm::from_str("fixed_window"), + Some(Algorithm::FixedWindow) + ); + assert_eq!( + Algorithm::from_str("sliding_window"), + Some(Algorithm::SlidingWindow) + ); + assert_eq!( + Algorithm::from_str("token_bucket"), + Some(Algorithm::TokenBucket) + ); + assert_eq!(Algorithm::from_str("unknown"), None); + } + + // --- EngineConfig --- + + #[test] + fn engine_config_parses_all_fields() { + let mut by_tool = HashMap::new(); + by_tool.insert("Search".to_string(), "10/m".to_string()); + by_tool.insert(" Summarise ".to_string(), "5/m".to_string()); + + let cfg = EngineConfig::new(Some("30/m"), Some("300/m"), by_tool, "fixed_window").unwrap(); + + assert_eq!(cfg.by_user.unwrap().count, 30); + assert_eq!(cfg.by_tenant.unwrap().count, 300); + // Keys must be normalised + assert!(cfg.by_tool.contains_key("search")); + assert!(cfg.by_tool.contains_key("summarise")); + assert!(!cfg.by_tool.contains_key("Search")); + assert_eq!(cfg.algorithm, Algorithm::FixedWindow); + } + + #[test] + fn engine_config_all_none_is_valid() { + let cfg = EngineConfig::new(None, None, HashMap::new(), "sliding_window").unwrap(); + assert!(cfg.by_user.is_none()); + assert!(cfg.by_tenant.is_none()); + assert!(cfg.by_tool.is_empty()); + } + + #[test] + fn engine_config_invalid_rate_propagates_error() { + assert!(EngineConfig::new(Some("bad"), None, HashMap::new(), "fixed_window").is_err()); + } + + #[test] + fn engine_config_invalid_algorithm_propagates_error() { + assert!(EngineConfig::new(None, None, HashMap::new(), "leaky_bucket").is_err()); + } +} diff --git a/plugins_rust/rate_limiter/src/engine.rs b/plugins_rust/rate_limiter/src/engine.rs new file mode 100644 index 0000000000..698b1b2bc3 --- /dev/null +++ b/plugins_rust/rate_limiter/src/engine.rs @@ -0,0 +1,615 @@ +// Copyright 2026 +// SPDX-License-Identifier: Apache-2.0 +// +// `RateLimiterEngine` — the single PyO3-exposed class (IFACE-02). +// +// Python calls `check(user, tenant, tool, now_unix)` once per hook +// invocation (ARCH-01). The engine builds dimension keys, evaluates, +// aggregates, and returns pre-built header/meta dicts (ARCH-02). +// The Python wrapper is policy-only and never does rate math (ARCH-03). +// +// The older `evaluate_many()` / `evaluate_many_async()` entry points are +// retained for backward compatibility and test use but are not on the +// production hot path. + +use std::collections::HashMap; +use std::sync::Arc; + +use log::warn; +use pyo3::prelude::*; +use pyo3::types::{PyDict, PyList}; + +use pyo3_async_runtimes::tokio::future_into_py; +use pyo3_stub_gen::derive::*; + +use crate::clock::{Clock, SystemClock}; +use crate::config::{ConfigError, EngineConfig}; +use crate::memory::MemoryStore; +use crate::redis_backend::RedisRateLimiter; +use crate::types::{DimResult, EvalResult}; + +// --------------------------------------------------------------------------- +// Backend selection +// --------------------------------------------------------------------------- + +#[derive(Clone)] +enum EngineBackend { + Memory(Arc), + Redis(Arc), +} + +// --------------------------------------------------------------------------- +// Engine +// --------------------------------------------------------------------------- + +/// High-performance rate limiter engine. +/// +/// Construct once per plugin instance (`__init__`), then call +/// `check()` / `check_async()` on every hook invocation. +/// +/// Backend is selected at init time from the config dict: +/// - `backend: "memory"` (default) — in-process counting via `MemoryStore` +/// - `backend: "redis"` — Rust owns the Redis connection; same batch Lua +/// scripts as the Python `RedisBackend`, one EVAL per hook invocation +#[gen_stub_pyclass] +#[pyclass] +pub struct RateLimiterEngine { + config: EngineConfig, + backend: EngineBackend, + clock: Arc, +} + +impl RateLimiterEngine { + /// Internal constructor — always uses the memory backend. + /// Used by tests and benchmarks where clock injection is required. + pub fn new_with_clock(config: EngineConfig, clock: Arc) -> Self { + Self { + backend: EngineBackend::Memory(Arc::new(MemoryStore::new())), + config, + clock, + } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl RateLimiterEngine { + /// Construct from the Python config dict. + /// + /// Parses all rate strings and normalises `by_tool` keys at init time — + /// never on the request path (IFACE-01, IFACE-05). + /// + /// Extra keys consumed here (not part of `EngineConfig`): + /// - `backend`: `"memory"` (default) or `"redis"` + /// - `redis_url`: required when `backend = "redis"` + /// - `redis_key_prefix`: key namespace prefix (default `"rl"`) + #[new] + pub fn new(config: &Bound<'_, PyDict>) -> PyResult { + let by_user: Option = match config.get_item("by_user")? { + Some(v) if !v.is_none() => Some(v.extract::().map_err(|_| { + pyo3::exceptions::PyValueError::new_err("by_user must be a string like '60/m'") + })?), + _ => None, + }; + let by_tenant: Option = match config.get_item("by_tenant")? { + Some(v) if !v.is_none() => Some(v.extract::().map_err(|_| { + pyo3::exceptions::PyValueError::new_err("by_tenant must be a string like '600/m'") + })?), + _ => None, + }; + let algorithm: String = match config.get_item("algorithm")? { + Some(v) if !v.is_none() => v.extract::().map_err(|_| { + pyo3::exceptions::PyValueError::new_err( + "algorithm must be a string ('fixed_window', 'sliding_window', or 'token_bucket')", + ) + })?, + _ => "fixed_window".to_string(), + }; + + let by_tool: HashMap = match config.get_item("by_tool")? { + Some(v) if !v.is_none() => v.extract::>().map_err(|_| { + pyo3::exceptions::PyValueError::new_err( + "by_tool must be a dict of {tool_name: rate_string}", + ) + })?, + _ => HashMap::new(), + }; + + let engine_config = EngineConfig::new( + by_user.as_deref(), + by_tenant.as_deref(), + by_tool, + &algorithm, + ) + .map_err(|e: ConfigError| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + + let backend_str: String = config + .get_item("backend")? + .and_then(|v| v.extract().ok()) + .unwrap_or_else(|| "memory".to_string()); + + let backend = if backend_str == "redis" { + let redis_url: String = config + .get_item("redis_url")? + .and_then(|v| v.extract().ok()) + .ok_or_else(|| { + pyo3::exceptions::PyValueError::new_err( + "redis_url is required when backend=redis", + ) + })?; + let prefix: String = config + .get_item("redis_key_prefix")? + .and_then(|v| v.extract().ok()) + .unwrap_or_else(|| "rl".to_string()); + let redis_limiter = RedisRateLimiter::new(&redis_url, engine_config.algorithm, prefix) + .map_err(|e| { + warn!("Rust rate limiter: Redis backend init failed: {}", e); + pyo3::exceptions::PyRuntimeError::new_err(e.to_string()) + })?; + EngineBackend::Redis(Arc::new(redis_limiter)) + } else { + EngineBackend::Memory(Arc::new(MemoryStore::new())) + }; + + Ok(Self { + config: engine_config, + backend, + clock: Arc::new(SystemClock), + }) + } + + /// Evaluate all active dimensions in a single call (ARCH-01, IFACE-02). + /// + /// `checks` is a list of `(key, limit_count, window_nanos)` tuples built + /// by the Python wrapper from the request context. + /// + /// `now_unix` is `int(time.time())` from Python — passing it here means + /// Python test mocks of `time.time()` propagate to header timestamps (CORR-02). + /// + /// Returns the most restrictive `EvalResult` across all dimensions (ARCH-02). + /// + /// **Warning:** For the Redis backend, this method calls `block_on` on a + /// dedicated Tokio runtime. It must not be called from within an existing + /// Tokio runtime (e.g. from `pyo3-async-runtimes` worker threads) or it + /// will panic. Use `evaluate_many_async` for async contexts instead. + pub fn evaluate_many( + &self, + checks: Vec<(String, u64, u64)>, + now_unix: i64, + ) -> PyResult { + let dim_results = eval_dims_sync( + &self.backend, + self.config.algorithm, + &self.clock, + checks, + now_unix, + )?; + Ok(EvalResult::from_dims(&dim_results)) + } + + /// Evaluate all active dimensions asynchronously. + /// + /// Intended for Redis-backed deployments so Python async hooks can await + /// the Rust Redis path without blocking the event loop. + pub fn evaluate_many_async<'py>( + &self, + py: Python<'py>, + checks: Vec<(String, u64, u64)>, + now_unix: i64, + ) -> PyResult> { + let backend = self.backend.clone(); + let algorithm = self.config.algorithm; + let clock = Arc::clone(&self.clock); + + future_into_py(py, async move { + let dim_results = eval_dims_async(backend, algorithm, clock, checks, now_unix).await?; + Python::attach(|py| Py::new(py, EvalResult::from_dims(&dim_results))) + }) + } + + /// High-level check: builds dimension keys internally, evaluates, and + /// returns pre-built Python dicts for headers and metadata. + /// + /// This eliminates all per-attribute PyO3 accesses on the Python side. + /// The Python wrapper calls this once per hook invocation instead of + /// `evaluate_many()` + `_rust_to_plugin_meta()` + `_rust_to_plugin_headers()`. + /// + /// Returns `(allowed, headers_dict, meta_dict)`. + pub fn check<'py>( + &self, + py: Python<'py>, + user: &str, + tenant: Option<&str>, + tool: &str, + now_unix: i64, + include_retry_after: bool, + ) -> PyResult<(bool, Bound<'py, PyDict>, Bound<'py, PyDict>)> { + let checks = self.build_checks(user, tenant, tool); + if checks.is_empty() { + let headers = PyDict::new(py); + let meta = PyDict::new(py); + meta.set_item("limited", false)?; + return Ok((true, headers, meta)); + } + + let dim_results = eval_dims_sync( + &self.backend, + self.config.algorithm, + &self.clock, + checks, + now_unix, + )?; + + let eval = EvalResult::from_dims(&dim_results); + let headers = build_headers_dict(py, &eval, include_retry_after)?; + let meta = build_meta_dict(py, &eval, now_unix)?; + Ok((eval.allowed, headers, meta)) + } + + /// Async variant of `check()` for Redis-backed deployments. + /// + /// Returns an awaitable that resolves to `(allowed, headers_dict, meta_dict)`. + pub fn check_async<'py>( + &self, + py: Python<'py>, + user: &str, + tenant: Option<&str>, + tool: &str, + now_unix: i64, + include_retry_after: bool, + ) -> PyResult> { + let checks = self.build_checks(user, tenant, tool); + if checks.is_empty() { + return future_into_py(py, async move { + Python::attach(|py| -> PyResult> { + let headers = PyDict::new(py); + let meta = PyDict::new(py); + meta.set_item("limited", false)?; + let tup = pyo3::types::PyTuple::new( + py, + [ + true.into_pyobject(py)?.to_owned().into_any(), + headers.into_any(), + meta.into_any(), + ], + )?; + Ok(tup.into()) + }) + }); + } + + let backend = self.backend.clone(); + let algorithm = self.config.algorithm; + let clock = Arc::clone(&self.clock); + + future_into_py(py, async move { + let dim_results = eval_dims_async(backend, algorithm, clock, checks, now_unix).await?; + + let eval = EvalResult::from_dims(&dim_results); + Python::attach(|py| -> PyResult> { + let headers = build_headers_dict(py, &eval, include_retry_after)?; + let meta = build_meta_dict(py, &eval, now_unix)?; + let tup = pyo3::types::PyTuple::new( + py, + [ + eval.allowed.into_pyobject(py)?.to_owned().into_any(), + headers.into_any(), + meta.into_any(), + ], + )?; + Ok(tup.into()) + }) + }) + } +} + +// --------------------------------------------------------------------------- +// Shared dimension evaluation — used by evaluate_many, check (sync + async) +// --------------------------------------------------------------------------- + +/// Evaluate dimension checks synchronously (memory: GIL-released, Redis: block_on). +fn eval_dims_sync( + backend: &EngineBackend, + algorithm: crate::config::Algorithm, + clock: &Arc, + checks: Vec<(String, u64, u64)>, + now_unix: i64, +) -> PyResult> { + Python::attach(|py| { + py.detach(|| -> Result, String> { + eval_dims_inner(backend, algorithm, clock, checks, now_unix) + }) + .map_err(pyo3::exceptions::PyRuntimeError::new_err) + }) +} + +/// Evaluate dimension checks asynchronously (memory: direct, Redis: async). +async fn eval_dims_async( + backend: EngineBackend, + algorithm: crate::config::Algorithm, + clock: Arc, + checks: Vec<(String, u64, u64)>, + now_unix: i64, +) -> PyResult> { + match backend { + EngineBackend::Memory(store) => { + let now_mono = clock.now_monotonic(); + Ok(eval_dims_memory( + &store, algorithm, checks, now_mono, now_unix, + )) + } + EngineBackend::Redis(redis) => redis + .evaluate_many_async(&checks, now_unix) + .await + .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string())), + } +} + +/// Backend dispatch for synchronous evaluation (called inside `py.detach`). +fn eval_dims_inner( + backend: &EngineBackend, + algorithm: crate::config::Algorithm, + clock: &Arc, + checks: Vec<(String, u64, u64)>, + now_unix: i64, +) -> Result, String> { + match backend { + EngineBackend::Memory(store) => { + let now_mono = clock.now_monotonic(); + Ok(eval_dims_memory( + store, algorithm, checks, now_mono, now_unix, + )) + } + EngineBackend::Redis(redis) => redis + .evaluate_many(&checks, now_unix) + .map_err(|e| e.to_string()), + } +} + +/// Evaluate checks against the in-memory store. +fn eval_dims_memory( + store: &MemoryStore, + algorithm: crate::config::Algorithm, + checks: Vec<(String, u64, u64)>, + now_mono: crate::clock::Nanos, + now_unix: i64, +) -> Vec { + checks + .into_iter() + .map(|(key, limit_count, window_nanos)| { + store.check_and_increment( + &key, + limit_count, + window_nanos, + algorithm, + now_mono, + now_unix, + ) + }) + .collect() +} + +// --------------------------------------------------------------------------- +// Private helpers — dimension key building and dict construction +// --------------------------------------------------------------------------- + +impl RateLimiterEngine { + /// Build dimension checks from engine config. + /// Mirrors Python `_build_rust_checks()` but runs in Rust. + fn build_checks( + &self, + user: &str, + tenant: Option<&str>, + tool: &str, + ) -> Vec<(String, u64, u64)> { + let mut checks = Vec::with_capacity(3); + if let Some(ref rl) = self.config.by_user { + checks.push((format!("user:{}", user), rl.count, rl.window_nanos)); + } + if let (Some(t), Some(rl)) = (tenant, &self.config.by_tenant) { + checks.push((format!("tenant:{}", t), rl.count, rl.window_nanos)); + } + // Tool names are normalised (lowercase) in EngineConfig at init time. + // Defensive lowercase here to avoid silent mismatches if caller forgets. + let tool_lower = tool.to_ascii_lowercase(); + if let Some(rl) = self.config.by_tool.get(&tool_lower) { + checks.push((format!("tool:{}", tool_lower), rl.count, rl.window_nanos)); + } + checks + } +} + +/// Build HTTP rate-limit headers dict — mirrors Python `_make_headers()`. +fn build_headers_dict<'py>( + py: Python<'py>, + eval: &EvalResult, + include_retry_after: bool, +) -> PyResult> { + let headers = PyDict::new(py); + if eval.limit == u64::MAX { + return Ok(headers); + } + headers.set_item("X-RateLimit-Limit", eval.limit.to_string())?; + headers.set_item("X-RateLimit-Remaining", eval.remaining.to_string())?; + headers.set_item("X-RateLimit-Reset", eval.reset_timestamp.to_string())?; + if include_retry_after && let Some(retry) = eval.retry_after { + headers.set_item("Retry-After", retry.to_string())?; + } + Ok(headers) +} + +/// Build metadata dict — mirrors Python `_rust_to_plugin_meta()`. +fn build_meta_dict<'py>( + py: Python<'py>, + eval: &EvalResult, + now_unix: i64, +) -> PyResult> { + let meta = PyDict::new(py); + let reset_in = eval + .retry_after + .unwrap_or_else(|| (eval.reset_timestamp - now_unix).max(0)); + // "limited" means rate limits are configured, not that the request was blocked. + meta.set_item("limited", true)?; + meta.set_item("remaining", eval.remaining)?; + meta.set_item("reset_in", reset_in)?; + + let has_violated = !eval.violated_dimensions.is_empty(); + let has_allowed = !eval.allowed_dimensions.is_empty(); + + if has_violated || has_allowed { + let dims = PyDict::new(py); + if has_violated { + let violated_list = PyList::empty(py); + for dim in &eval.violated_dimensions { + let d = PyDict::new(py); + let dim_reset_in = dim + .retry_after + .unwrap_or_else(|| (dim.reset_timestamp - now_unix).max(0)); + d.set_item("limited", true)?; + d.set_item("remaining", dim.remaining)?; + d.set_item("reset_in", dim_reset_in)?; + violated_list.append(d)?; + } + dims.set_item("violated", violated_list)?; + } + if has_allowed { + let allowed_list = PyList::empty(py); + for dim in &eval.allowed_dimensions { + let d = PyDict::new(py); + let dim_reset_in = (dim.reset_timestamp - now_unix).max(0); + d.set_item("limited", true)?; + d.set_item("remaining", dim.remaining)?; + d.set_item("reset_in", dim_reset_in)?; + allowed_list.append(d)?; + } + dims.set_item("allowed", allowed_list)?; + } + meta.set_item("dimensions", dims)?; + } + + Ok(meta) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::clock::FakeClock; + use crate::config::Algorithm; + + fn init_python() { + Python::initialize(); + } + + fn engine_with_fake_clock( + by_user: Option<&str>, + algorithm: Algorithm, + ) -> (RateLimiterEngine, crate::clock::FakeClockHandle) { + init_python(); + let (clock, handle) = FakeClock::new(1_000_000); + let mut by_tool = HashMap::new(); + let cfg = EngineConfig { + by_user: by_user.map(|s| crate::config::parse_rate(s).unwrap()), + by_tenant: None, + by_tool: { + by_tool.insert( + "search".to_string(), + crate::config::parse_rate("5/m").unwrap(), + ); + by_tool + }, + algorithm, + }; + let engine = RateLimiterEngine::new_with_clock(cfg, Arc::new(clock)); + (engine, handle) + } + + // --- IFACE-01: config parsed at init --- + + #[test] + fn config_parsed_at_init_by_tool_normalised() { + let cfg = EngineConfig::new( + Some("10/s"), + None, + { + let mut m = HashMap::new(); + m.insert("Search".to_string(), "5/m".to_string()); + m + }, + "fixed_window", + ) + .unwrap(); + // Key must be lowercase + assert!(cfg.by_tool.contains_key("search")); + assert!(!cfg.by_tool.contains_key("Search")); + } + + // --- IFACE-02: evaluate_many returns EvalResult --- + + #[test] + fn evaluate_many_returns_eval_result_shape() { + let (engine, handle) = engine_with_fake_clock(Some("10/s"), Algorithm::FixedWindow); + let checks = vec![("user:alice".to_string(), 10, 1_000_000_000)]; + let result = engine.evaluate_many(checks, handle.unix_secs()).unwrap(); + // Shape: all fields present, first call always allowed + assert!(result.allowed); + assert_eq!(result.limit, 10); + assert!(result.remaining > 0); + assert!(result.retry_after.is_none()); + } + + // --- ARCH-01: evaluate_many is the only hot-path call --- + // (Structural — enforced by the interface: Python has no other method to call) + + // --- CORR-03: reset_timestamp > now on allowed requests --- + + #[test] + fn reset_timestamp_strictly_greater_than_now_on_allowed() { + let (engine, handle) = engine_with_fake_clock(Some("10/s"), Algorithm::FixedWindow); + let now_unix = handle.unix_secs(); + let checks = vec![("user:bob".to_string(), 10, 1_000_000_000)]; + let result = engine.evaluate_many(checks, now_unix).unwrap(); + assert!(result.allowed); + assert!( + result.reset_timestamp > now_unix, + "reset_timestamp {} must be > now {}", + result.reset_timestamp, + now_unix + ); + } + + // --- CORR-04: None tenant means no tenant check --- + // (Structural — Python wrapper never adds a tenant check when tenant_id is None) + + // --- CORR-07: multi-dimension aggregation picks most restrictive --- + + #[test] + fn evaluate_many_blocked_dimension_blocks_result() { + let (engine, _handle) = engine_with_fake_clock(Some("2/s"), Algorithm::FixedWindow); + // Exhaust the limit + let checks = || vec![("user:carol".to_string(), 2, 1_000_000_000)]; + let _ = engine.evaluate_many(checks(), 1_000_000).unwrap(); // 1 + let _ = engine.evaluate_many(checks(), 1_000_000).unwrap(); // 2 + let result = engine.evaluate_many(checks(), 1_000_000).unwrap(); // 3 — must be blocked + assert!(!result.allowed); + assert_eq!(result.remaining, 0); + assert!(result.retry_after.is_some()); + } + + #[test] + fn evaluate_many_multiple_dims_picks_most_restrictive() { + let (engine, _handle) = engine_with_fake_clock(None, Algorithm::FixedWindow); + // user has 10/s, tenant has 2/s — after 2 requests tenant is exhausted + let user_key = "user:dave".to_string(); + let tenant_key = "tenant:acme".to_string(); + let checks = || { + vec![ + (user_key.clone(), 10, 1_000_000_000), + (tenant_key.clone(), 2, 1_000_000_000), + ] + }; + let _ = engine.evaluate_many(checks(), 1_000_000).unwrap(); + let _ = engine.evaluate_many(checks(), 1_000_000).unwrap(); + let result = engine.evaluate_many(checks(), 1_000_000).unwrap(); + assert!(!result.allowed); // tenant exhausted → blocked + } +} diff --git a/plugins_rust/rate_limiter/src/lib.rs b/plugins_rust/rate_limiter/src/lib.rs new file mode 100644 index 0000000000..5e3dcd25a2 --- /dev/null +++ b/plugins_rust/rate_limiter/src/lib.rs @@ -0,0 +1,36 @@ +// Copyright 2026 +// SPDX-License-Identifier: Apache-2.0 +// +// Rate Limiter Engine — Rust implementation. +// +// Exposed to Python via PyO3. One public class: `RateLimiterEngine`. +// One public hot-path method: `evaluate_many()` (ARCH-01, IFACE-02). + +use pyo3::prelude::*; +use pyo3_stub_gen::define_stub_info_gatherer; + +pub mod clock; +pub mod config; +pub mod engine; +pub mod memory; +pub mod redis_backend; +pub mod types; + +pub use engine::RateLimiterEngine; +pub use types::{EvalDimension, EvalResult}; + +/// Python module definition. +#[pymodule] +fn rate_limiter_rust(m: &Bound<'_, PyModule>) -> PyResult<()> { + // Bridge Rust `log` macros into Python's `logging` module so Rust + // engine messages appear in the same log stream as the Python plugin. + pyo3_log::init(); + + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} + +// Generate Python type stubs (.pyi files). +define_stub_info_gatherer!(stub_info); diff --git a/plugins_rust/rate_limiter/src/memory.rs b/plugins_rust/rate_limiter/src/memory.rs new file mode 100644 index 0000000000..1bc31529f6 --- /dev/null +++ b/plugins_rust/rate_limiter/src/memory.rs @@ -0,0 +1,709 @@ +// Copyright 2026 +// SPDX-License-Identifier: Apache-2.0 +// +// In-process memory backend for the rate limiter engine. +// +// Per-key locking via `parking_lot::RwLock` — no single global lock (MEM-01). +// Typed key is the raw string passed from the engine; callers are responsible +// for constructing distinct keys per dimension (e.g. "user:alice", "tenant:acme"). +// +// Algorithms implemented: +// - FixedWindow (MEM-02): HashMap +// - SlidingWindow (MEM-03): HashMap> +// - TokenBucket (MEM-04): HashMap +// +// Cleanup is amortized on access — no background sweep thread (MEM-05). +// Idle key eviction runs every ~128 calls to reclaim memory (MEM-06). + +use std::collections::{HashMap, VecDeque}; +use std::sync::atomic::{AtomicU64, Ordering}; + +use parking_lot::RwLock; + +use crate::clock::{Nanos, UnixSecs}; +use crate::config::Algorithm; +use crate::types::DimResult; + +/// How often (in calls) the amortized sweep runs. Power of 2 for cheap +/// modulo via bitwise AND. 128 means ~0.8% of calls pay the sweep cost. +const SWEEP_INTERVAL: u64 = 128; + +/// Token bucket keys inactive for longer than this are evicted (1 hour in +/// nanos). Matches the Python `TokenBucketAlgorithm.sweep` threshold. +const TOKEN_BUCKET_STALE_NANOS: u64 = 3_600_000_000_000; + +// --------------------------------------------------------------------------- +// Per-key state +// --------------------------------------------------------------------------- + +#[derive(Debug)] +enum KeyState { + FixedWindow { + count: u64, + window_start: Nanos, + /// Unix timestamp when the window started — used to compute a constant + /// reset_timestamp within the window (matching Python backend behaviour). + window_start_unix: UnixSecs, + /// Window duration in nanos — stored per key so sweep can evict at the + /// actual window boundary instead of using a hardcoded 1-hour threshold. + window_nanos: Nanos, + }, + SlidingWindow { + timestamps: VecDeque, + /// Window duration in nanos — stored per key so sweep can drain stale + /// timestamps and evict idle keys without waiting for the next access. + window_nanos: Nanos, + }, + TokenBucket { + /// Tokens × 1000 to avoid floating-point (CORR-05). + tokens_milli: u64, + last_refill: Nanos, + }, +} + +// --------------------------------------------------------------------------- +// MemoryStore +// --------------------------------------------------------------------------- + +pub struct MemoryStore { + inner: RwLock>>, + call_count: AtomicU64, +} + +impl Default for MemoryStore { + fn default() -> Self { + Self::new() + } +} + +impl MemoryStore { + pub fn new() -> Self { + Self { + inner: RwLock::new(HashMap::new()), + call_count: AtomicU64::new(0), + } + } + + /// Amortized sweep: remove keys whose state is stale (MEM-06). + /// + /// - FixedWindow: evict if the configured window has fully elapsed. + /// - SlidingWindow: drain stale timestamps, then evict if the deque is empty. + /// - TokenBucket: inactive for > 1 hour (matching Python `TokenBucketAlgorithm.sweep`). + fn sweep(&self, now_mono: Nanos) { + let mut write = self.inner.write(); + write.retain(|_key, key_lock| { + // Skip keys that are currently write-locked (actively being used). + let mut state = match key_lock.try_write() { + Some(guard) => guard, + None => return true, // contended — keep + }; + match &mut *state { + KeyState::FixedWindow { + window_start, + window_nanos, + .. + } => { + // Evict if the configured window has fully elapsed. + now_mono.saturating_sub(*window_start) < *window_nanos + } + KeyState::SlidingWindow { + timestamps, + window_nanos, + } => { + // Drain stale timestamps that have fallen outside the window, + // then evict if the deque is empty. This reclaims keys that + // went cold after traffic — previously they lingered forever + // because stale timestamps were only drained on access. + let cutoff = now_mono.saturating_sub(*window_nanos); + while timestamps.front().is_some_and(|&t| t <= cutoff) { + timestamps.pop_front(); + } + !timestamps.is_empty() + } + KeyState::TokenBucket { last_refill, .. } => { + // Evict if inactive for more than 1 hour. + now_mono.saturating_sub(*last_refill) < TOKEN_BUCKET_STALE_NANOS + } + } + }); + } + + /// Check the rate for `key` and increment the counter if allowed. + /// + /// Returns a `DimResult` with allow/block, remaining, reset_timestamp, + /// and retry_after. All timing uses the injected `now_mono` and `now_unix` + /// values — no direct clock calls inside this function (CORR-06). + pub fn check_and_increment( + &self, + key: &str, + limit: u64, + window_nanos: u64, + algorithm: Algorithm, + now_mono: Nanos, + now_unix: UnixSecs, + ) -> DimResult { + // Fast path: key already exists — single read lock on outer map. + let result = { + let read = self.inner.read(); + if let Some(key_lock) = read.get(key) { + let mut state = key_lock.write(); + Some(evaluate_state( + &mut state, + limit, + window_nanos, + now_mono, + now_unix, + )) + } else { + None + } + }; + + let result = result.unwrap_or_else(|| { + // Slow path: key missing — write lock to insert, then evaluate. + // Only runs on first access per key; steady-state always hits fast path. + let mut write = self.inner.write(); + let key_lock = write.entry(key.to_string()).or_insert_with(|| { + RwLock::new(new_key_state( + algorithm, + limit, + window_nanos, + now_mono, + now_unix, + )) + }); + let mut state = key_lock.write(); + evaluate_state(&mut state, limit, window_nanos, now_mono, now_unix) + }); + + // All locks dropped — amortized sweep (MEM-06). + let n = self.call_count.fetch_add(1, Ordering::Relaxed); + if n & (SWEEP_INTERVAL - 1) == 0 && n > 0 { + self.sweep(now_mono); + } + result + } +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/// Create the initial key state for a new rate-limit key. +fn new_key_state( + algorithm: Algorithm, + limit: u64, + window_nanos: u64, + now_mono: Nanos, + now_unix: UnixSecs, +) -> KeyState { + match algorithm { + Algorithm::FixedWindow => KeyState::FixedWindow { + count: 0, + window_start: now_mono, + window_start_unix: now_unix, + window_nanos, + }, + Algorithm::SlidingWindow => KeyState::SlidingWindow { + timestamps: VecDeque::new(), + window_nanos, + }, + Algorithm::TokenBucket => KeyState::TokenBucket { + tokens_milli: limit.saturating_mul(1000), + last_refill: now_mono, + }, + } +} + +/// Dispatch to the correct algorithm based on the key state variant. +fn evaluate_state( + state: &mut KeyState, + limit: u64, + window_nanos: u64, + now_mono: Nanos, + now_unix: UnixSecs, +) -> DimResult { + match state { + KeyState::FixedWindow { + count, + window_start, + window_start_unix, + .. + } => fixed_window( + count, + window_start, + window_start_unix, + limit, + window_nanos, + now_mono, + now_unix, + ), + KeyState::SlidingWindow { timestamps, .. } => { + sliding_window(timestamps, limit, window_nanos, now_mono, now_unix) + } + KeyState::TokenBucket { + tokens_milli, + last_refill, + } => token_bucket( + tokens_milli, + last_refill, + limit, + window_nanos, + now_mono, + now_unix, + ), + } +} + +// --------------------------------------------------------------------------- +// Algorithm implementations +// --------------------------------------------------------------------------- + +fn fixed_window( + count: &mut u64, + window_start: &mut Nanos, + window_start_unix: &mut UnixSecs, + limit: u64, + window_nanos: u64, + now_mono: Nanos, + now_unix: UnixSecs, +) -> DimResult { + // Reset if window has elapsed (amortized cleanup, MEM-05). + if now_mono.saturating_sub(*window_start) >= window_nanos { + *count = 0; + *window_start = now_mono; + *window_start_unix = now_unix; + } + + // At least 1 second so reset_timestamp is always in the future, even if + // window_nanos < 1 billion (sub-second window — currently unreachable via + // config parsing but guarded defensively). + let window_secs = (window_nanos / 1_000_000_000).max(1) as i64; + // Constant within a window — matches Python backend behaviour (CORR-02). + let reset_timestamp = *window_start_unix + window_secs; + + if *count < limit { + *count += 1; + let remaining = limit - *count; + DimResult { + allowed: true, + limit, + remaining, + reset_timestamp, + retry_after: None, + } + } else { + let elapsed_nanos = now_mono.saturating_sub(*window_start); + let remaining_nanos = window_nanos.saturating_sub(elapsed_nanos); + let retry_after = (remaining_nanos / 1_000_000_000) as i64; + DimResult { + allowed: false, + limit, + remaining: 0, + reset_timestamp, + retry_after: Some(retry_after.max(1)), + } + } +} + +fn sliding_window( + timestamps: &mut VecDeque, + limit: u64, + window_nanos: u64, + now_mono: Nanos, + now_unix: UnixSecs, +) -> DimResult { + // Evict timestamps older than the window (amortized cleanup). + let cutoff = now_mono.saturating_sub(window_nanos); + while timestamps.front().is_some_and(|&t| t <= cutoff) { + timestamps.pop_front(); + } + + let count = timestamps.len() as u64; + + // Reset timestamp: when the oldest timestamp in the window expires. + // .max(1) on the division result ensures reset_timestamp is always + // strictly in the future, even when the oldest entry expires in < 1 s + // (integer division would otherwise truncate to 0). + let reset_timestamp = if let Some(&oldest) = timestamps.front() { + let nanos_until_oldest_expires = (oldest + window_nanos).saturating_sub(now_mono); + now_unix + (nanos_until_oldest_expires / 1_000_000_000).max(1) as i64 + } else { + // No requests in window — reset is now + window. + now_unix + (window_nanos / 1_000_000_000) as i64 + }; + + if count < limit { + timestamps.push_back(now_mono); + let remaining = limit - count - 1; + DimResult { + allowed: true, + limit, + remaining, + reset_timestamp, + retry_after: None, + } + } else { + // Oldest timestamp expiry = retry_after. + let retry_after = if let Some(&oldest) = timestamps.front() { + let nanos_until = (oldest + window_nanos).saturating_sub(now_mono); + (nanos_until / 1_000_000_000) as i64 + } else { + 1 + }; + DimResult { + allowed: false, + limit, + remaining: 0, + reset_timestamp, + retry_after: Some(retry_after.max(1)), + } + } +} + +fn token_bucket( + tokens_milli: &mut u64, + last_refill: &mut Nanos, + limit: u64, + window_nanos: u64, + now_mono: Nanos, + now_unix: UnixSecs, +) -> DimResult { + // Refill tokens proportional to elapsed time (integer math, CORR-05). + // refill_rate = limit * 1000 tokens per window_nanos nanoseconds. + let elapsed = now_mono.saturating_sub(*last_refill); + if elapsed > 0 { + // tokens_to_add = limit * 1000 * elapsed / window_nanos + let tokens_to_add = (limit as u128 * 1000 * elapsed as u128 / window_nanos as u128) as u64; + *tokens_milli = (*tokens_milli + tokens_to_add).min(limit.saturating_mul(1000)); + *last_refill = now_mono; + } + + let cap_milli = limit.saturating_mul(1000); + + if *tokens_milli >= 1000 { + *tokens_milli -= 1000; + let remaining = *tokens_milli / 1000; + // reset_timestamp: when bucket would next be full if no more requests arrive. + let tokens_needed_milli = cap_milli.saturating_sub(*tokens_milli); + let refill_secs = if tokens_needed_milli == 0 { + 0i64 + } else { + let nanos = (tokens_needed_milli as u128 * window_nanos as u128 + / (limit as u128 * 1000)) as u64; + (nanos / 1_000_000_000).max(1) as i64 + }; + let reset_timestamp = now_unix + refill_secs; + DimResult { + allowed: true, + limit, + remaining, + reset_timestamp, + retry_after: None, + } + } else { + // No token available — compute time until 1 token refills. + let tokens_needed_milli = 1000u128.saturating_sub(*tokens_milli as u128); + let nanos_until_token = + (tokens_needed_milli * window_nanos as u128 / (limit as u128 * 1000)).max(1); + let retry_after = nanos_until_token.div_ceil(1_000_000_000).max(1) as i64; + let reset_timestamp = now_unix + retry_after; + DimResult { + allowed: false, + limit, + remaining: 0, + reset_timestamp, + retry_after: Some(retry_after), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::Algorithm; + + const WINDOW: u64 = 1_000_000_000; // 1 second in nanos + const T0: Nanos = 1_000_000_000_000; // arbitrary start + const T0_UNIX: UnixSecs = 1_000_000; + + fn check(store: &MemoryStore, key: &str, limit: u64, algo: Algorithm, t: Nanos) -> DimResult { + store.check_and_increment( + key, + limit, + WINDOW, + algo, + t, + T0_UNIX + ((t - T0) / 1_000_000_000) as i64, + ) + } + + // --- Fixed window --- + + #[test] + fn fixed_window_allows_up_to_limit() { + let store = MemoryStore::new(); + for _ in 0..3 { + let r = check(&store, "u:a", 3, Algorithm::FixedWindow, T0); + assert!(r.allowed); + } + let r = check(&store, "u:a", 3, Algorithm::FixedWindow, T0); + assert!(!r.allowed); + } + + #[test] + fn fixed_window_resets_after_window() { + let store = MemoryStore::new(); + for _ in 0..3 { + check(&store, "u:b", 3, Algorithm::FixedWindow, T0); + } + // Advance past window + let r = check(&store, "u:b", 3, Algorithm::FixedWindow, T0 + WINDOW + 1); + assert!(r.allowed); + } + + #[test] + fn fixed_window_reset_timestamp_constant_within_window() { + let store = MemoryStore::new(); + let r1 = check(&store, "u:c", 10, Algorithm::FixedWindow, T0); + let r2 = check(&store, "u:c", 10, Algorithm::FixedWindow, T0 + 100_000_000); + assert!(r1.allowed); + assert!(r2.allowed); + // reset_timestamp must be identical across requests in the same window. + assert_eq!(r1.reset_timestamp, r2.reset_timestamp); + assert!(r1.reset_timestamp > T0_UNIX); + } + + #[test] + fn fixed_window_retry_after_at_least_one() { + let store = MemoryStore::new(); + for _ in 0..2 { + check(&store, "u:d", 2, Algorithm::FixedWindow, T0); + } + let r = check(&store, "u:d", 2, Algorithm::FixedWindow, T0); + assert!(!r.allowed); + assert!(r.retry_after.unwrap() >= 1); + } + + // --- Sliding window --- + + #[test] + fn sliding_window_allows_up_to_limit() { + let store = MemoryStore::new(); + for _ in 0..3 { + assert!(check(&store, "sw:a", 3, Algorithm::SlidingWindow, T0).allowed); + } + assert!(!check(&store, "sw:a", 3, Algorithm::SlidingWindow, T0).allowed); + } + + #[test] + fn sliding_window_allows_after_oldest_expires() { + let store = MemoryStore::new(); + check(&store, "sw:b", 3, Algorithm::SlidingWindow, T0); + check( + &store, + "sw:b", + 3, + Algorithm::SlidingWindow, + T0 + 100_000_000, + ); + check( + &store, + "sw:b", + 3, + Algorithm::SlidingWindow, + T0 + 200_000_000, + ); + // Blocked at T0 + assert!( + !check( + &store, + "sw:b", + 3, + Algorithm::SlidingWindow, + T0 + 500_000_000 + ) + .allowed + ); + // Oldest (T0) expires after WINDOW; T0 + WINDOW + 1 > T0 + WINDOW + assert!(check(&store, "sw:b", 3, Algorithm::SlidingWindow, T0 + WINDOW + 1).allowed); + } + + #[test] + fn sliding_window_no_boundary_burst() { + // With fixed window you could get 2N at the boundary. + // Sliding window prevents this: N requests just before window end, + // then N at window start should still block. + let store = MemoryStore::new(); + let mid = T0 + WINDOW / 2; + for _ in 0..3 { + check(&store, "sw:c", 3, Algorithm::SlidingWindow, mid); + } + // Just after window start, the mid-window requests are still in range. + let r = check(&store, "sw:c", 3, Algorithm::SlidingWindow, T0 + WINDOW + 1); + // mid timestamps expire at mid + WINDOW = T0 + WINDOW/2 + WINDOW + // T0 + WINDOW + 1 < T0 + 3*WINDOW/2, so they're still in range → blocked + assert!(!r.allowed); + } + + // --- Token bucket --- + + #[test] + fn token_bucket_allows_up_to_capacity() { + let store = MemoryStore::new(); + for _ in 0..3 { + assert!(check(&store, "tb:a", 3, Algorithm::TokenBucket, T0).allowed); + } + assert!(!check(&store, "tb:a", 3, Algorithm::TokenBucket, T0).allowed); + } + + #[test] + fn token_bucket_refills_over_time() { + let store = MemoryStore::new(); + // Exhaust 3-token bucket + for _ in 0..3 { + check(&store, "tb:b", 3, Algorithm::TokenBucket, T0); + } + // Wait full window — should refill to capacity + let r = check(&store, "tb:b", 3, Algorithm::TokenBucket, T0 + WINDOW); + assert!(r.allowed); + } + + #[test] + fn token_bucket_integer_math_no_overflow() { + let store = MemoryStore::new(); + // Large limit — should not overflow u64 + let r = check(&store, "tb:c", u64::MAX / 1001, Algorithm::TokenBucket, T0); + assert!(r.allowed); + } + + #[test] + fn token_bucket_reset_timestamp_strictly_greater_than_now() { + let store = MemoryStore::new(); + let r = check(&store, "tb:d", 10, Algorithm::TokenBucket, T0); + assert!(r.allowed); + assert!(r.reset_timestamp > T0_UNIX); + } + + // --- Key isolation --- + + #[test] + fn different_keys_have_independent_counters() { + let store = MemoryStore::new(); + for _ in 0..3 { + check(&store, "u:x", 3, Algorithm::FixedWindow, T0); + } + // Different key must still be allowed + let r = check(&store, "u:y", 3, Algorithm::FixedWindow, T0); + assert!(r.allowed); + } + + // --- Sweep (MEM-06) --- + + #[test] + fn sweep_evicts_stale_fixed_window_keys() { + let store = MemoryStore::new(); + check(&store, "sweep:fw", 3, Algorithm::FixedWindow, T0); + assert_eq!(store.inner.read().len(), 1); + + // Advance just past window_nanos (WINDOW = 1 s) — fixed-window eviction + // uses the per-key window duration, not TOKEN_BUCKET_STALE_NANOS. + let stale_time = T0 + WINDOW + 1; + store.sweep(stale_time); + assert_eq!( + store.inner.read().len(), + 0, + "stale fixed window key must be evicted" + ); + } + + #[test] + fn sweep_evicts_empty_sliding_window_keys() { + let store = MemoryStore::new(); + // Create a sliding window entry then advance past the window so + // the per-access cleanup drains the deque. + check(&store, "sweep:sw", 3, Algorithm::SlidingWindow, T0); + assert_eq!(store.inner.read().len(), 1); + + // Access after window elapses — the per-access cutoff drains all timestamps. + check( + &store, + "sweep:sw", + 3, + Algorithm::SlidingWindow, + T0 + WINDOW + 1, + ); + // Deque now has one fresh entry; sweep should keep it. + store.sweep(T0 + WINDOW + 1); + assert_eq!( + store.inner.read().len(), + 1, + "active sliding window key must be kept" + ); + + // Advance far enough that a sweep after window drain would evict. + let far_future = T0 + WINDOW * 100; + // Access once to create a timestamp, then advance past its window. + check(&store, "sweep:sw2", 1, Algorithm::SlidingWindow, T0); + let after_window = T0 + WINDOW + 1; + // This access drains T0, adds after_window. + check( + &store, + "sweep:sw2", + 1, + Algorithm::SlidingWindow, + after_window, + ); + // Now advance far past, access to drain the deque with a blocked request. + let _ = check(&store, "sweep:sw2", 1, Algorithm::SlidingWindow, far_future); + // The above drains old entries and adds one new one; next access after that window: + let very_far = far_future + WINDOW + 1; + // This drains the far_future entry (outside window) — but adds a new one. + // We need the deque truly empty: exhaust limit then wait. + // Simpler: call sweep directly and check that a key with empty deque gets evicted. + // Manually construct this scenario: + { + let read = store.inner.read(); + if let Some(lock) = read.get("sweep:sw2") { + let mut state = lock.write(); + if let KeyState::SlidingWindow { timestamps, .. } = &mut *state { + timestamps.clear(); + } + } + } + store.sweep(very_far); + assert!( + store.inner.read().get("sweep:sw2").is_none(), + "sliding window key with empty deque must be evicted" + ); + } + + #[test] + fn sweep_evicts_stale_token_bucket_keys() { + let store = MemoryStore::new(); + check(&store, "sweep:tb", 3, Algorithm::TokenBucket, T0); + assert_eq!(store.inner.read().len(), 1); + + let stale_time = T0 + super::TOKEN_BUCKET_STALE_NANOS + 1; + store.sweep(stale_time); + assert_eq!( + store.inner.read().len(), + 0, + "stale token bucket key must be evicted" + ); + } + + #[test] + fn sweep_keeps_active_keys() { + let store = MemoryStore::new(); + check(&store, "sweep:active", 10, Algorithm::FixedWindow, T0); + // Sweep at a time within the window — key should be kept. + // WINDOW is 1s; sweep 500ms later (still inside the window). + store.sweep(T0 + 500_000_000); + assert_eq!( + store.inner.read().len(), + 1, + "active key must not be evicted" + ); + } +} diff --git a/plugins_rust/rate_limiter/src/redis_backend.rs b/plugins_rust/rate_limiter/src/redis_backend.rs new file mode 100644 index 0000000000..724d580dc6 --- /dev/null +++ b/plugins_rust/rate_limiter/src/redis_backend.rs @@ -0,0 +1,660 @@ +// Copyright 2026 +// SPDX-License-Identifier: Apache-2.0 +// +// Redis backend for the rate limiter engine. +// +// Holds a lazily-created multiplexed async Redis connection. +// Fires the same batch Lua scripts as the Python RedisBackend — one call per +// evaluate_many() invocation regardless of dimension count (REDIS-01/03). +// Uses EVALSHA with NOSCRIPT fallback to EVAL (REDIS-02). +// +// Key format: `{prefix}:{dimension_key}:{window_seconds}` +// This matches the Python RedisBackend key format exactly so that instances +// running the Rust backend and instances running the Python fallback share the +// same Redis counters during a rolling upgrade. + +use std::cmp::max; +use std::sync::OnceLock; +use std::sync::atomic::{AtomicU64, Ordering}; + +use parking_lot::Mutex; +use redis::aio::MultiplexedConnection; +use tokio::runtime::{Builder, Handle, Runtime}; + +use crate::config::Algorithm; +use crate::types::DimResult; + +// --------------------------------------------------------------------------- +// Batch Lua scripts — identical to Python RedisBackend._LUA_BATCH_* constants +// +// INVARIANT (rolling-upgrade compatibility): +// These scripts and the key format ({prefix}:{dimension_key}:{window_seconds}) +// MUST stay in sync with the Python RedisBackend in +// plugins/rate_limiter/rate_limiter.py. Both implementations share the same +// Redis counters so that mixed Rust/Python deployments enforce a single set +// of limits during a rolling upgrade. +// +// If you change a script or the key format here, update the Python copy and +// validate with the test_redis_key_format_parity_* tests. +// +// LIMITATION: The Rust path derives `now_float` from `now_unix as f64` +// (whole-second precision) while the Python path passes raw `time.time()` +// (sub-second precision). During a mixed rolling upgrade, sorted-set +// members will have different precision levels for the same logical +// timestamp. The functional impact is at most 1 second on response +// headers — rate enforcement correctness is unaffected. +// --------------------------------------------------------------------------- +// +// LIMITATION: Batch scripts pass multiple KEYS (one per dimension) in a +// single EVAL/EVALSHA call. In Redis Cluster, all keys must hash to the +// same slot. The key format `{prefix}:{dim}:{window}` does NOT use hash +// tags, so these scripts will fail on Redis Cluster. Use standalone Redis +// or Sentinel for multi-dimension batch evaluation. +// --------------------------------------------------------------------------- + +const LUA_BATCH_FIXED: &str = r#" +local results = {} +for i = 1, #KEYS do + local current = redis.call('INCR', KEYS[i]) + if current == 1 then + redis.call('EXPIRE', KEYS[i], ARGV[i]) + end + local ttl = redis.call('TTL', KEYS[i]) + results[i] = {current, ttl} +end +return results +"#; + +const LUA_BATCH_SLIDING: &str = r#" +local now = tonumber(ARGV[1]) +local results = {} +for i = 1, #KEYS do + local base = 1 + (i-1)*3 + 1 + local window = tonumber(ARGV[base]) + local limit = tonumber(ARGV[base+1]) + local member = ARGV[base+2] + local cutoff = now - window + redis.call('ZREMRANGEBYSCORE', KEYS[i], '-inf', cutoff) + local count = tonumber(redis.call('ZCARD', KEYS[i])) + redis.call('EXPIRE', KEYS[i], window + 1) + if count >= limit then + local oldest = redis.call('ZRANGE', KEYS[i], 0, 0, 'WITHSCORES') + local oldest_ts = 0 + if #oldest > 0 then oldest_ts = tonumber(oldest[2]) end + results[i] = {0, count, oldest_ts} + else + redis.call('ZADD', KEYS[i], now, member) + count = count + 1 + local oldest = redis.call('ZRANGE', KEYS[i], 0, 0, 'WITHSCORES') + local oldest_ts = 0 + if #oldest > 0 then oldest_ts = tonumber(oldest[2]) end + results[i] = {1, count, oldest_ts} + end +end +return results +"#; + +// NOTE: Lua uses floating-point arithmetic for token refill (tokens + elapsed * rate), +// while the in-memory Rust backend uses integer milli-token math (u128). Under sustained +// high-frequency traffic the two may diverge by ±1 token due to float precision loss. +// This is acceptable for rate limiting — the behavioral contract is identical. +const LUA_BATCH_TOKEN_BUCKET: &str = r#" +local now = tonumber(ARGV[1]) +local results = {} +for i = 1, #KEYS do + local base = 1 + (i-1)*2 + 1 + local capacity = tonumber(ARGV[base]) + local rate = tonumber(ARGV[base+1]) + local data = redis.call('HMGET', KEYS[i], 'tokens', 'last_refill') + local tokens = tonumber(data[1]) + local last_refill = tonumber(data[2]) + if tokens == nil then + tokens = capacity - 1 + redis.call('HSET', KEYS[i], 'tokens', tokens, 'last_refill', now) + local ttl = math.ceil(capacity / rate) + 1 + redis.call('EXPIRE', KEYS[i], ttl) + results[i] = {1, math.floor(tokens), 0} + else + local elapsed = now - last_refill + tokens = math.min(capacity, tokens + elapsed * rate) + local allowed, time_to_next + if tokens >= 1.0 then + tokens = tokens - 1.0 + allowed = 1 + time_to_next = 0 + else + allowed = 0 + time_to_next = math.ceil((1.0 - tokens) / rate) + end + redis.call('HSET', KEYS[i], 'tokens', tokens, 'last_refill', now) + local ttl = math.ceil((capacity - tokens) / rate) + 1 + redis.call('EXPIRE', KEYS[i], ttl) + results[i] = {allowed, math.floor(tokens), time_to_next} + end +end +return results +"#; + +// --------------------------------------------------------------------------- +// Unique member counter for sliding window sorted sets +// --------------------------------------------------------------------------- + +static MEMBER_CTR: AtomicU64 = AtomicU64::new(0); + +/// Process-unique PID, cached once. +fn process_id() -> u32 { + static PID: OnceLock = OnceLock::new(); + *PID.get_or_init(std::process::id) +} + +/// Random nonce generated once at process start. Combined with PID and atomic +/// counter this guarantees unique sorted-set members across gateway replicas +/// even in containerized environments where PID 1 is common, preventing ZADD +/// overwrites that would cause undercounting. +fn instance_nonce() -> u64 { + static NONCE: OnceLock = OnceLock::new(); + *NONCE.get_or_init(|| { + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; + let mut h = DefaultHasher::new(); + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_nanos() + .hash(&mut h); + std::process::id().hash(&mut h); + h.finish() + }) +} + +fn unique_member(now: f64) -> String { + use std::fmt::Write; + let n = MEMBER_CTR.fetch_add(1, Ordering::Relaxed); + let mut buf = String::with_capacity(60); + let _ = write!( + buf, + "{:.6}:{}:{}:{}", + now, + process_id(), + instance_nonce(), + n + ); + buf +} + +// --------------------------------------------------------------------------- +// Value extraction helpers +// --------------------------------------------------------------------------- + +fn val_i64(v: &redis::Value) -> i64 { + match v { + redis::Value::Int(i) => *i, + redis::Value::BulkString(b) => std::str::from_utf8(b) + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or_else(|| { + log::error!("Redis returned unparseable i64 BulkString; defaulting to 0"); + 0 + }), + other => { + log::error!( + "Redis returned unexpected value type for i64: {:?}; defaulting to 0", + other + ); + 0 + } + } +} + +fn val_f64(v: &redis::Value) -> f64 { + match v { + redis::Value::Int(i) => *i as f64, + redis::Value::BulkString(b) => std::str::from_utf8(b) + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or_else(|| { + log::error!("Redis returned unparseable f64 BulkString; defaulting to 0.0"); + 0.0 + }), + other => { + log::error!( + "Redis returned unexpected value type for f64: {:?}; defaulting to 0.0", + other + ); + 0.0 + } + } +} + +fn inner_array(outer: &redis::Value, i: usize) -> Option<&Vec> { + match outer { + redis::Value::Array(a) => match a.get(i) { + Some(redis::Value::Array(inner)) => Some(inner), + _ => None, + }, + _ => None, + } +} + +// --------------------------------------------------------------------------- +// RedisRateLimiter +// --------------------------------------------------------------------------- + +pub struct RedisRateLimiter { + client: redis::Client, + conn: Mutex>, + algorithm: Algorithm, + prefix: String, + /// Cached SHA for the active algorithm's batch Lua script (REDIS-02). + /// Populated on first use via SCRIPT LOAD; cleared on connection reset. + script_sha: Mutex>, +} + +fn shared_runtime() -> Result<&'static Runtime, redis::RedisError> { + static RUNTIME: OnceLock> = OnceLock::new(); + let result = RUNTIME.get_or_init(|| { + Builder::new_multi_thread() + .worker_threads(2) + .enable_all() + .build() + .map_err(|e| e.to_string()) + }); + match result { + Ok(rt) => Ok(rt), + Err(msg) => Err(redis::RedisError::from(( + redis::ErrorKind::IoError, + "tokio runtime init failed", + msg.clone(), + ))), + } +} + +impl RedisRateLimiter { + pub fn new( + redis_url: &str, + algorithm: Algorithm, + prefix: String, + ) -> Result { + let client = redis::Client::open(redis_url)?; + Ok(Self { + client, + conn: Mutex::new(None), + algorithm, + prefix, + script_sha: Mutex::new(None), + }) + } + + async fn connection_async(&self) -> Result { + { + let conn_guard = self.conn.lock(); + if let Some(conn) = conn_guard.as_ref() { + return Ok(conn.clone()); + } + } + + // Timeout prevents blocking the gateway thread indefinitely when + // Redis is unreachable (network partition, DNS failure, etc.). + let conn = tokio::time::timeout( + std::time::Duration::from_secs(5), + self.client.get_multiplexed_tokio_connection(), + ) + .await + .map_err(|_| { + redis::RedisError::from(( + redis::ErrorKind::IoError, + "Redis connection timed out after 5 s", + )) + })??; + + let mut conn_guard = self.conn.lock(); + if let Some(existing) = conn_guard.as_ref() { + return Ok(existing.clone()); + } + *conn_guard = Some(conn.clone()); + Ok(conn) + } + + fn reset_connection(&self) { + log::warn!("Redis connection reset after error; will reconnect on next request"); + *self.conn.lock() = None; + *self.script_sha.lock() = None; + } + + /// Return the batch Lua script for the active algorithm. + fn batch_script(&self) -> &'static str { + match self.algorithm { + Algorithm::FixedWindow => LUA_BATCH_FIXED, + Algorithm::SlidingWindow => LUA_BATCH_SLIDING, + Algorithm::TokenBucket => LUA_BATCH_TOKEN_BUCKET, + } + } + + /// REDIS-02: Load the active algorithm's script via SCRIPT LOAD and cache + /// the SHA. Returns the cached SHA on subsequent calls. + async fn ensure_script_loaded( + &self, + conn: &mut MultiplexedConnection, + ) -> Result { + { + let guard = self.script_sha.lock(); + if let Some(sha) = guard.as_ref() { + return Ok(sha.clone()); + } + } + let sha: String = redis::cmd("SCRIPT") + .arg("LOAD") + .arg(self.batch_script()) + .query_async(conn) + .await?; + *self.script_sha.lock() = Some(sha.clone()); + Ok(sha) + } + + /// REDIS-02: Execute via EVALSHA when the SHA is cached; fall back to EVAL + /// on NOSCRIPT (Redis restarted and flushed its script cache). + async fn evalsha_or_eval( + &self, + conn: &mut MultiplexedConnection, + num_keys: usize, + keys: &[String], + args: &[Vec], + ) -> Result { + // Try EVALSHA if we have a cached SHA. + if let Ok(sha) = self.ensure_script_loaded(conn).await { + let mut cmd = redis::cmd("EVALSHA"); + cmd.arg(&sha).arg(num_keys); + for k in keys { + cmd.arg(k.as_bytes()); + } + for a in args { + cmd.arg(a.as_slice()); + } + match cmd.query_async::(conn).await { + Ok(val) => return Ok(val), + Err(e) if e.kind() == redis::ErrorKind::NoScriptError => { + // NOSCRIPT — clear cached SHA, fall through to EVAL. + *self.script_sha.lock() = None; + } + Err(e) => return Err(e), + } + } + + // Fallback: full EVAL (first call or after NOSCRIPT). + let mut cmd = redis::cmd("EVAL"); + cmd.arg(self.batch_script()).arg(num_keys); + for k in keys { + cmd.arg(k.as_bytes()); + } + for a in args { + cmd.arg(a.as_slice()); + } + let result: redis::Value = cmd.query_async(conn).await?; + + // Re-cache SHA for next call. + let _ = self.ensure_script_loaded(conn).await; + + Ok(result) + } + + /// Evaluate all dimension checks in a single Redis call. + /// + /// `checks` is `(dimension_key, limit_count, window_nanos)` — same shape + /// as the memory engine. Returns one `DimResult` per check. + pub fn evaluate_many( + &self, + checks: &[(String, u64, u64)], + now_unix: i64, + ) -> Result, redis::RedisError> { + // Guard: block_on from within an existing Tokio runtime panics. + // Return a clear error instead of crashing the Python process. + if Handle::try_current().is_ok() { + return Err(redis::RedisError::from(( + redis::ErrorKind::IoError, + "evaluate_many (sync) called from within a Tokio runtime; use evaluate_many_async instead", + ))); + } + shared_runtime()?.block_on(self.evaluate_many_async(checks, now_unix)) + } + + pub async fn evaluate_many_async( + &self, + checks: &[(String, u64, u64)], + now_unix: i64, + ) -> Result, redis::RedisError> { + if checks.is_empty() { + return Ok(vec![]); + } + + // Derive from the passed-in now_unix so Python time mocks propagate + // to Redis Lua scripts (CORR-02). + let now_float = now_unix as f64; + + let mut conn = self.connection_async().await?; + let result = match self.algorithm { + Algorithm::FixedWindow => self.eval_fixed(&mut conn, checks, now_unix).await, + Algorithm::SlidingWindow => { + self.eval_sliding(&mut conn, checks, now_float, now_unix) + .await + } + Algorithm::TokenBucket => { + self.eval_token_bucket(&mut conn, checks, now_float, now_unix) + .await + } + }; + if let Err(ref e) = result { + // Only reset the multiplexed connection on transport-level errors. + // Script/type errors are recoverable without dropping in-flight requests. + if matches!( + e.kind(), + redis::ErrorKind::IoError + | redis::ErrorKind::BusyLoadingError + | redis::ErrorKind::TryAgain + ) { + self.reset_connection(); + } + } + result + } + + fn redis_key(&self, dim_key: &str, window_nanos: u64) -> String { + let window_secs = (window_nanos / 1_000_000_000).max(1); + format!("{}:{}:{}", self.prefix, dim_key, window_secs) + } + + fn token_bucket_time_to_full(limit: u64, remaining: u64, window_nanos: u64) -> i64 { + if remaining >= limit { + return 0; + } + let window_secs = window_nanos as f64 / 1_000_000_000.0; + let refill_rate = limit as f64 / window_secs; + let tokens_needed = limit - remaining; + max(1, (tokens_needed as f64 / refill_rate) as i64) + } + + // --- Fixed window --- + + async fn eval_fixed( + &self, + conn: &mut MultiplexedConnection, + checks: &[(String, u64, u64)], + now_unix: i64, + ) -> Result, redis::RedisError> { + let keys: Vec = checks + .iter() + .map(|(k, _, w)| self.redis_key(k, *w)) + .collect(); + let args: Vec> = checks + .iter() + .map(|(_, _, w)| format!("{}", (w / 1_000_000_000).max(1)).into_bytes()) + .collect(); + + let raw = self.evalsha_or_eval(conn, keys.len(), &keys, &args).await?; + let mut results = Vec::with_capacity(checks.len()); + + for (i, (_, limit, _)) in checks.iter().enumerate() { + let inner = inner_array(&raw, i).ok_or_else(|| { + redis::RedisError::from((redis::ErrorKind::TypeError, "expected inner array")) + })?; + let count = val_i64(inner.first().unwrap_or(&redis::Value::Int(0))) as u64; + let ttl = val_i64(inner.get(1).unwrap_or(&redis::Value::Int(0))); + let reset_timestamp = now_unix + ttl.max(0); + + if count > *limit { + results.push(DimResult { + allowed: false, + limit: *limit, + remaining: 0, + reset_timestamp, + retry_after: Some(ttl.max(1)), + }); + } else { + results.push(DimResult { + allowed: true, + limit: *limit, + remaining: limit - count, + reset_timestamp, + retry_after: None, + }); + } + } + Ok(results) + } + + // --- Sliding window --- + + async fn eval_sliding( + &self, + conn: &mut MultiplexedConnection, + checks: &[(String, u64, u64)], + now_float: f64, + now_unix: i64, + ) -> Result, redis::RedisError> { + let keys: Vec = checks + .iter() + .map(|(k, _, w)| self.redis_key(k, *w)) + .collect(); + + let mut args: Vec> = vec![format!("{}", now_float).into_bytes()]; + for (_, limit, window_nanos) in checks { + let window_secs = (window_nanos / 1_000_000_000).max(1); + args.push(format!("{}", window_secs).into_bytes()); + args.push(format!("{}", limit).into_bytes()); + args.push(unique_member(now_float).into_bytes()); + } + + let raw = self.evalsha_or_eval(conn, keys.len(), &keys, &args).await?; + let mut results = Vec::with_capacity(checks.len()); + + for (i, (_, limit, window_nanos)) in checks.iter().enumerate() { + let inner = inner_array(&raw, i).ok_or_else(|| { + redis::RedisError::from((redis::ErrorKind::TypeError, "expected inner array")) + })?; + let allowed_int = val_i64(inner.first().unwrap_or(&redis::Value::Int(0))); + let count = val_i64(inner.get(1).unwrap_or(&redis::Value::Int(0))) as u64; + let oldest_ts = val_f64(inner.get(2).unwrap_or(&redis::Value::Int(0))); + let window_secs = (window_nanos / 1_000_000_000) as f64; + let reset_timestamp = (oldest_ts + window_secs) as i64; + let reset_in = (reset_timestamp - now_unix).max(1); + + if allowed_int == 0 { + results.push(DimResult { + allowed: false, + limit: *limit, + remaining: 0, + reset_timestamp, + retry_after: Some(reset_in), + }); + } else { + results.push(DimResult { + allowed: true, + limit: *limit, + remaining: limit.saturating_sub(count), + reset_timestamp, + retry_after: None, + }); + } + } + Ok(results) + } + + // --- Token bucket --- + + async fn eval_token_bucket( + &self, + conn: &mut MultiplexedConnection, + checks: &[(String, u64, u64)], + now_float: f64, + now_unix: i64, + ) -> Result, redis::RedisError> { + let keys: Vec = checks + .iter() + .map(|(k, _, w)| self.redis_key(k, *w)) + .collect(); + + let mut args: Vec> = vec![format!("{}", now_float).into_bytes()]; + for (_, limit, window_nanos) in checks { + let window_secs = *window_nanos as f64 / 1_000_000_000.0; + let rate = *limit as f64 / window_secs; + args.push(format!("{}", limit).into_bytes()); + args.push(format!("{}", rate).into_bytes()); + } + + let raw = self.evalsha_or_eval(conn, keys.len(), &keys, &args).await?; + let mut results = Vec::with_capacity(checks.len()); + + for (i, (_, limit, window_nanos)) in checks.iter().enumerate() { + let inner = inner_array(&raw, i).ok_or_else(|| { + redis::RedisError::from((redis::ErrorKind::TypeError, "expected inner array")) + })?; + let allowed_int = val_i64(inner.first().unwrap_or(&redis::Value::Int(0))); + let remaining = val_i64(inner.get(1).unwrap_or(&redis::Value::Int(0))) as u64; + let time_to_next = val_i64(inner.get(2).unwrap_or(&redis::Value::Int(0))); + + if allowed_int == 0 { + let reset_timestamp = now_unix + time_to_next.max(1); + results.push(DimResult { + allowed: false, + limit: *limit, + remaining: 0, + reset_timestamp, + retry_after: Some(time_to_next.max(1)), + }); + } else { + let time_to_full = + Self::token_bucket_time_to_full(*limit, remaining, *window_nanos); + let reset_timestamp = now_unix + time_to_full; + results.push(DimResult { + allowed: true, + limit: *limit, + remaining, + reset_timestamp, + retry_after: None, + }); + } + } + Ok(results) + } +} + +#[cfg(test)] +mod tests { + use super::RedisRateLimiter; + + #[test] + fn token_bucket_success_reset_uses_time_to_full() { + let window_nanos = 10_000_000_000_u64; // 10s + let limit = 10_u64; + let remaining = 9_u64; + assert_eq!( + RedisRateLimiter::token_bucket_time_to_full(limit, remaining, window_nanos), + 1 + ); + + let remaining = 5_u64; + assert_eq!( + RedisRateLimiter::token_bucket_time_to_full(limit, remaining, window_nanos), + 5 + ); + } +} diff --git a/plugins_rust/rate_limiter/src/types.rs b/plugins_rust/rate_limiter/src/types.rs new file mode 100644 index 0000000000..864e872ff7 --- /dev/null +++ b/plugins_rust/rate_limiter/src/types.rs @@ -0,0 +1,273 @@ +// Copyright 2026 +// SPDX-License-Identifier: Apache-2.0 +// +// Public result types for the rate limiter engine. +// +// `EvalResult` is the compact typed struct returned by `evaluate_many()`. +// It matches the shape described in IFACE-03 and is the only type that +// crosses the PyO3 boundary. + +use pyo3::prelude::*; +use pyo3_stub_gen::derive::*; + +/// The outcome of a single dimension check. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DimResult { + pub allowed: bool, + /// Configured request limit for this dimension. + pub limit: u64, + /// Requests remaining in the current window (0 when blocked). + pub remaining: u64, + /// Unix timestamp when the current window resets. + pub reset_timestamp: i64, + /// Seconds until the window resets — `Some` only when blocked. + pub retry_after: Option, +} + +/// The outcome of a single active dimension, exposed to Python for +/// per-dimension inspection (e.g. which dimension blocked the request). +#[gen_stub_pyclass] +#[pyclass(get_all, from_py_object)] +#[derive(Debug, Clone)] +pub struct EvalDimension { + /// Requests remaining for this active dimension. + pub remaining: u64, + /// Unix timestamp when this dimension resets or refills. + pub reset_timestamp: i64, + /// Seconds until retry — populated only for blocked dimensions. + pub retry_after: Option, +} + +/// The aggregated result returned to Python via `evaluate_many()`. +/// +/// Contains the most restrictive outcome across all active dimensions +/// (min remaining, earliest unblock among blocked dimensions — matching +/// Python `_select_most_restrictive`). +#[gen_stub_pyclass] +#[pyclass(get_all, from_py_object)] +#[derive(Debug, Clone)] +pub struct EvalResult { + /// `True` if all active dimensions allow the request. + pub allowed: bool, + /// Configured limit for the most restrictive active dimension. + pub limit: u64, + /// Remaining requests for the most restrictive active dimension. + pub remaining: u64, + /// Unix timestamp when the most restrictive dimension resets. + pub reset_timestamp: i64, + /// Seconds until reset — populated only when `allowed == False`. + pub retry_after: Option, + /// Per-dimension outcomes that were blocked for this request. + pub violated_dimensions: Vec, + /// Per-dimension outcomes that still allowed this request. + pub allowed_dimensions: Vec, +} + +#[gen_stub_pymethods] +#[pymethods] +impl EvalResult { + fn __repr__(&self) -> String { + format!( + "EvalResult(allowed={}, limit={}, remaining={}, reset_timestamp={}, retry_after={:?})", + self.allowed, self.limit, self.remaining, self.reset_timestamp, self.retry_after + ) + } +} + +impl EvalResult { + /// Construct an "unlimited" result used when no dimensions are configured. + pub fn unlimited(reset_timestamp: i64) -> Self { + Self { + allowed: true, + limit: u64::MAX, + remaining: u64::MAX, + reset_timestamp, + retry_after: None, + violated_dimensions: Vec::new(), + allowed_dimensions: Vec::new(), + } + } + + /// Select the most restrictive result across a slice of `DimResult`s. + /// + /// Rules (matching Python `_select_most_restrictive`): + /// - Any blocked dimension → result is blocked. + /// - Among blocked: lowest `retry_after` wins (soonest retry). + /// - Among allowed: lowest `remaining` wins (closest to limit). + /// - `retry_after` is set iff the result is blocked. + /// + /// The "lowest retry_after" policy signals the next state change — the + /// caller learns when at least one dimension will re-open, even if other + /// dimensions remain blocked longer. An alternative (max) would + /// guarantee success on retry but delays the first attempt. This is a + /// deliberate product-level contract shared by both implementations. + pub fn from_dims(dims: &[DimResult]) -> Self { + if dims.is_empty() { + return Self::unlimited(0); + } + + let any_blocked = dims.iter().any(|d| !d.allowed); + let violated_dimensions: Vec = dims + .iter() + .filter(|d| !d.allowed) + .map(|d| EvalDimension { + remaining: d.remaining, + reset_timestamp: d.reset_timestamp, + retry_after: d.retry_after, + }) + .collect(); + let allowed_dimensions: Vec = dims + .iter() + .filter(|d| d.allowed) + .map(|d| EvalDimension { + remaining: d.remaining, + reset_timestamp: d.reset_timestamp, + retry_after: None, + }) + .collect(); + + if any_blocked { + // Among blocked dimensions, pick the one that unblocks soonest. + let worst = dims + .iter() + .filter(|d| !d.allowed) + .min_by_key(|d| d.retry_after.unwrap_or(i64::MAX)) + .unwrap(); + Self { + allowed: false, + limit: worst.limit, + remaining: 0, + reset_timestamp: worst.reset_timestamp, + retry_after: worst.retry_after, + violated_dimensions, + allowed_dimensions, + } + } else { + // All allowed — pick the one with the fewest remaining. + let most_restrictive = dims.iter().min_by_key(|d| d.remaining).unwrap(); + Self { + allowed: true, + limit: most_restrictive.limit, + remaining: most_restrictive.remaining, + reset_timestamp: most_restrictive.reset_timestamp, + retry_after: None, + violated_dimensions, + allowed_dimensions, + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn dim(allowed: bool, remaining: u64, reset: i64) -> DimResult { + DimResult { + allowed, + limit: 10, + remaining, + reset_timestamp: reset, + retry_after: if allowed { None } else { Some(reset - 1000) }, + } + } + + // --- IFACE-03: EvalResult field types --- + + #[test] + fn eval_result_fields_accessible() { + let r = EvalResult { + allowed: true, + limit: 30, + remaining: 25, + reset_timestamp: 9_999_999, + retry_after: None, + violated_dimensions: Vec::new(), + allowed_dimensions: vec![EvalDimension { + remaining: 25, + reset_timestamp: 9_999_999, + retry_after: None, + }], + }; + assert!(r.allowed); + assert_eq!(r.limit, 30); + assert_eq!(r.remaining, 25); + assert_eq!(r.reset_timestamp, 9_999_999); + assert!(r.retry_after.is_none()); + } + + #[test] + fn eval_result_retry_after_populated_when_blocked() { + let r = EvalResult { + allowed: false, + limit: 30, + remaining: 0, + reset_timestamp: 9_999_999, + retry_after: Some(42), + violated_dimensions: vec![EvalDimension { + remaining: 0, + reset_timestamp: 9_999_999, + retry_after: Some(42), + }], + allowed_dimensions: Vec::new(), + }; + assert!(!r.allowed); + assert_eq!(r.retry_after, Some(42)); + } + + // --- CORR-07: from_dims aggregation --- + + #[test] + fn from_dims_empty_is_unlimited() { + let r = EvalResult::from_dims(&[]); + assert!(r.allowed); + assert_eq!(r.limit, u64::MAX); + assert!(r.allowed_dimensions.is_empty()); + assert!(r.violated_dimensions.is_empty()); + } + + #[test] + fn from_dims_all_allowed_picks_min_remaining() { + let dims = vec![dim(true, 20, 2000), dim(true, 5, 1500), dim(true, 15, 1800)]; + let r = EvalResult::from_dims(&dims); + assert!(r.allowed); + assert_eq!(r.remaining, 5); + assert_eq!(r.reset_timestamp, 1500); + assert!(r.retry_after.is_none()); + assert_eq!(r.allowed_dimensions.len(), 3); + assert!(r.violated_dimensions.is_empty()); + } + + #[test] + fn from_dims_any_blocked_result_is_blocked() { + let dims = vec![dim(true, 5, 1500), dim(false, 0, 2000), dim(true, 10, 1800)]; + let r = EvalResult::from_dims(&dims); + assert!(!r.allowed); + assert_eq!(r.remaining, 0); + assert_eq!(r.allowed_dimensions.len(), 2); + assert_eq!(r.violated_dimensions.len(), 1); + } + + #[test] + fn from_dims_multiple_blocked_picks_soonest_retry() { + let dims = vec![ + dim(false, 0, 3000), + dim(false, 0, 1000), + dim(false, 0, 2000), + ]; + let r = EvalResult::from_dims(&dims); + assert!(!r.allowed); + assert_eq!(r.reset_timestamp, 1000); + assert_eq!(r.retry_after, Some(0)); + assert_eq!(r.violated_dimensions.len(), 3); + } + + #[test] + fn from_dims_retry_after_none_when_allowed() { + let dims = vec![dim(true, 1, 9000)]; + let r = EvalResult::from_dims(&dims); + assert!(r.retry_after.is_none()); + assert_eq!(r.allowed_dimensions[0].remaining, 1); + assert_eq!(r.allowed_dimensions[0].reset_timestamp, 9000); + } +} diff --git a/tests/integration/test_rate_limiter.py b/tests/integration/test_rate_limiter.py index 40de40f49e..f07adc0d0f 100644 --- a/tests/integration/test_rate_limiter.py +++ b/tests/integration/test_rate_limiter.py @@ -20,14 +20,17 @@ prompt_pre_fetch and tool_pre_invoke. """ +# Standard import asyncio +import socket +import subprocess import time -from typing import AsyncIterator, Dict -from unittest.mock import patch -import pytest +# Third-Party from fastapi.testclient import TestClient +import pytest +# First-Party from mcpgateway.main import app from mcpgateway.plugins.framework import ( GlobalContext, @@ -36,45 +39,17 @@ PromptPrehookPayload, ToolPreInvokePayload, ) -from mcpgateway.utils.create_jwt_token import _create_jwt_token -from plugins.rate_limiter.rate_limiter import RateLimiterPlugin, _store - +from mcpgateway.plugins.framework.base import HookRef, PluginRef +from mcpgateway.plugins.framework.errors import PluginViolationError +from mcpgateway.plugins.framework.manager import PluginExecutor +from mcpgateway.plugins.framework.models import PluginMode +from plugins.rate_limiter.rate_limiter import RateLimiterPlugin # API Endpoints PROMPT_ENDPOINT = "/api/v1/prompts/" TOOL_INVOKE_ENDPOINT = "/api/v1/tools/invoke" -@pytest.fixture(autouse=True) -def clear_rate_limit_store(): - """Clear rate limit store before and after each test.""" - _store.clear() - yield - _store.clear() - - -@pytest.fixture -def jwt_token_alice(): - """JWT token for user alice in team1.""" - return _create_jwt_token( - {"sub": "alice", "username": "alice"}, - expires_in_minutes=60, - user_data={"email": "alice@example.com", "full_name": "Alice", "is_admin": False, "auth_provider": "test"}, - teams=["team1"], - ) - - -@pytest.fixture -def jwt_token_bob(): - """JWT token for user bob in team2.""" - return _create_jwt_token( - {"sub": "bob", "username": "bob"}, - expires_in_minutes=60, - user_data={"email": "bob@example.com", "full_name": "Bob", "is_admin": False, "auth_provider": "test"}, - teams=["team2"], - ) - - @pytest.fixture def rate_limit_plugin_2_per_second(): """Rate limiter plugin configured for 2 requests per second.""" @@ -83,11 +58,7 @@ def rate_limit_plugin_2_per_second(): kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", hooks=["prompt_pre_fetch", "tool_pre_invoke"], priority=100, - config={ - "by_user": "2/s", - "by_tenant": None, - "by_tool": {} - } + config={"by_user": "2/s", "by_tenant": None, "by_tool": {}}, ) return RateLimiterPlugin(config) @@ -100,13 +71,7 @@ def rate_limit_plugin_multi_dimensional(): kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", hooks=["prompt_pre_fetch", "tool_pre_invoke"], priority=100, - config={ - "by_user": "10/s", - "by_tenant": "5/s", - "by_tool": { - "restricted_tool": "1/s" - } - } + config={"by_user": "10/s", "by_tenant": "5/s", "by_tool": {"restricted_tool": "1/s"}}, ) return RateLimiterPlugin(config) @@ -281,11 +246,7 @@ async def test_user_rate_limit_enforced(self): kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", hooks=["prompt_pre_fetch"], priority=100, - config={ - "by_user": "10/s", - "by_tenant": None, # No tenant limit - "by_tool": {} - } + config={"by_user": "10/s", "by_tenant": None, "by_tool": {}}, # No tenant limit ) plugin = RateLimiterPlugin(config) @@ -363,7 +324,7 @@ async def test_most_restrictive_dimension_selected(self): config={ "by_user": "10/s", # More permissive "by_tenant": "2/s", # More restrictive - } + }, ) plugin = RateLimiterPlugin(config) @@ -424,32 +385,1034 @@ class TestStoreCleanup: @pytest.mark.asyncio async def test_store_cleanup_between_tests(self, rate_limit_plugin_2_per_second): - """Verify store is cleaned up between tests.""" - # Store should be empty at start (autouse fixture) - assert len(_store) == 0 - + """Verify each plugin instance starts with an empty store.""" plugin = rate_limit_plugin_2_per_second + backend = plugin._rate_backend + + # Fresh instance — store must be empty before any requests + assert len(backend._algorithm._store) == 0 + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) payload = PromptPrehookPayload(prompt_id="test", args={}) - # Make a request await plugin.prompt_pre_fetch(payload, ctx) - # Store should have entries - assert len(_store) > 0 + # After one request a window entry must exist + assert len(backend._algorithm._store) > 0 @pytest.mark.asyncio async def test_multiple_users_create_separate_windows(self, rate_limit_plugin_2_per_second): - """Verify multiple users create separate windows in store.""" + """Verify multiple users create separate window entries in the backend store.""" plugin = rate_limit_plugin_2_per_second + backend = plugin._rate_backend ctx_alice = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) ctx_bob = PluginContext(global_context=GlobalContext(request_id="r2", user="bob")) payload = PromptPrehookPayload(prompt_id="test", args={}) - # Make requests from both users await plugin.prompt_pre_fetch(payload, ctx_alice) await plugin.prompt_pre_fetch(payload, ctx_bob) - # Store should have entries for both users - assert len(_store) >= 2 + # Each user must have their own key in the store + assert len(backend._algorithm._store) >= 2 + + +class TestSlidingWindowIntegration: + """End-to-end integration tests for the sliding_window algorithm.""" + + @pytest.fixture + def plugin(self): + config = PluginConfig( + name="RateLimiter", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=["prompt_pre_fetch", "tool_pre_invoke"], + priority=100, + config={"by_user": "3/s", "algorithm": "sliding_window"}, + ) + return RateLimiterPlugin(config) + + @pytest.mark.asyncio + async def test_sliding_window_enforces_limit(self, plugin): + """Sliding window allows exactly N requests then blocks.""" + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + for _ in range(3): + result = await plugin.tool_pre_invoke(payload, ctx) + assert result.violation is None + + result = await plugin.tool_pre_invoke(payload, ctx) + assert result.violation is not None + assert result.violation.http_status_code == 429 + + @pytest.mark.asyncio + async def test_sliding_window_returns_ratelimit_headers(self, plugin): + """Sliding window includes X-RateLimit-* headers on allowed requests.""" + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + result = await plugin.tool_pre_invoke(payload, ctx) + assert result.violation is None + assert result.http_headers is not None + assert "X-RateLimit-Limit" in result.http_headers + assert "X-RateLimit-Remaining" in result.http_headers + assert "X-RateLimit-Reset" in result.http_headers + assert "Retry-After" not in result.http_headers + + @pytest.mark.asyncio + async def test_sliding_window_retry_after_on_violation(self, plugin): + """Sliding window includes Retry-After on violations.""" + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + for _ in range(3): + await plugin.tool_pre_invoke(payload, ctx) + + result = await plugin.tool_pre_invoke(payload, ctx) + assert result.violation is not None + assert "Retry-After" in result.violation.http_headers + + @pytest.mark.asyncio + async def test_sliding_window_resets_after_window(self, plugin): + """Sliding window allows requests again after the window elapses.""" + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + for _ in range(3): + await plugin.tool_pre_invoke(payload, ctx) + + result = await plugin.tool_pre_invoke(payload, ctx) + assert result.violation is not None + + await asyncio.sleep(1.1) + + result = await plugin.tool_pre_invoke(payload, ctx) + assert result.violation is None + + @pytest.mark.asyncio + async def test_sliding_window_independent_users(self, plugin): + """Sliding window tracks separate counters per user.""" + ctx_alice = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + ctx_bob = PluginContext(global_context=GlobalContext(request_id="r2", user="bob")) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + for _ in range(3): + await plugin.tool_pre_invoke(payload, ctx_alice) + + alice_blocked = await plugin.tool_pre_invoke(payload, ctx_alice) + assert alice_blocked.violation is not None + + bob_allowed = await plugin.tool_pre_invoke(payload, ctx_bob) + assert bob_allowed.violation is None + + +class TestTokenBucketIntegration: + """End-to-end integration tests for the token_bucket algorithm.""" + + @pytest.fixture + def plugin(self): + config = PluginConfig( + name="RateLimiter", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=["prompt_pre_fetch", "tool_pre_invoke"], + priority=100, + config={"by_user": "3/s", "algorithm": "token_bucket"}, + ) + return RateLimiterPlugin(config) + + @pytest.mark.asyncio + async def test_token_bucket_enforces_limit(self, plugin): + """Token bucket allows up to capacity requests then blocks.""" + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + for _ in range(3): + result = await plugin.tool_pre_invoke(payload, ctx) + assert result.violation is None + + result = await plugin.tool_pre_invoke(payload, ctx) + assert result.violation is not None + assert result.violation.http_status_code == 429 + + @pytest.mark.asyncio + async def test_token_bucket_returns_ratelimit_headers(self, plugin): + """Token bucket includes X-RateLimit-* headers on allowed requests.""" + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + result = await plugin.tool_pre_invoke(payload, ctx) + assert result.violation is None + assert result.http_headers is not None + assert "X-RateLimit-Limit" in result.http_headers + assert "X-RateLimit-Remaining" in result.http_headers + assert "X-RateLimit-Reset" in result.http_headers + + @pytest.mark.asyncio + async def test_token_bucket_remaining_decrements(self, plugin): + """Token bucket X-RateLimit-Remaining decrements with each request.""" + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + r1 = await plugin.tool_pre_invoke(payload, ctx) + r2 = await plugin.tool_pre_invoke(payload, ctx) + + remaining1 = int(r1.http_headers["X-RateLimit-Remaining"]) + remaining2 = int(r2.http_headers["X-RateLimit-Remaining"]) + assert remaining2 < remaining1 + + @pytest.mark.asyncio + async def test_token_bucket_refills_over_time(self, plugin): + """Token bucket allows requests again after tokens refill.""" + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + for _ in range(3): + await plugin.tool_pre_invoke(payload, ctx) + + result = await plugin.tool_pre_invoke(payload, ctx) + assert result.violation is not None + + await asyncio.sleep(1.1) + + result = await plugin.tool_pre_invoke(payload, ctx) + assert result.violation is None + + @pytest.mark.asyncio + async def test_token_bucket_independent_users(self, plugin): + """Token bucket tracks separate buckets per user.""" + ctx_alice = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + ctx_bob = PluginContext(global_context=GlobalContext(request_id="r2", user="bob")) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + for _ in range(3): + await plugin.tool_pre_invoke(payload, ctx_alice) + + alice_blocked = await plugin.tool_pre_invoke(payload, ctx_alice) + assert alice_blocked.violation is not None + + bob_allowed = await plugin.tool_pre_invoke(payload, ctx_bob) + assert bob_allowed.violation is None + + +class TestCrossHookSharing: + """Verify that prompt_pre_fetch and tool_pre_invoke share the same rate limit counters. + + Both hooks key by_user as 'user:{username}' and by_tenant as 'tenant:{tenant_id}'. + A user consuming quota via one hook must be counted against the same bucket + when using the other hook — the limit is per-identity, not per-hook. + """ + + @pytest.fixture + def plugin(self): + config = PluginConfig( + name="RateLimiter", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=["prompt_pre_fetch", "tool_pre_invoke"], + priority=100, + config={"by_user": "5/s"}, + ) + return RateLimiterPlugin(config) + + @pytest.mark.asyncio + async def test_prompt_and_tool_share_user_counter(self, plugin): + """Requests via prompt_pre_fetch and tool_pre_invoke decrement the same user bucket.""" + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + prompt_payload = PromptPrehookPayload(prompt_id="p", args={}) + tool_payload = ToolPreInvokePayload(name="tool", arguments={}) + + # 3 prompt requests + for _ in range(3): + result = await plugin.prompt_pre_fetch(prompt_payload, ctx) + assert result.violation is None + + # 2 tool requests — total 5, still within limit + for _ in range(2): + result = await plugin.tool_pre_invoke(tool_payload, ctx) + assert result.violation is None + + # 6th request (either hook) must be blocked + result = await plugin.tool_pre_invoke(tool_payload, ctx) + assert result.violation is not None, "6th request should be blocked — prompt and tool hooks must share the same user counter" + + @pytest.mark.asyncio + async def test_remaining_count_decrements_across_hooks(self, plugin): + """X-RateLimit-Remaining reflects consumption from both hooks.""" + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + prompt_payload = PromptPrehookPayload(prompt_id="p", args={}) + tool_payload = ToolPreInvokePayload(name="tool", arguments={}) + + r1 = await plugin.prompt_pre_fetch(prompt_payload, ctx) + remaining_after_prompt = int(r1.http_headers["X-RateLimit-Remaining"]) + + r2 = await plugin.tool_pre_invoke(tool_payload, ctx) + remaining_after_tool = int(r2.http_headers["X-RateLimit-Remaining"]) + + assert remaining_after_tool < remaining_after_prompt, "Remaining count must decrease after a tool request following a prompt request — same shared counter" + + @pytest.mark.asyncio + async def test_tenant_counter_shared_across_hooks_and_users(self, plugin): + """Tenant bucket is shared across all users in the same tenant, regardless of hook.""" + config = PluginConfig( + name="RateLimiter", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=["prompt_pre_fetch", "tool_pre_invoke"], + priority=100, + config={"by_user": "10/s", "by_tenant": "4/s"}, + ) + plugin = RateLimiterPlugin(config) + + ctx_alice = PluginContext(global_context=GlobalContext(request_id="r1", user="alice", tenant_id="team1")) + ctx_bob = PluginContext(global_context=GlobalContext(request_id="r2", user="bob", tenant_id="team1")) + + # Alice: 2 prompt requests + for _ in range(2): + await plugin.prompt_pre_fetch(PromptPrehookPayload(prompt_id="p", args={}), ctx_alice) + + # Bob: 2 tool requests — total 4 for team1, tenant limit reached + for _ in range(2): + await plugin.tool_pre_invoke(ToolPreInvokePayload(name="tool", arguments={}), ctx_bob) + + # 5th request from either user must be blocked by tenant limit + result = await plugin.prompt_pre_fetch(PromptPrehookPayload(prompt_id="p", args={}), ctx_alice) + assert result.violation is not None, "Tenant limit must be enforced across both users and both hooks" + + +class TestPermissiveMode: + """Permissive mode logs violations but never blocks requests. + + Mode enforcement is handled by PluginExecutor.execute_plugin(), not the + plugin itself. These tests go through PluginExecutor to exercise that path. + """ + + def _make_plugin_and_hook(self, limit: str) -> tuple: + config = PluginConfig( + name="RateLimiter", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=["tool_pre_invoke"], + priority=100, + mode=PluginMode.PERMISSIVE, + config={"by_user": limit}, + ) + plugin = RateLimiterPlugin(config) + hook_ref = HookRef("tool_pre_invoke", PluginRef(plugin)) + executor = PluginExecutor(timeout=5) + return plugin, hook_ref, executor + + @pytest.mark.asyncio + async def test_permissive_mode_does_not_raise_on_violation(self): + """PluginExecutor must not raise PluginViolationError in permissive mode.""" + plugin, hook_ref, executor = self._make_plugin_and_hook("1/s") + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + await executor.execute_plugin(hook_ref, payload, ctx, violations_as_exceptions=True) + + try: + result = await executor.execute_plugin(hook_ref, payload, ctx, violations_as_exceptions=True) + except PluginViolationError: + pytest.fail("PluginViolationError raised in permissive mode — should be suppressed by executor") + + # Violation info is surfaced for observability but request is not blocked + assert result.violation is not None + assert result.violation.http_status_code == 429 + + @pytest.mark.asyncio + async def test_permissive_mode_still_tracks_counters(self): + """Permissive mode still decrements the counter — backend store must grow.""" + plugin, hook_ref, executor = self._make_plugin_and_hook("10/s") + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + await executor.execute_plugin(hook_ref, payload, ctx, violations_as_exceptions=True) + await executor.execute_plugin(hook_ref, payload, ctx, violations_as_exceptions=True) + + # Counter must have been incremented — key exists in backend store + store = plugin._rate_backend._algorithm._store + assert len(store) > 0, "Permissive mode must still track counters in the backend store" + key = next(iter(store)) + assert store[key].count == 2, f"Expected count=2 after 2 requests, got {store[key].count}" + + @pytest.mark.asyncio + async def test_permissive_mode_contrast_with_enforce(self): + """Enforce mode raises PluginViolationError; permissive mode does not.""" + enforce_config = PluginConfig( + name="RateLimiter", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=["tool_pre_invoke"], + config={"by_user": "1/s"}, + mode=PluginMode.ENFORCE, + ) + enforce_plugin = RateLimiterPlugin(enforce_config) + enforce_ref = HookRef("tool_pre_invoke", PluginRef(enforce_plugin)) + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="tool", arguments={}) + executor = PluginExecutor(timeout=5) + + await executor.execute_plugin(enforce_ref, payload, ctx, violations_as_exceptions=True) + + with pytest.raises(PluginViolationError): + await executor.execute_plugin(enforce_ref, payload, ctx, violations_as_exceptions=True) + + +class TestDisabledMode: + """Disabled mode — PluginExecutor.execute() skips the plugin entirely. + + The disabled check lives in execute() (batch), not execute_plugin() (single), + so tests must go through execute() with a list of hook_refs. + """ + + def _make_plugin_and_refs(self) -> tuple: + config = PluginConfig( + name="RateLimiter", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=["tool_pre_invoke"], + priority=100, + mode=PluginMode.DISABLED, + config={"by_user": "1/s"}, + ) + plugin = RateLimiterPlugin(config) + hook_ref = HookRef("tool_pre_invoke", PluginRef(plugin)) + executor = PluginExecutor(timeout=5) + return plugin, [hook_ref], executor + + @pytest.mark.asyncio + async def test_disabled_mode_never_blocks(self): + """execute() skips a disabled plugin — no violation ever returned.""" + plugin, hook_refs, executor = self._make_plugin_and_refs() + global_ctx = GlobalContext(request_id="r1", user="alice") + payload = ToolPreInvokePayload(name="tool", arguments={}) + + for _ in range(10): + result, _ = await executor.execute(hook_refs, payload, global_ctx, "tool_pre_invoke", violations_as_exceptions=True) + assert result.violation is None, "Disabled plugin must never produce a violation" + + @pytest.mark.asyncio + async def test_disabled_mode_does_not_track_counters(self): + """execute() skips the plugin — backend store must remain empty.""" + plugin, hook_refs, executor = self._make_plugin_and_refs() + global_ctx = GlobalContext(request_id="r1", user="alice") + payload = ToolPreInvokePayload(name="tool", arguments={}) + + for _ in range(5): + await executor.execute(hook_refs, payload, global_ctx, "tool_pre_invoke", violations_as_exceptions=True) + + assert len(plugin._rate_backend._algorithm._store) == 0, "Disabled plugin must not write to the backend store — executor skips it entirely" + + def test_disabled_plugin_mode_property(self): + """Plugin mode property reflects the configured disabled mode.""" + plugin, _, _ = self._make_plugin_and_refs() + assert plugin.mode == PluginMode.DISABLED + + +class TestTenantIsolation: + """Tenant isolation tests reflecting the real production GlobalContext path. + + In production (mcpgateway/auth.py): + - global_context.tenant_id is always None (not derived from JWT teams) + - global_context.user is set as a dict {"email": ..., "is_admin": ..., "full_name": ...} + + These tests document the actual behaviour of the rate limiter under those + conditions so that regressions are caught if the production path changes. + """ + + @pytest.fixture + def plugin(self): + config = PluginConfig( + name="RateLimiter", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=["tool_pre_invoke"], + priority=100, + config={"by_user": "3/s", "by_tenant": "5/s"}, + ) + return RateLimiterPlugin(config) + + @pytest.mark.asyncio + async def test_user_dict_identity_is_rate_limited_independently(self, plugin): + """When user is a dict (production path), each distinct dict is a separate bucket. + + In production global_context.user = {"email": "alice@...", "is_admin": False, ...}. + The rate limiter uses this dict as the key via str(dict), so two users with + different email addresses must have independent per-user counters. + """ + alice_dict = {"email": "alice@example.com", "is_admin": False, "full_name": "Alice"} + bob_dict = {"email": "bob@example.com", "is_admin": False, "full_name": "Bob"} + + ctx_alice = PluginContext(global_context=GlobalContext(request_id="r1", user=alice_dict)) + ctx_bob = PluginContext(global_context=GlobalContext(request_id="r2", user=bob_dict)) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + for _ in range(3): + await plugin.tool_pre_invoke(payload, ctx_alice) + + alice_blocked = await plugin.tool_pre_invoke(payload, ctx_alice) + assert alice_blocked.violation is not None, "Alice must be blocked after exhausting her limit" + + bob_allowed = await plugin.tool_pre_invoke(payload, ctx_bob) + assert bob_allowed.violation is None, "Bob must have an independent counter — Alice's limit must not affect him" + + @pytest.mark.asyncio + async def test_explicit_tenant_id_isolates_teams(self, plugin): + """When tenant_id is explicitly set, different teams have independent tenant buckets. + + This is the behaviour a custom auth plugin would produce if it populates + global_context.tenant_id from the JWT teams claim. + """ + ctx_team1 = PluginContext(global_context=GlobalContext(request_id="r1", user="alice", tenant_id="team1")) + ctx_team2 = PluginContext(global_context=GlobalContext(request_id="r2", user="bob", tenant_id="team2")) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + # Exhaust team1's tenant limit (5/s) + for _ in range(5): + await plugin.tool_pre_invoke(payload, ctx_team1) + + team1_blocked = await plugin.tool_pre_invoke(payload, ctx_team1) + assert team1_blocked.violation is not None, "team1 must be blocked after 5 requests" + + # team2 must be unaffected — its own counter starts at 0 + team2_allowed = await plugin.tool_pre_invoke(payload, ctx_team2) + assert team2_allowed.violation is None, "team2 must have its own independent tenant bucket" + + @pytest.mark.asyncio + async def test_anonymous_user_has_separate_bucket_from_authenticated(self, plugin): + """Unauthenticated requests (user=None → 'anonymous') must not consume authenticated user quota.""" + ctx_anon = PluginContext(global_context=GlobalContext(request_id="r1", user=None)) + ctx_alice = PluginContext(global_context=GlobalContext(request_id="r2", user="alice")) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + # Exhaust anonymous bucket + for _ in range(3): + await plugin.tool_pre_invoke(payload, ctx_anon) + + anon_blocked = await plugin.tool_pre_invoke(payload, ctx_anon) + assert anon_blocked.violation is not None, "Anonymous bucket must be exhausted" + + # Alice must be unaffected + alice_allowed = await plugin.tool_pre_invoke(payload, ctx_alice) + assert alice_allowed.violation is None, "Authenticated user must have a separate bucket from anonymous" + + # ------------------------------------------------------------------ + # P0: desired behavior after the tenant_id propagation fix + # ------------------------------------------------------------------ + + @pytest.mark.asyncio + async def test_none_tenant_id_skips_by_tenant_entirely(self): + """When tenant_id is None, by_tenant must be skipped — not enforced against a shared 'default' bucket. + + Production path (mcpgateway/auth.py) always sets tenant_id=None. Bucketing + every request into 'tenant:default' creates a global shared limit that + cross-throttles unrelated users — worse than no tenant limiting at all. + + Expected behavior after fix: by_tenant is a no-op when tenant_id is absent. + Uses a high by_user limit so only by_tenant could trigger a block. + """ + config = PluginConfig( + name="RateLimiter", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=["tool_pre_invoke"], + priority=100, + config={"by_user": "100/s", "by_tenant": "5/s"}, + ) + plugin = RateLimiterPlugin(config) + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice", tenant_id=None)) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + # by_tenant limit is 5/s; without a real tenant, no request should be + # blocked by the tenant dimension regardless of how many we send. + for i in range(7): + result = await plugin.tool_pre_invoke(payload, ctx) + assert result.violation is None, f"Request {i + 1}: by_tenant must be skipped when tenant_id is None — " "no request should be blocked by a phantom 'default' tenant bucket" + + @pytest.mark.asyncio + async def test_multi_team_users_do_not_share_tenant_bucket(self, plugin): + """Two users with tenant_id=None must not throttle each other via a shared 'default' bucket. + + This is the multi-tenant deployment correctness test: if alice and bob are + from different organisations but both have tenant_id=None (e.g. multi-team + API tokens), a fake 'default' bucket would cross-throttle them. The plugin + must skip by_tenant for both instead. + """ + ctx_alice = PluginContext(global_context=GlobalContext(request_id="r1", user="alice", tenant_id=None)) + ctx_bob = PluginContext(global_context=GlobalContext(request_id="r2", user="bob", tenant_id=None)) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + # Alice sends 5 requests — tenant limit is 5/s + for _ in range(5): + await plugin.tool_pre_invoke(payload, ctx_alice) + + # Bob's first request must not be blocked — he should not share Alice's bucket + bob_result = await plugin.tool_pre_invoke(payload, ctx_bob) + assert bob_result.violation is None, "Bob must not be blocked by Alice's activity — " "users with tenant_id=None must not share a 'default' tenant bucket" + + @pytest.mark.asyncio + async def test_explicit_tenant_scopes_correctly_after_fix(self): + """P1: when tenant_id IS provided, by_tenant still enforces correctly. + + This is a regression guard: the fix must not break the case where tenant_id + is explicitly set (e.g. by a custom auth plugin or future auth-layer fix). + Uses a high by_user limit so only by_tenant can trigger a block. + """ + config = PluginConfig( + name="RateLimiter", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=["tool_pre_invoke"], + priority=100, + config={"by_user": "100/s", "by_tenant": "5/s"}, + ) + plugin = RateLimiterPlugin(config) + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice", tenant_id="org-acme")) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + for _ in range(5): + result = await plugin.tool_pre_invoke(payload, ctx) + assert result.violation is None + + blocked = await plugin.tool_pre_invoke(payload, ctx) + assert blocked.violation is not None, "by_tenant must still enforce when tenant_id is explicitly set" + + +class TestNoLimitsAndMissingContext: + """Behaviour when no limits are configured or GlobalContext fields are absent. + + These tests document the plugin's safe defaults so regressions are caught + if the fallback logic in prompt_pre_fetch / tool_pre_invoke changes. + """ + + @pytest.mark.asyncio + async def test_no_limits_configured_allows_all_requests(self): + """Plugin with all dimensions None must allow every request without tracking.""" + config = PluginConfig( + name="RateLimiter", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=["tool_pre_invoke"], + config={}, # no by_user, no by_tenant, no by_tool + ) + plugin = RateLimiterPlugin(config) + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + for _ in range(20): + result = await plugin.tool_pre_invoke(payload, ctx) + assert result.violation is None, "Unconfigured plugin must never block" + + @pytest.mark.asyncio + async def test_no_limits_configured_returns_no_headers(self): + """Plugin with no configured limits must not set X-RateLimit-* headers.""" + config = PluginConfig( + name="RateLimiter", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=["tool_pre_invoke"], + config={}, + ) + plugin = RateLimiterPlugin(config) + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + result = await plugin.tool_pre_invoke(payload, ctx) + assert not result.http_headers, "No limits configured — X-RateLimit-* headers must not be present" + + @pytest.mark.asyncio + async def test_none_user_defaults_to_anonymous_bucket(self): + """user=None in GlobalContext must fall back to 'anonymous' as the rate limit key.""" + config = PluginConfig( + name="RateLimiter", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=["tool_pre_invoke"], + config={"by_user": "2/s"}, + ) + plugin = RateLimiterPlugin(config) + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user=None)) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + await plugin.tool_pre_invoke(payload, ctx) + await plugin.tool_pre_invoke(payload, ctx) + + result = await plugin.tool_pre_invoke(payload, ctx) + assert result.violation is not None, "user=None must be treated as 'anonymous' and enforced" + + # Confirm the key in the store is 'user:anonymous' + store = plugin._rate_backend._algorithm._store + assert any("anonymous" in k for k in store), "Expected 'anonymous' bucket key in store when user=None" + + @pytest.mark.asyncio + async def test_none_tenant_id_skips_by_tenant_check(self): + """tenant_id=None in GlobalContext must skip the by_tenant check entirely — no 'default' bucket.""" + config = PluginConfig( + name="RateLimiter", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=["tool_pre_invoke"], + config={"by_tenant": "2/s"}, + ) + plugin = RateLimiterPlugin(config) + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice", tenant_id=None)) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + # With no by_user limit, by_tenant is the only dimension — but it must be skipped + for _ in range(3): + result = await plugin.tool_pre_invoke(payload, ctx) + assert result.violation is None, "by_tenant must be skipped when tenant_id is None" + + store = plugin._rate_backend._algorithm._store + assert not any("tenant" in k for k in store), "No tenant bucket must be created in the store when tenant_id is None" + + @pytest.mark.asyncio + async def test_both_user_and_tenant_none_still_enforces(self): + """With both user=None and tenant_id=None the plugin must still enforce limits.""" + config = PluginConfig( + name="RateLimiter", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=["tool_pre_invoke"], + config={"by_user": "2/s", "by_tenant": "10/s"}, + ) + plugin = RateLimiterPlugin(config) + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user=None, tenant_id=None)) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + await plugin.tool_pre_invoke(payload, ctx) + await plugin.tool_pre_invoke(payload, ctx) + + result = await plugin.tool_pre_invoke(payload, ctx) + assert result.violation is not None, "With user=None and tenant_id=None the plugin must still enforce via anonymous/default buckets" + + @pytest.mark.asyncio + async def test_separate_plugin_instances_have_independent_stores(self): + """Two RateLimiterPlugin instances must never share backend state.""" + + def make_plugin(): + return RateLimiterPlugin( + PluginConfig( + name="RateLimiter", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=["tool_pre_invoke"], + config={"by_user": "2/s"}, + ) + ) + + plugin_a = make_plugin() + plugin_b = make_plugin() + + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + # Exhaust plugin_a + await plugin_a.tool_pre_invoke(payload, ctx) + await plugin_a.tool_pre_invoke(payload, ctx) + a_blocked = await plugin_a.tool_pre_invoke(payload, ctx) + assert a_blocked.violation is not None + + # plugin_b must be completely unaffected + b_allowed = await plugin_b.tool_pre_invoke(payload, ctx) + assert b_allowed.violation is None, "Two plugin instances must have independent stores — exhausting one must not affect the other" + + +# ============================================================================= +# Redis Backend Integration Tests +# ============================================================================= +# +# These tests require a real Redis instance. They are skipped automatically +# when Redis is not reachable and Docker cannot start one. Each test flushes +# DB 15 before use to avoid cross-test contamination. +# +# Run with: uv run pytest tests/integration/test_rate_limiter.py -k Redis -v +# ============================================================================= + + +def _redis_port_open(host: str = "127.0.0.1", port: int = 6379, timeout: float = 0.2) -> bool: + """Return True if a TCP connection to host:port succeeds.""" + try: + with socket.create_connection((host, port), timeout=timeout): + return True + except Exception: + return False + + +@pytest.fixture(scope="module") +def redis_url_for_integration(): + """Yield a Redis URL pointing at a real Redis instance. + + Tries localhost:6379 first. If not reachable, attempts to start a + temporary Docker container. Skips the test module if neither works. + Container is stopped automatically after all tests in the module finish. + """ + try: + # Third-Party + import redis.asyncio # noqa: F401 + except Exception: + pytest.skip("redis.asyncio package not installed") + + host, port = "127.0.0.1", 6379 + container_id = None + + if not _redis_port_open(host, port): + try: + res = subprocess.run( + ["docker", "run", "-d", "--rm", "-p", f"{port}:6379", "--name", "pytest-rl-redis-integ", "redis:7"], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + container_id = res.stdout.strip() + except Exception as exc: + pytest.skip(f"Redis unavailable and docker start failed: {exc}") + + for _ in range(50): + if _redis_port_open(host, port): + break + time.sleep(0.1) + else: + if container_id: + subprocess.run(["docker", "stop", container_id], check=False) + pytest.skip("Redis did not start in time") + + yield f"redis://{host}:{port}/15" # DB 15 — isolated from other data + + if container_id: + subprocess.run(["docker", "stop", container_id], check=False) + + +def _make_redis_plugin(redis_url: str, algorithm: str = "fixed_window", limit: str = "3/s") -> RateLimiterPlugin: + """Create a RateLimiterPlugin backed by real Redis.""" + return RateLimiterPlugin( + PluginConfig( + name="RateLimiter", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=["tool_pre_invoke"], + priority=100, + config={ + "by_user": limit, + "backend": "redis", + "redis_url": redis_url, + "algorithm": algorithm, + }, + ) + ) + + +async def _flush_redis(redis_url: str) -> None: + """Flush DB 15 before each test to ensure a clean slate.""" + # Third-Party + import redis.asyncio as aioredis # noqa: PLC0415 + + client = aioredis.from_url(redis_url) + await client.flushdb() + await client.aclose() + + +class TestRedisBackendIntegration: + """End-to-end integration tests for the Redis backend. + + Validates plugin wiring, shared-counter semantics, TTL/window reset + behavior, and fallback behavior against a real Redis-backed gateway flow. + """ + + @pytest.mark.asyncio + async def test_redis_plugin_enforces_limit(self, redis_url_for_integration): + """Plugin wired to real Redis blocks on N+1 requests within the window.""" + await _flush_redis(redis_url_for_integration) + + plugin = _make_redis_plugin(redis_url_for_integration, algorithm="fixed_window", limit="3/s") + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + for _ in range(3): + result = await plugin.tool_pre_invoke(payload, ctx) + assert result.violation is None + + result = await plugin.tool_pre_invoke(payload, ctx) + assert result.violation is not None + assert result.violation.http_status_code == 429 + + @pytest.mark.asyncio + async def test_redis_shared_counter_across_plugin_instances(self, redis_url_for_integration): + """Two plugin instances pointing at the same Redis share rate limit counters. + + This is the core multi-instance correctness test: after instance A exhausts + the limit, instance B must be blocked because they share the same Redis key. + """ + await _flush_redis(redis_url_for_integration) + + plugin_a = _make_redis_plugin(redis_url_for_integration, limit="3/s") + plugin_b = _make_redis_plugin(redis_url_for_integration, limit="3/s") + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + for _ in range(3): + result = await plugin_a.tool_pre_invoke(payload, ctx) + assert result.violation is None + + result = await plugin_b.tool_pre_invoke(payload, ctx) + assert result.violation is not None, "Redis backend must share counters across plugin instances — " "instance B must be blocked after instance A exhausts the limit" + + @pytest.mark.asyncio + async def test_redis_window_resets_after_ttl(self, redis_url_for_integration): + """After the rate window expires, Redis TTL resets counters and requests are allowed again.""" + await _flush_redis(redis_url_for_integration) + + plugin = _make_redis_plugin(redis_url_for_integration, algorithm="fixed_window", limit="2/s") + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + await plugin.tool_pre_invoke(payload, ctx) + await plugin.tool_pre_invoke(payload, ctx) + blocked = await plugin.tool_pre_invoke(payload, ctx) + assert blocked.violation is not None + + # Wait for the 1-second window to expire via real Redis TTL + await asyncio.sleep(1.1) + + result = await plugin.tool_pre_invoke(payload, ctx) + assert result.violation is None, "After the rate window expires, Redis TTL must reset counters and allow fresh requests" + + @pytest.mark.asyncio + async def test_redis_fallback_to_memory_on_unavailable_redis(self): + """Plugin with an unreachable Redis URL falls back to memory backend without crashing.""" + plugin = _make_redis_plugin("redis://127.0.0.1:19999/0", limit="3/s") + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + # Must not raise — fallback to memory backend should handle the request + result = await plugin.tool_pre_invoke(payload, ctx) + assert result.violation is None, "Plugin must fall back to memory backend when Redis is unavailable — must not crash" + + # ------------------------------------------------------------------ + # sliding_window on real Redis + # ------------------------------------------------------------------ + + @pytest.mark.asyncio + async def test_redis_sliding_window_enforces_limit(self, redis_url_for_integration): + """sliding_window on real Redis blocks on N+1 requests within the window.""" + await _flush_redis(redis_url_for_integration) + + plugin = _make_redis_plugin(redis_url_for_integration, algorithm="sliding_window", limit="3/s") + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + for _ in range(3): + result = await plugin.tool_pre_invoke(payload, ctx) + assert result.violation is None + + result = await plugin.tool_pre_invoke(payload, ctx) + assert result.violation is not None + assert result.violation.http_status_code == 429 + + @pytest.mark.asyncio + async def test_redis_sliding_window_shared_counter_across_instances(self, redis_url_for_integration): + """Two sliding_window plugin instances share counters via Redis. + + After instance A exhausts the limit, instance B must be blocked because + they share the same Redis sorted-set key. + """ + await _flush_redis(redis_url_for_integration) + + plugin_a = _make_redis_plugin(redis_url_for_integration, algorithm="sliding_window", limit="3/s") + plugin_b = _make_redis_plugin(redis_url_for_integration, algorithm="sliding_window", limit="3/s") + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + for _ in range(3): + result = await plugin_a.tool_pre_invoke(payload, ctx) + assert result.violation is None + + result = await plugin_b.tool_pre_invoke(payload, ctx) + assert result.violation is not None, "sliding_window Redis backend must share counters across instances — " "instance B must be blocked after instance A exhausts the limit" + + @pytest.mark.asyncio + async def test_redis_sliding_window_resets_after_window(self, redis_url_for_integration): + """After the sliding window elapses, Redis TTL resets the sorted set and requests are allowed again.""" + await _flush_redis(redis_url_for_integration) + + plugin = _make_redis_plugin(redis_url_for_integration, algorithm="sliding_window", limit="2/s") + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + await plugin.tool_pre_invoke(payload, ctx) + await plugin.tool_pre_invoke(payload, ctx) + blocked = await plugin.tool_pre_invoke(payload, ctx) + assert blocked.violation is not None + + await asyncio.sleep(1.1) + + result = await plugin.tool_pre_invoke(payload, ctx) + assert result.violation is None, "After the sliding window elapses, Redis TTL must reset the sorted set and allow fresh requests" + + # ------------------------------------------------------------------ + # token_bucket on real Redis + # ------------------------------------------------------------------ + + @pytest.mark.asyncio + async def test_redis_token_bucket_enforces_limit(self, redis_url_for_integration): + """token_bucket on real Redis blocks when bucket is empty.""" + await _flush_redis(redis_url_for_integration) + + plugin = _make_redis_plugin(redis_url_for_integration, algorithm="token_bucket", limit="3/s") + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + for _ in range(3): + result = await plugin.tool_pre_invoke(payload, ctx) + assert result.violation is None + + result = await plugin.tool_pre_invoke(payload, ctx) + assert result.violation is not None + assert result.violation.http_status_code == 429 + + @pytest.mark.asyncio + async def test_redis_token_bucket_shared_counter_across_instances(self, redis_url_for_integration): + """Two token_bucket plugin instances share bucket state via Redis. + + After instance A drains the bucket, instance B must be blocked because + they share the same Redis hash key. + """ + await _flush_redis(redis_url_for_integration) + + plugin_a = _make_redis_plugin(redis_url_for_integration, algorithm="token_bucket", limit="3/s") + plugin_b = _make_redis_plugin(redis_url_for_integration, algorithm="token_bucket", limit="3/s") + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + for _ in range(3): + result = await plugin_a.tool_pre_invoke(payload, ctx) + assert result.violation is None + + result = await plugin_b.tool_pre_invoke(payload, ctx) + assert result.violation is not None, "token_bucket Redis backend must share bucket state across instances — " "instance B must be blocked after instance A drains the bucket" + + @pytest.mark.asyncio + async def test_redis_token_bucket_refills_over_time(self, redis_url_for_integration): + """After the bucket drains, tokens refill over time and requests are allowed again.""" + await _flush_redis(redis_url_for_integration) + + plugin = _make_redis_plugin(redis_url_for_integration, algorithm="token_bucket", limit="2/s") + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + await plugin.tool_pre_invoke(payload, ctx) + await plugin.tool_pre_invoke(payload, ctx) + blocked = await plugin.tool_pre_invoke(payload, ctx) + assert blocked.violation is not None + + await asyncio.sleep(1.1) + + result = await plugin.tool_pre_invoke(payload, ctx) + assert result.violation is None, "After tokens refill over time, token_bucket Redis backend must allow requests again" + + +# ============================================================================= +# Auth boundary deny-path tests (HTTP level) +# ============================================================================= + + +class TestRateLimiterAuthBoundary: + """Deny-path tests: unauthenticated requests must get 401 before + reaching the rate limiter. These use the FastAPI TestClient fixture. + """ + + def test_unauthenticated_prompt_request_returns_401(self, client): + """Unauthenticated request to /api/v1/prompts/ must receive 401.""" + response = client.get(PROMPT_ENDPOINT) + assert response.status_code == 401, "Unauthenticated request must be rejected with 401 before hitting rate limiter" + + def test_unauthenticated_tool_invoke_returns_401(self, client): + """Unauthenticated request to /api/v1/tools/invoke must receive 401.""" + response = client.post(TOOL_INVOKE_ENDPOINT, json={"name": "test", "arguments": {}}) + assert response.status_code == 401, "Unauthenticated tool invoke must be rejected with 401 before hitting rate limiter" diff --git a/tests/loadtest/locustfile_rate_limiter_backend_correctness.py b/tests/loadtest/locustfile_rate_limiter_backend_correctness.py new file mode 100644 index 0000000000..db61770d37 --- /dev/null +++ b/tests/loadtest/locustfile_rate_limiter_backend_correctness.py @@ -0,0 +1,506 @@ +# -*- coding: utf-8 -*- +"""Rate limiter correctness load test. + +Validates that the RateLimiterPlugin enforces per-user limits correctly across +multiple gateway instances. + +How it works +------------ +A single user sends requests at a fixed pace of 1 req/s (60 req/min) — exactly +twice the default 30/m per-user limit. nginx round-robins across 3 gateway +instances, so each instance sees ~20 req/min from this user. + + Memory backend (broken, pre-fix) + Each instance has its own counter. 20 req/min < 30/m limit on every + instance → user is NEVER blocked. Effective limit = 3 × 30 = 90/m. + Expected result: ~0% failures. + + Redis backend (fixed) + All instances share one counter. 60 req/min > 30/m → user is blocked + after the first 30 requests in each 60-second window. + Expected result: ~50% failures. + +The ~50% vs ~0% difference is visible at a glance in the results table. + +Usage +----- + make benchmark-rate-limiter + + # Or direct invocation: + locust -f tests/loadtest/locustfile_rate_limiter_backend_correctness.py \\ + --host=http://localhost:8080 \\ + --users=1 --spawn-rate=1 --run-time=120s \\ + --headless RateLimitedUser + + # To test with a different limit, set by_user in plugins/config.yaml and + # pass the configured value so the banner is accurate: + RL_LIMIT_PER_MIN=60 make benchmark-rate-limiter + +Environment Variables +--------------------- + MCP_SERVER_ID: Virtual server UUID (auto-detected if empty) + JWT_SECRET_KEY: JWT signing secret (default: my-test-key-but-now-longer-than-32-bytes) + JWT_ALGORITHM: JWT algorithm (default: HS256) + JWT_AUDIENCE: JWT audience (default: mcpgateway-api) + JWT_ISSUER: JWT issuer (default: mcpgateway) + PLATFORM_ADMIN_EMAIL Admin email for auth (default: admin@example.com) + RL_LIMIT_PER_MIN: Configured rate limit displayed in output banner + (default: 30) + +Copyright 2026 +SPDX-License-Identifier: Apache-2.0 +""" + +# Standard +from datetime import datetime, timedelta, timezone +import json +import logging +import os +from pathlib import Path +from typing import Any +import uuid + +# Third-Party +from locust import constant_throughput, events, tag, task +from locust.contrib.fasthttp import FastHttpUser +from locust.runners import WorkerRunner + +# ============================================================================= +# Configuration +# ============================================================================= + + +def _load_env_file() -> dict[str, str]: + """Load .env file from project root.""" + env_vars: dict[str, str] = {} + search_paths = [ + Path.cwd() / ".env", + Path.cwd().parent / ".env", + Path.cwd().parent.parent / ".env", + Path(__file__).parent.parent.parent / ".env", + ] + for path in search_paths: + if path.exists(): + with open(path, "r", encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line or line.startswith("#"): + continue + if "=" in line: + key, _, value = line.partition("=") + env_vars[key.strip()] = value.strip().strip("\"'") + break + return env_vars + + +_ENV = _load_env_file() + + +def _cfg(key: str, default: str = "") -> str: + return os.environ.get(key) or _ENV.get(key) or default + + +JWT_SECRET_KEY = _cfg("JWT_SECRET_KEY", "my-test-key-but-now-longer-than-32-bytes") +JWT_ALGORITHM = _cfg("JWT_ALGORITHM", "HS256") +JWT_AUDIENCE = _cfg("JWT_AUDIENCE", "mcpgateway-api") +JWT_ISSUER = _cfg("JWT_ISSUER", "mcpgateway") +ADMIN_EMAIL = _cfg("PLATFORM_ADMIN_EMAIL", "admin@example.com") +MCP_SERVER_ID = _cfg("MCP_SERVER_ID", "") + +# Rate limit as configured in plugins/config.yaml — only used for the banner. +RL_LIMIT_PER_MIN = int(_cfg("RL_LIMIT_PER_MIN", "30")) + +# Fixed pace: 1 req/s = 60 req/min = 2× the default 30/m limit. +# Each of the 3 gateway instances sees ~20 req/min (below the per-instance +# threshold of 30/m in the memory backend but over the shared limit in Redis). +_REQS_PER_SECOND = 1.0 + +logging.basicConfig(level=logging.ERROR) +logger = logging.getLogger(__name__) + +# ============================================================================= +# Shared state +# ============================================================================= + +_server_id: str = "" +_tool_names: list[str] = [] +_detect_done = False + + +# ============================================================================= +# JWT token +# ============================================================================= + + +def _make_token() -> str: + """Generate a JWT for the admin user (guaranteed to exist in the DB).""" + # Third-Party + import jwt # pylint: disable=import-outside-toplevel + + payload = { + "sub": ADMIN_EMAIL, + "exp": datetime.now(timezone.utc) + timedelta(hours=8760), + "iat": datetime.now(timezone.utc), + "aud": JWT_AUDIENCE, + "iss": JWT_ISSUER, + "jti": str(uuid.uuid4()), + "token_use": "session", + "user": { + "email": ADMIN_EMAIL, + "full_name": "Rate Limit Load Test", + "is_admin": True, + "auth_provider": "local", + }, + } + return jwt.encode(payload, JWT_SECRET_KEY, algorithm=JWT_ALGORITHM) + + +_token: str = "" + + +def _get_token() -> str: + global _token # pylint: disable=global-statement + if not _token: + _token = _make_token() + return _token + + +# ============================================================================= +# Auto-detect server and tools +# ============================================================================= + + +def _auto_detect(host: str) -> None: + global _server_id, _tool_names, _detect_done # pylint: disable=global-statement + if _detect_done: + return + _detect_done = True + + # Third-Party + import requests # pylint: disable=import-outside-toplevel + + headers = {"Authorization": f"Bearer {_get_token()}", "Accept": "application/json"} + + if MCP_SERVER_ID: + _server_id = MCP_SERVER_ID + else: + try: + resp = requests.get(f"{host}/servers", headers=headers, timeout=10) + servers = resp.json() if resp.status_code == 200 else [] + if isinstance(servers, list) and servers: + _server_id = servers[0].get("id", "") + except Exception as exc: + logger.warning("Server auto-detect failed: %s", exc) + + if _server_id: + try: + payload = {"jsonrpc": "2.0", "id": "1", "method": "tools/list", "params": {}} + resp = requests.post( + f"{host}/servers/{_server_id}/mcp", + json=payload, + headers={**headers, "Content-Type": "application/json"}, + timeout=10, + ) + if resp.status_code == 200: + result = resp.json().get("result", {}) + _tool_names = [t["name"] for t in result.get("tools", [])] + except Exception as exc: + logger.warning("Tool auto-detect failed: %s", exc) + + logger.info("Rate limiter test: server=%s tools=%s", _server_id, _tool_names) + + +# ============================================================================= +# Event handlers +# ============================================================================= + + +@events.init_command_line_parser.add_listener +def set_defaults(parser): + # Default: 1 user, 120s — enough to see two full 60-second rate limit windows + parser.set_defaults(users=1, spawn_rate=1, run_time="120s") + + +@events.test_start.add_listener +def on_test_start(environment, **kwargs): + host = environment.host or "http://localhost:8080" + _auto_detect(host) + if isinstance(environment.runner, WorkerRunner): + return + + reqs_per_min = int(_REQS_PER_SECOND * 60) + memory_per_instance = reqs_per_min // 3 + expected_blocked_pct = max(0.0, (reqs_per_min - RL_LIMIT_PER_MIN) / reqs_per_min * 100) + + logger.error("=" * 70) + logger.error("RATE LIMITER CORRECTNESS TEST") + logger.error("=" * 70) + logger.error(" Host: %s", host) + logger.error(" Server: %s", _server_id) + logger.error(" Tools: %s", ", ".join(_tool_names[:5]) or "(none)") + logger.error(" User: %s", ADMIN_EMAIL) + logger.error(" Rate limit: %d req/min (as configured in plugins/config.yaml)", RL_LIMIT_PER_MIN) + logger.error(" Test pace: %.0f req/s = %d req/min (%.0fx the limit)", _REQS_PER_SECOND, reqs_per_min, reqs_per_min / RL_LIMIT_PER_MIN) + logger.error(" Per-instance rate: ~%d req/min (nginx round-robin across 3 instances)", memory_per_instance) + logger.error("") + logger.error(" Memory backend: ~0%% failures — each instance sees %d req/min < %d/m limit", memory_per_instance, RL_LIMIT_PER_MIN) + logger.error(" Redis backend: ~%.0f%% failures — shared counter: %d req/min > %d/m limit", expected_blocked_pct, reqs_per_min, RL_LIMIT_PER_MIN) + logger.error("=" * 70) + + +@events.test_stop.add_listener +def on_test_stop(environment, **kwargs): + if isinstance(environment.runner, WorkerRunner): + return + + stats = environment.stats + total = stats.total.num_requests + infra_fails = stats.total.num_failures + + # Rate-limited requests: tracked as the "[rate-limited]" named entry + rl_entry = stats.entries.get(("MCP tools/call [rate-limited]", "POST"), None) + rl_count = rl_entry.num_requests if rl_entry else 0 + allowed_entry = stats.entries.get(("MCP tools/call [allowed]", "POST"), None) + allowed_count = allowed_entry.num_requests if allowed_entry else 0 + + infra_pct = (infra_fails / total * 100) if total > 0 else 0 + rl_pct = (rl_count / total * 100) if total > 0 else 0 + reqs_per_min = int(_REQS_PER_SECOND * 60) + expected_pct = max(0.0, (reqs_per_min - RL_LIMIT_PER_MIN) / reqs_per_min * 100) + + print("\n" + "=" * 90) + print("RATE LIMITER CORRECTNESS — RESULTS") + print("=" * 90) + print(f"\n {'OVERALL':^86}") + print(" " + "-" * 86) + print(f" Total tool call requests: {total:>8,} (tool calls sent)") + print(f" Allowed through: {allowed_count:>8,}") + print(f" Rate-limited (blocked): {rl_count:>8,} ({rl_pct:.1f}% | expected ~{expected_pct:.0f}% with Redis)") + print(f" Infrastructure failures: {infra_fails:>8,} ({infra_pct:.1f}%)") + print() + print(f" Configured limit: {RL_LIMIT_PER_MIN} req/min per user") + print(f" Test pace: {reqs_per_min} req/min (~{reqs_per_min // 3} req/min per gateway instance)") + print() + + if rl_pct >= expected_pct * 0.8: + verdict = f"✅ REDIS BACKEND — limit correctly enforced ({rl_pct:.0f}% blocked, expected ~{expected_pct:.0f}%)" + elif rl_pct < 2 and total > 50: + verdict = f"❌ MEMORY BACKEND — limit NOT enforced across instances ({rl_pct:.0f}% blocked, expected ~{expected_pct:.0f}%)" + else: + verdict = f"⚠️ INCONCLUSIVE — {rl_pct:.0f}% blocked (expected ~{expected_pct:.0f}%)" + + print(f" Verdict: {verdict}") + + if total > 0: + print("\n Response Times (all requests, ms):") + print(f" Average: {stats.total.avg_response_time:>8.1f}") + print(f" p50: {stats.total.get_response_time_percentile(0.50):>8.1f}") + print(f" p90: {stats.total.get_response_time_percentile(0.90):>8.1f}") + print(f" p99: {stats.total.get_response_time_percentile(0.99):>8.1f}") + + entries = sorted(stats.entries.values(), key=lambda e: e.num_requests, reverse=True) + if entries: + print(f"\n {'BREAKDOWN':^86}") + print(" " + "-" * 86) + print(f" {'Name':<50} {'Reqs':>8} {'Fails':>8} {'Avg(ms)':>8}") + print(" " + "-" * 86) + for entry in entries[:10]: + print(f" {entry.name:<50} {entry.num_requests:>8,} {entry.num_failures:>8,} {entry.avg_response_time:>8.1f}") + + print("\n" + "=" * 90 + "\n") + + +# ============================================================================= +# Helpers +# ============================================================================= + + +def _jsonrpc(method: str, params: dict | None = None) -> dict[str, Any]: + body: dict[str, Any] = {"jsonrpc": "2.0", "id": str(uuid.uuid4()), "method": method} + if params is not None: + body["params"] = params + return body + + +# ============================================================================= +# RateLimitedUser +# ============================================================================= + + +class RateLimitedUser(FastHttpUser): + """Sends tool calls at a fixed pace designed to expose the multi-instance + rate limit enforcement gap. + + Pace: 1 req/s = 60 req/min = 2× the default 30/m per-user limit. + + With 3 gateway instances behind nginx: + - Each instance sees ~20 req/min → below the per-instance limit + - Memory backend: never blocked → ~0% failures (the bug) + - Redis backend: blocked after 30/min → ~50% failures (the fix) + + Rate-limited responses (isError: true in the MCP result) are re-issued as + a separate named POST so they appear as a distinct row in the results table. + """ + + wait_time = constant_throughput(_REQS_PER_SECOND) + connection_timeout = 30.0 + network_timeout = 30.0 + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._mcp_session_id: str | None = None + self._initialized = False + + def _headers(self) -> dict[str, str]: + h = { + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": f"Bearer {_get_token()}", + } + if self._mcp_session_id: + h["Mcp-Session-Id"] = self._mcp_session_id + return h + + def _mcp_post(self, method: str, params: dict | None, name: str) -> dict | None: + if not _server_id: + return None + try: + with self.client.post( + f"/servers/{_server_id}/mcp", + data=json.dumps(_jsonrpc(method, params)), + headers=self._headers(), + name=name, + catch_response=True, + ) as response: + sid = response.headers.get("Mcp-Session-Id") if response.headers else None + if sid: + self._mcp_session_id = sid + + if response.status_code in (502, 503, 504): + response.failure(f"Infrastructure error: {response.status_code}") + return None + if response.status_code != 200: + response.failure(f"HTTP {response.status_code}") + return None + try: + data = response.json() + except Exception: + response.failure("Invalid JSON") + return None + if data is None: + response.failure("Null response") + return None + if "error" in data: + err = data["error"] + response.failure(f"JSON-RPC error {err.get('code', '?')}: {err.get('message', '?')}") + return None + response.success() + return data.get("result") + except Exception as exc: + logger.warning("Request failed (%s): %s", name, exc) + return None + + def _ensure_initialized(self) -> None: + if self._initialized or not _server_id: + return + result = self._mcp_post( + "initialize", + { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": {"name": "locust-rate-limiter-test", "version": "1.0.0"}, + }, + "MCP initialize", + ) + if result is not None: + self._initialized = True + + def on_start(self) -> None: + self._ensure_initialized() + + @task + @tag("rate-limit", "tools", "call") + def call_tool(self) -> None: + """Call a tool; classify the response in-place as allowed, rate-limited, or infra-error. + + A single tools/call request is sent. The Locust stat name is set based on + the semantic outcome — no second request is fired: + - 'MCP tools/call [allowed]' — gateway processed the call normally + - 'MCP tools/call [rate-limited]' — HTTP 429 or MCP result.isError (plugin rate-limit block) + - 'MCP tools/call [infra-error]' — HTTP error or malformed response + + Rate-limited responses are recorded as success() so they do not inflate + the infrastructure failure count. Block-rate and latency numbers are + purely observational — the harness is never a participant in rate limiting. + """ + if not _tool_names or not _server_id: + return + + tool = _tool_names[0] + name_lower = tool.lower() + if "time" in name_lower or "timezone" in name_lower: + args: dict[str, Any] = {"timezone": "UTC"} + elif "convert" in name_lower: + args = {"time": "2025-01-01T00:00:00Z", "source_timezone": "UTC", "target_timezone": "Europe/London"} + elif "echo" in name_lower: + args = {"message": "rate-limit-test"} + else: + args = {} + + try: + with self.client.post( + f"/servers/{_server_id}/mcp", + data=json.dumps(_jsonrpc("tools/call", {"name": tool, "arguments": args})), + headers=self._headers(), + name="MCP tools/call", + catch_response=True, + ) as response: + sid = response.headers.get("Mcp-Session-Id") if response.headers else None + if sid: + self._mcp_session_id = sid + + if response.status_code == 429: + response.request_meta["name"] = "MCP tools/call [rate-limited]" + response.success() + return + if response.status_code in (502, 503, 504): + response.request_meta["name"] = "MCP tools/call [infra-error]" + response.failure(f"Infrastructure error: {response.status_code}") + return + if response.status_code != 200: + response.request_meta["name"] = "MCP tools/call [infra-error]" + response.failure(f"HTTP {response.status_code}") + return + try: + data = response.json() + except Exception: + response.request_meta["name"] = "MCP tools/call [infra-error]" + response.failure("Invalid JSON") + return + if data is None: + response.request_meta["name"] = "MCP tools/call [infra-error]" + response.failure("Null response") + return + if "error" in data: + err = data["error"] + err_msg = str(err.get("message", "")).lower() + err_data_str = str(err.get("data", "")).lower() + # Rate-limit violations may arrive as JSON-RPC errors when the + # PluginViolationError is caught by FastAPI's global handler. + if "rate" in err_msg or "rate_limit" in err_data_str or err.get("code") == 429: + response.request_meta["name"] = "MCP tools/call [rate-limited]" + response.success() + return + response.request_meta["name"] = "MCP tools/call [infra-error]" + response.failure(f"JSON-RPC error {err.get('code', '?')}: {err.get('message', '?')}") + return + + result = data.get("result") + if isinstance(result, dict) and result.get("isError"): + response.request_meta["name"] = "MCP tools/call [rate-limited]" + response.success() + else: + response.request_meta["name"] = "MCP tools/call [allowed]" + response.success() + except Exception as exc: + logger.warning("Request failed (tools/call): %s", exc) diff --git a/tests/loadtest/locustfile_rate_limiter_redis_capacity.py b/tests/loadtest/locustfile_rate_limiter_redis_capacity.py new file mode 100644 index 0000000000..b92ce5504c --- /dev/null +++ b/tests/loadtest/locustfile_rate_limiter_redis_capacity.py @@ -0,0 +1,638 @@ +# -*- coding: utf-8 -*- +"""Rate limiter Redis capacity test on the prompt pre-fetch path. + +This benchmark is intentionally narrower than the existing tools/call scale test. +It exercises: + + client -> nginx -> 3 gateways -> auth -> prompt_pre_fetch -> Redis rate limiter -> prompt render + +It avoids downstream MCP tool invocation so the measurement stays focused on the +Redis-backed rate limiter path. The benchmark is intended to answer: + + "How many concurrent users can the async Redis hot path sustain at a given pace + while preserving correct shared-counter behavior?" + +Usage: + docker exec mcp-context-forge-redis-1 redis-cli FLUSHDB + make benchmark-rate-limiter-redis-capacity + +Environment Variables: + RL_USERS: Concurrent users (default: 100) + RL_SPAWN_RATE: User spawn rate (default: 10) + RL_RUN_TIME: Run duration (default: 120s) + RL_REQS_PER_SECOND: Per-user request pace (default: 0.25) + RL_LIMIT_PER_MIN: Configured limit for the output banner only (default: 30) + RL_PROMPT_ID: Optional prompt UUID to target (auto-detected if empty) + JWT_SECRET_KEY: JWT signing secret (default: my-test-key) + JWT_ALGORITHM: JWT algorithm (default: HS256) + JWT_AUDIENCE: JWT audience (default: mcpgateway-api) + JWT_ISSUER: JWT issuer (default: mcpgateway) + DOCKER_GATEWAY_PATTERN: Gateway container pattern (default: mcp-context-forge-gateway) + DOCKER_REDIS_CONTAINER: Redis container name (default: mcp-context-forge-redis-1) + +Copyright 2026 +SPDX-License-Identifier: Apache-2.0 +""" + +# Standard +from dataclasses import dataclass +from datetime import timedelta +import logging +import os +from pathlib import Path +import re +import subprocess +import tempfile +import threading +import uuid + +# Third-Party +from locust import constant_throughput, events, tag, task +from locust.contrib.fasthttp import FastHttpUser +from locust.runners import WorkerRunner + + +def _load_env_file() -> dict[str, str]: + """Load .env values from the repository root.""" + env_vars: dict[str, str] = {} + search_paths = [ + Path.cwd() / ".env", + Path.cwd().parent / ".env", + Path.cwd().parent.parent / ".env", + Path(__file__).parent.parent.parent / ".env", + ] + for path in search_paths: + if path.exists(): + with open(path, "r", encoding="utf-8", errors="replace") as handle: + for line in handle: + line = line.strip() + if not line or line.startswith("#"): + continue + if "=" in line: + key, _, value = line.partition("=") + env_vars[key.strip()] = value.strip().strip("\"'") + break + return env_vars + + +_ENV = _load_env_file() + + +def _cfg(key: str, default: str = "") -> str: + return os.environ.get(key) or _ENV.get(key) or default + + +JWT_SECRET_KEY = _cfg("JWT_SECRET_KEY", "my-test-key") +JWT_ALGORITHM = _cfg("JWT_ALGORITHM", "HS256") +JWT_AUDIENCE = _cfg("JWT_AUDIENCE", "mcpgateway-api") +JWT_ISSUER = _cfg("JWT_ISSUER", "mcpgateway") + +RL_USERS = int(_cfg("RL_USERS", "100")) +RL_SPAWN_RATE = int(_cfg("RL_SPAWN_RATE", "10")) +RL_RUN_TIME = _cfg("RL_RUN_TIME", "120s") +RL_REQS_PER_SECOND = float(_cfg("RL_REQS_PER_SECOND", "0.25")) +RL_LIMIT_PER_MIN = int(_cfg("RL_LIMIT_PER_MIN", "30")) +RL_PROMPT_ID = _cfg("RL_PROMPT_ID", "") +RL_FORCE_PYTHON = _cfg("RATE_LIMITER_FORCE_PYTHON", "").strip().lower() in ("1", "true", "yes") + +DOCKER_GATEWAY_PATTERN = _cfg("DOCKER_GATEWAY_PATTERN", "mcp-context-forge-gateway") +DOCKER_REDIS_CONTAINER = _cfg("DOCKER_REDIS_CONTAINER", "mcp-context-forge-redis-1") + +logging.basicConfig(level=logging.ERROR) +logger = logging.getLogger(__name__) + + +@dataclass(frozen=True) +class PromptTarget: + """Benchmark prompt target with default argument values.""" + + prompt_id: str + name: str + required_arguments: dict[str, str] + + +_prompt_target: PromptTarget | None = None +_detect_done = False +_user_counter = 0 +_user_counter_lock = threading.Lock() +_user_tokens: list[str] = [] +_registered_state: dict[str, object] = {} +_valid_users = 0 +_created_prompt_id: str | None = None + +_stats_file = None +_stats_proc = None +_stats_path = "" + +_TEST_PASSWORD = "CapacityTest123!" # pragma: allowlist secret +_USER_PREFIX = "rl-capacity" + + +def _default_prompt_argument_value(prompt_name: str, argument_name: str) -> str: + """Return a deterministic low-cost argument value for benchmark prompts.""" + name = argument_name.lower() + prompt = prompt_name.lower() + + if "timezones" in name: + return "UTC,Europe/Dublin" + if "secondary_timezone" in name or "timezone_b" in name: + return "Europe/Dublin" + if "primary_timezone" in name or "source_timezone" in name or "timezone_a" in name: + return "UTC" + if "timezone" in name: + return "UTC" + if "date" in name or "time" in name: + return "2025-01-15T12:00:00Z" + if "email" in name: + return "capacity@example.com" + if "location" in name or "city" in name: + return "Dublin" + if "include_" in name or name.startswith("with_") or name.endswith("_enabled"): + return "true" + if "name" in name or "title" in name or "subject" in name or "message" in name: + return "capacity-test" + if "compare" in prompt: + return "UTC" + return "capacity-test" + + +def _make_token(email: str) -> str: + """Generate a rich admin token with explicit teams=null for admin bypass.""" + # First-Party + from mcpgateway.utils.create_jwt_token import _create_jwt_token # pylint: disable=import-outside-toplevel + + return _create_jwt_token( + {"sub": email}, + expires_in_minutes=int(timedelta(hours=24).total_seconds() // 60), + secret=JWT_SECRET_KEY, + algorithm=JWT_ALGORITHM, + user_data={ + "email": email, + "full_name": "Rate Limiter Capacity Benchmark", + "is_admin": True, + "auth_provider": "local", + }, + teams=None, + ) + + +def _admin_jwt() -> str: + """Create a short-lived admin JWT for discovery and user bootstrap.""" + admin_email = _cfg("PLATFORM_ADMIN_EMAIL", "admin@example.com") + return _make_token(admin_email) + + +def _admin_session(host: str): + """Build a requests session pinned to the admin token.""" + # Third-Party + import requests # pylint: disable=import-outside-toplevel + + session = requests.Session() + session.headers.update( + { + "Authorization": f"Bearer {_admin_jwt()}", + "Accept": "application/json", + "Content-Type": "application/json", + } + ) + session.base_url = host # type: ignore[attr-defined] + return session + + +def _extract_prompt_list(payload) -> list[dict]: + """Normalize list responses across plain and paginated prompt payloads.""" + if isinstance(payload, list): + return [item for item in payload if isinstance(item, dict)] + if isinstance(payload, dict): + for key in ("items", "prompts", "results"): + items = payload.get(key) + if isinstance(items, list): + return [item for item in items if isinstance(item, dict)] + return [] + + +def _pick_prompt(prompts: list[dict]) -> PromptTarget | None: + """Select the cheapest local prompt: local template, few args, short template.""" + candidates: list[tuple[tuple[int, int, str], PromptTarget]] = [] + for prompt in prompts: + prompt_id = str(prompt.get("id") or "").strip() + name = str(prompt.get("name") or prompt.get("custom_name") or "").strip() + template = str(prompt.get("template") or "") + enabled = prompt.get("enabled", True) + if not prompt_id or not name or not enabled or not template: + continue + + required_arguments: dict[str, str] = {} + for argument in prompt.get("arguments", []) or []: + if not isinstance(argument, dict): + continue + arg_name = str(argument.get("name") or "").strip() + if argument.get("required") and arg_name: + required_arguments[arg_name] = _default_prompt_argument_value(name, arg_name) + + target = PromptTarget(prompt_id=prompt_id, name=name, required_arguments=required_arguments) + sort_key = (len(required_arguments), len(template), name) + candidates.append((sort_key, target)) + + if not candidates: + return None + candidates.sort(key=lambda item: item[0]) + return candidates[0][1] + + +def _create_benchmark_prompt(host: str): + """Create a tiny temporary local prompt when none exists.""" + global _created_prompt_id # pylint: disable=global-statement + admin = _admin_session(host) + name = f"rl-capacity-benchmark-{uuid.uuid4().hex[:8]}" + response = admin.post( + f"{host}/prompts", + json={ + "prompt": { + "name": name, + "custom_name": name, + "display_name": name, + "description": "Temporary prompt for Redis capacity benchmarking", + "template": "rate limiter capacity benchmark", + "arguments": [], + "tags": ["benchmark", "rate-limiter"], + }, + "visibility": "public", + }, + timeout=10, + ) + response.raise_for_status() + prompt = response.json() + _created_prompt_id = str(prompt.get("id") or "") + return PromptTarget(prompt_id=_created_prompt_id, name=str(prompt.get("name") or name), required_arguments={}) + + +def _auto_detect(host: str) -> None: + """Resolve the benchmark prompt once per process.""" + global _prompt_target, _detect_done # pylint: disable=global-statement + if _detect_done: + return + _detect_done = True + + admin = _admin_session(host) + headers = dict(admin.headers) + + if RL_PROMPT_ID: + response = admin.get(f"{host}/prompts/{RL_PROMPT_ID}", headers=headers, timeout=10) + response.raise_for_status() + prompt = response.json() + required_arguments: dict[str, str] = {} + name = str(prompt.get("name") or RL_PROMPT_ID) + for argument in prompt.get("arguments", []) or []: + if not isinstance(argument, dict): + continue + arg_name = str(argument.get("name") or "").strip() + if argument.get("required") and arg_name: + required_arguments[arg_name] = _default_prompt_argument_value(name, arg_name) + _prompt_target = PromptTarget(prompt_id=str(prompt.get("id") or RL_PROMPT_ID), name=name, required_arguments=required_arguments) + return + + response = admin.get(f"{host}/prompts", headers=headers, timeout=10) + response.raise_for_status() + prompt = _pick_prompt(_extract_prompt_list(response.json())) + if prompt is None: + prompt = _create_benchmark_prompt(host) + _prompt_target = prompt + + +def _bootstrap_users(host: str) -> None: + """Register benchmark users and pre-build one token per unique identity.""" + global _user_tokens, _registered_state, _valid_users # pylint: disable=global-statement + admin = _admin_session(host) + + registered: list[dict[str, str]] = [] + tokens: list[str] = [] + run_id = uuid.uuid4().hex[:6] + + for index in range(RL_USERS): + email = f"{_USER_PREFIX}-{run_id}-{index:04d}@loadtest.internal" + try: + response = admin.post( + f"{host}/auth/email/admin/users", + json={ + "email": email, + "password": _TEST_PASSWORD, + "full_name": f"Rate Limit Capacity User {index:04d}", + "is_admin": True, + "is_active": True, + "password_change_required": False, + }, + timeout=10, + ) + if response.status_code not in (200, 201): + logger.warning("User registration failed for %s: %s %s", email, response.status_code, response.text[:200]) + tokens.append("") + registered.append({"email": email}) + continue + + tokens.append(_make_token(email)) + registered.append({"email": email}) + except Exception as exc: + logger.warning("User bootstrap failed for %s: %s", email, exc) + tokens.append("") + registered.append({"email": email}) + + _user_tokens = tokens + _registered_state = {"host": host, "users": registered} + _valid_users = sum(1 for token in tokens if token) + + +def _cleanup_users() -> None: + """Delete any benchmark users created during bootstrap.""" + if not _registered_state: + return + + host = str(_registered_state.get("host") or "") + users = _registered_state.get("users") or [] + if not host or not isinstance(users, list): + return + + admin = _admin_session(host) + for user in users: + if not isinstance(user, dict): + continue + email = user.get("email") + if not isinstance(email, str) or not email: + continue + try: + admin.delete(f"{host}/auth/email/admin/users/{email}", timeout=10) + except Exception as exc: + logger.warning("Cleanup failed for %s: %s", email, exc) + + +def _cleanup_prompt(host: str) -> None: + """Delete the temporary benchmark prompt when one was created.""" + if not _created_prompt_id: + return + admin = _admin_session(host) + try: + admin.delete(f"{host}/prompts/{_created_prompt_id}", timeout=10) + except Exception as exc: + logger.warning("Prompt cleanup failed for %s: %s", _created_prompt_id, exc) + + +def _mem_to_mib(raw: str) -> float: + """Convert docker stats memory values to MiB.""" + raw = raw.strip() + match = re.match(r"([\d.]+)\s*([KMGTkmgt]i?[Bb]?)", raw) + if not match: + return 0.0 + value, unit = float(match.group(1)), match.group(2).upper() + if unit.startswith("K"): + return value / 1024 + if unit.startswith("M"): + return value + if unit.startswith("G"): + return value * 1024 + if unit.startswith("T"): + return value * 1024 * 1024 + return value + + +def _parse_stats_file(path: str) -> dict[str, dict[str, float]]: + """Parse docker stats output into avg/peak CPU and memory samples.""" + ansi = re.compile(r"\x1b\[[0-9;]*[A-Za-z]|\x1b\[[?][0-9;]*[A-Za-z]") + samples: dict[str, list[tuple[float, float]]] = {} + try: + with open(path, "r", encoding="utf-8", errors="replace") as handle: + for line in handle: + line = ansi.sub("", line).strip() + parts = line.split("\t") + if len(parts) < 3: + continue + name = parts[0].strip() + if not name: + continue + mem = _mem_to_mib(parts[1].split("/")[0].strip()) + try: + cpu = float(parts[2].replace("%", "").strip()) + except ValueError: + continue + if mem <= 0: + continue + samples.setdefault(name, []).append((mem, cpu)) + except FileNotFoundError: + return {} + + parsed: dict[str, dict[str, float]] = {} + for name, points in samples.items(): + mems = [point[0] for point in points] + cpus = [point[1] for point in points] + parsed[name] = { + "mem_avg": sum(mems) / len(mems), + "mem_peak": max(mems), + "cpu_avg": sum(cpus) / len(cpus), + "cpu_peak": max(cpus), + "samples": float(len(points)), + } + return parsed + + +def _start_stats_monitor() -> None: + """Start a docker stats subprocess for gateway/Redis resource sampling.""" + global _stats_file, _stats_proc, _stats_path # pylint: disable=global-statement + try: + fd, _stats_path = tempfile.mkstemp(prefix="rl_capacity_stats_", suffix=".tsv") + _stats_file = os.fdopen(fd, "w") + _stats_proc = subprocess.Popen( + ["docker", "stats", "--format", "{{.Name}}\t{{.MemUsage}}\t{{.CPUPerc}}"], + stdout=_stats_file, + stderr=subprocess.DEVNULL, + ) + except Exception as exc: # pragma: no cover - best effort benchmark instrumentation + logger.error("docker stats monitor failed to start: %s", exc) + + +def _stop_stats_monitor() -> dict[str, dict[str, float]]: + """Stop docker stats and return parsed samples.""" + global _stats_proc, _stats_file # pylint: disable=global-statement + if _stats_proc: + try: + _stats_proc.terminate() + _stats_proc.wait(timeout=5) + except Exception: # pragma: no cover - best effort benchmark instrumentation + pass + _stats_proc = None + if _stats_file: + try: + _stats_file.flush() + _stats_file.close() + except Exception: # pragma: no cover - best effort benchmark instrumentation + pass + _stats_file = None + return _parse_stats_file(_stats_path) if _stats_path else {} + + +@events.init_command_line_parser.add_listener +def set_defaults(parser): + """Match Makefile defaults when invoked directly.""" + parser.set_defaults(users=RL_USERS, spawn_rate=RL_SPAWN_RATE, run_time=RL_RUN_TIME) + + +@events.test_start.add_listener +def on_test_start(environment, **kwargs): + """Resolve prompt target and print a short benchmark banner.""" + del kwargs + host = environment.host or "http://localhost:8080" + _auto_detect(host) + _bootstrap_users(host) + _start_stats_monitor() + if isinstance(environment.runner, WorkerRunner): + return + + engine_label = "Python (RATE_LIMITER_FORCE_PYTHON=1)" if RL_FORCE_PYTHON else "Rust (default)" + + print("\n" + "=" * 90) + print("RATE LIMITER REDIS CAPACITY TEST") + print("=" * 90) + print(f" Host: {host}") + print(" Topology: nginx -> 3 gateways -> shared Redis") + print(" Path: REST /prompts/{id} (prompt_pre_fetch)") + print(f" Engine: {engine_label}") + print(f" Prompt: {(_prompt_target.name if _prompt_target else '(none)')} [{(_prompt_target.prompt_id if _prompt_target else '')}]") + print(f" Required args: {len(_prompt_target.required_arguments) if _prompt_target else 0}") + print(f" Valid users: {_valid_users}/{RL_USERS}") + print(f" Users: {RL_USERS}") + print(f" Spawn rate: {RL_SPAWN_RATE}/s") + print(f" Per-user pace: {RL_REQS_PER_SECOND:.2f} req/s ({RL_REQS_PER_SECOND * 60:.0f} req/min)") + print(f" Banner limit: {RL_LIMIT_PER_MIN}/min per user") + print(f" Duration: {RL_RUN_TIME}") + print("=" * 90) + + +@events.test_stop.add_listener +def on_test_stop(environment, **kwargs): + """Print semantic request counts plus sampled gateway/Redis resource usage.""" + del kwargs + host = environment.host or "http://localhost:8080" + resource_data = _stop_stats_monitor() + _cleanup_users() + _cleanup_prompt(host) + if isinstance(environment.runner, WorkerRunner): + return + + stats = environment.stats + total_http = stats.total.num_requests + infra_fails = stats.total.num_failures + + allowed_entry = stats.entries.get(("Prompt execute [allowed]", "POST")) + blocked_entry = stats.entries.get(("Prompt execute [rate-limited]", "POST")) + allowed_count = allowed_entry.num_requests if allowed_entry else 0 + blocked_count = blocked_entry.num_requests if blocked_entry else 0 + semantic_total = allowed_count + blocked_count + blocked_pct = (blocked_count / semantic_total * 100) if semantic_total else 0.0 + + engine_label = "Python (RATE_LIMITER_FORCE_PYTHON=1)" if RL_FORCE_PYTHON else "Rust (default)" + + print("\n" + "=" * 90) + print("RATE LIMITER REDIS CAPACITY RESULTS") + print("=" * 90) + print(f" Engine: {engine_label}") + print(f" Prompt target: {(_prompt_target.name if _prompt_target else '(none)')}") + print(f" HTTP requests observed: {total_http:>10,}") + print(f" Semantic prompt calls: {semantic_total:>10,}") + print(f" Allowed responses: {allowed_count:>10,}") + print(f" Rate-limited responses: {blocked_count:>10,} ({blocked_pct:.1f}%)") + print(f" Infrastructure failures: {infra_fails:>10,}") + print(f" Throughput (req/s): {stats.total.total_rps:>10.2f}") + if total_http > 0: + print("\n Response Times (ms):") + print(f" Average: {stats.total.avg_response_time:>10.1f}") + print(f" p50: {stats.total.get_response_time_percentile(0.50):>10.1f}") + print(f" p95: {stats.total.get_response_time_percentile(0.95):>10.1f}") + print(f" p99: {stats.total.get_response_time_percentile(0.99):>10.1f}") + + if resource_data: + gateways = sorted([(name, data) for name, data in resource_data.items() if DOCKER_GATEWAY_PATTERN in name], key=lambda item: item[0]) + redis_rows = [(name, data) for name, data in resource_data.items() if name == DOCKER_REDIS_CONTAINER or name.endswith("/redis-1") or "redis" in name.lower()] + if gateways or redis_rows: + print(f"\n {'CONTAINER RESOURCE USAGE':^86}") + print(" " + "-" * 86) + print(f" {'Container':<36} {'Mem avg':>9} {'Mem peak':>9} {'CPU avg':>8} {'CPU peak':>9} {'Samples':>7}") + print(" " + "-" * 86) + total_mem_avg = 0.0 + total_mem_peak = 0.0 + for name, data in gateways + redis_rows: + short = name.replace("mcp-context-forge-", "") + print(f" {short:<36} {data['mem_avg']:>7.1f}M {data['mem_peak']:>7.1f}M " f"{data['cpu_avg']:>7.1f}% {data['cpu_peak']:>8.1f}% {int(data['samples']):>7}") + if DOCKER_GATEWAY_PATTERN in name: + total_mem_avg += data["mem_avg"] + total_mem_peak += data["mem_peak"] + if len(gateways) > 1: + print(" " + "-" * 86) + print(f" {'All gateways combined':<36} {total_mem_avg:>7.1f}M {total_mem_peak:>7.1f}M") + + print("\n" + "=" * 90 + "\n") + + +class CapacityPromptUser(FastHttpUser): + """Concurrent prompt caller with a unique JWT identity per Locust user.""" + + wait_time = constant_throughput(RL_REQS_PER_SECOND) + connection_timeout = 30.0 + network_timeout = 30.0 + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + global _user_counter # pylint: disable=global-statement + with _user_counter_lock: + user_id = _user_counter + _user_counter += 1 + self._token = _user_tokens[user_id % len(_user_tokens)] if _user_tokens else "" + + def _headers(self) -> dict[str, str]: + return { + "Accept": "application/json", + "Content-Type": "application/json", + "Authorization": f"Bearer {self._token}", + } + + @task + @tag("rate-limit", "redis", "capacity", "prompts") + def execute_prompt(self) -> None: + """Execute the chosen prompt and classify the response semantically.""" + if _prompt_target is None: + return + + try: + with self.client.post( + f"/prompts/{_prompt_target.prompt_id}", + json=_prompt_target.required_arguments, + headers=self._headers(), + name="Prompt execute", + catch_response=True, + ) as response: + if response.status_code == 200: + response.request_meta["name"] = "Prompt execute [allowed]" + response.success() + return + + body_text = "" + try: + payload = response.json() + if isinstance(payload, dict): + body_text = str(payload.get("message") or payload.get("details") or payload) + else: + body_text = str(payload) + except Exception: + body_text = response.text or "" + + if response.status_code == 429: + response.request_meta["name"] = "Prompt execute [rate-limited]" + response.success() + return + body_lower = body_text.lower() + if response.status_code in (422, 403) and ("rate" in body_lower or "rate_limit" in body_lower): + response.request_meta["name"] = "Prompt execute [rate-limited]" + response.success() + return + + response.request_meta["name"] = "Prompt execute [infra-error]" + response.failure(f"HTTP {response.status_code}: {body_text[:160]}") + except Exception as exc: + logger.warning("Prompt execute request failed: %s", exc) diff --git a/tests/loadtest/locustfile_rate_limiter_scale.py b/tests/loadtest/locustfile_rate_limiter_scale.py new file mode 100644 index 0000000000..0256167976 --- /dev/null +++ b/tests/loadtest/locustfile_rate_limiter_scale.py @@ -0,0 +1,987 @@ +# -*- coding: utf-8 -*- +"""Rate limiter algorithm scale test — resource divergence across algorithms. + +Why this test exists +-------------------- +The algorithm comparison test (locustfile_rate_limiter_algorithms.py) uses a +single user, which creates one Redis key per algorithm. At that scale the +memory difference between fixed_window (1 integer per key) and sliding_window +(30 timestamps per key) is invisible — a few hundred bytes vs a single integer. + +This test uses many unique users so each one creates its own rate limit key. +Redis memory diverges visibly as user count grows: + + fixed_window 1 key per user = N integers (O(N)) + sliding_window 1 key per user = N × W timestamps (O(N × W)) + token_bucket 1 hash per user = tokens + last_refill (~0.2 KiB/key) + +With 100 users and a 30/m limit (W=30 timestamps/window): + fixed_window ~100 Redis keys → ~10 KB + sliding_window ~100 Redis keys → ~30–90 KB (sorted sets, ~30 entries each) + +At 1,000 users the gap becomes ~1 MB vs ~30 MB — clearly measurable. + +How it works +------------ + - N unique users (default 100), each with a distinct email identity + - Each user's JWT sub is unique → separate rate limit key in Redis per user + - All users send at 1 req/s (60 req/min) = 2× the 30/m per-user limit + - docker stats streams gateway CPU/memory for the full test duration + - A background thread polls Redis memory (DBSIZE + INFO memory) every 10s + and builds a timeline showing how memory grows as users are added + - Results show: rate accuracy, gateway resources, and Redis memory timeline + +Run it the same way as the single-user test — just more users: + docker exec mcp-context-forge-redis-1 redis-cli FLUSHDB + RL_ALGORITHM=fixed_window make benchmark-rate-limiter-scale + # restart gateways, flush Redis + RL_ALGORITHM=sliding_window make benchmark-rate-limiter-scale + +Environment Variables +--------------------- + RL_ALGORITHM: Algorithm (default: fixed_window) + RL_LIMIT_PER_MIN: Configured limit (default: 30) + RL_USERS: Number of unique users (default: 500) + RL_SPAWN_RATE: Users spawned per second (default: 20) + RL_RUN_TIME: Test duration (default: 300s — 5 full 60s windows) + RL_REQS_PER_SECOND: Request pace per user (default: 1.0 = 60 req/min, 2× limit) + MCP_SERVER_ID: Virtual server UUID (auto-detected if empty) + DOCKER_GATEWAY_PATTERN: Container name pattern (default: mcp-context-forge-gateway) + DOCKER_REDIS_CONTAINER: Redis container name (default: mcp-context-forge-redis-1) + JWT_SECRET_KEY: JWT signing secret (default: my-test-key) + JWT_ALGORITHM: JWT algorithm (default: HS256) + JWT_AUDIENCE: JWT audience (default: mcpgateway-api) + JWT_ISSUER: JWT issuer (default: mcpgateway) + +Copyright 2026 +SPDX-License-Identifier: Apache-2.0 +""" + +# Standard +from collections import defaultdict +import json +import logging +import os +from pathlib import Path +import re +import subprocess +import tempfile +import threading +import time +from typing import Any +import uuid + +# Third-Party +from locust import constant_throughput, events, tag, task +from locust.contrib.fasthttp import FastHttpUser +from locust.runners import WorkerRunner + +# ============================================================================= +# Configuration +# ============================================================================= + + +def _load_env_file() -> dict[str, str]: + env_vars: dict[str, str] = {} + search_paths = [ + Path.cwd() / ".env", + Path.cwd().parent / ".env", + Path.cwd().parent.parent / ".env", + Path(__file__).parent.parent.parent / ".env", + ] + for path in search_paths: + if path.exists(): + with open(path, "r", encoding="utf-8", errors="replace") as f: + for line in f: + line = line.strip() + if not line or line.startswith("#"): + continue + if "=" in line: + key, _, value = line.partition("=") + env_vars[key.strip()] = value.strip().strip("\"'") + break + return env_vars + + +_ENV = _load_env_file() + + +def _cfg(key: str, default: str = "") -> str: + return os.environ.get(key) or _ENV.get(key) or default + + +JWT_SECRET_KEY = _cfg("JWT_SECRET_KEY", "my-test-key") +JWT_ALGORITHM_CFG = _cfg("JWT_ALGORITHM", "HS256") +JWT_AUDIENCE = _cfg("JWT_AUDIENCE", "mcpgateway-api") +JWT_ISSUER = _cfg("JWT_ISSUER", "mcpgateway") +MCP_SERVER_ID = _cfg("MCP_SERVER_ID", "") + +RL_ALGORITHM = _cfg("RL_ALGORITHM", "fixed_window") +RL_LIMIT_PER_MIN = int(_cfg("RL_LIMIT_PER_MIN", "30")) +RL_USERS = int(_cfg("RL_USERS", "100")) +RL_SPAWN_RATE = int(_cfg("RL_SPAWN_RATE", "10")) +RL_RUN_TIME = _cfg("RL_RUN_TIME", "300s") + +DOCKER_GATEWAY_PATTERN = _cfg("DOCKER_GATEWAY_PATTERN", "mcp-context-forge-gateway") +DOCKER_REDIS_CONTAINER = _cfg("DOCKER_REDIS_CONTAINER", "mcp-context-forge-redis-1") + +_REQS_PER_SECOND = float(_cfg("RL_REQS_PER_SECOND", "1.0")) + +logging.basicConfig(level=logging.ERROR) +logger = logging.getLogger(__name__) + +# ============================================================================= +# Shared state +# ============================================================================= + +_server_id: str = "" +_tool_names: list[str] = [] +_detect_done = False +_test_start_time: float = 0.0 + +# Per-user identity counter — assigns each locust user a slot in _user_tokens +_user_counter = 0 +_user_counter_lock = threading.Lock() + +# Populated by _bootstrap_users() before locust users start: +# index N → gateway-issued access_token for scale-user-N +_user_tokens: list[str] = [] + +# Tracks what was registered so _cleanup_users() can delete it all +_registered_state: dict[str, Any] = {} # {"host": ..., "users": [{"email": ...}]} + +_stats_lock = threading.Lock() + +# 30-second window buckets — {bucket_index: {"allowed": int, "blocked": int}} +_bucket_stats: dict[int, dict[str, int]] = defaultdict(lambda: {"allowed": 0, "blocked": 0}) + +# Pre-bootstrap Redis snapshot (t=0, before any RL keys exist) +_redis_baseline: dict[str, Any] | None = None +# Algorithm detected from Redis key types after steady state +_detected_algorithm: str = "" +# Number of bootstrapped users with valid JWT tokens +_valid_users: int = 0 + +_TEST_PASSWORD = "ScaleTest123!" # pragma: allowlist secret +_USER_PREFIX = "rl-scale" + +# ============================================================================= +# docker stats monitor (streaming subprocess) +# ============================================================================= + +_stats_file: Any = None +_stats_proc: Any = None +_stats_path: str = "" + + +def _mem_to_mib(raw: str) -> float: + raw = raw.strip() + m = re.match(r"([\d.]+)\s*([KMGTkmgt]i?[Bb]?)", raw) + if not m: + return 0.0 + val, unit = float(m.group(1)), m.group(2).upper() + if unit.startswith("K"): + return val / 1024 + if unit.startswith("M"): + return val + if unit.startswith("G"): + return val * 1024 + if unit.startswith("T"): + return val * 1024 * 1024 + return val + + +def _parse_stats_file(path: str) -> dict[str, dict[str, float]]: + _ansi = re.compile(r"\x1b\[[0-9;]*[A-Za-z]|\x1b\[[?][0-9;]*[A-Za-z]") + samples: dict[str, list[tuple[float, float]]] = defaultdict(list) + try: + with open(path, "r", encoding="utf-8", errors="replace") as fh: + for line in fh: + line = _ansi.sub("", line).strip() + parts = line.split("\t") + if len(parts) < 3: + continue + name = parts[0].strip() + if not name: + continue + used_mem = parts[1].split("/")[0].strip() + mem_mib = _mem_to_mib(used_mem) + cpu_str = parts[2].replace("%", "").strip() + try: + cpu = float(cpu_str) + except ValueError: + continue + if mem_mib > 0: + samples[name].append((mem_mib, cpu)) + except FileNotFoundError: + pass + result: dict[str, dict[str, float]] = {} + for name, pts in samples.items(): + if not pts: + continue + mems = [p[0] for p in pts] + cpus = [p[1] for p in pts] + result[name] = { + "mem_avg": sum(mems) / len(mems), + "mem_peak": max(mems), + "cpu_avg": sum(cpus) / len(cpus), + "cpu_peak": max(cpus), + "samples": len(pts), + } + return result + + +def _start_stats_monitor() -> None: + global _stats_file, _stats_proc, _stats_path # pylint: disable=global-statement + try: + fd, _stats_path = tempfile.mkstemp(prefix="rl_scale_stats_", suffix=".tsv") + _stats_file = os.fdopen(fd, "w") + _stats_proc = subprocess.Popen( + ["docker", "stats", "--format", "{{.Name}}\t{{.MemUsage}}\t{{.CPUPerc}}"], + stdout=_stats_file, + stderr=subprocess.DEVNULL, + ) + except Exception as exc: + logger.error("docker stats monitor failed to start: %s", exc) + + +def _stop_stats_monitor() -> dict[str, dict[str, float]]: + global _stats_proc, _stats_file # pylint: disable=global-statement + if _stats_proc: + try: + _stats_proc.terminate() + _stats_proc.wait(timeout=5) + except Exception: + pass + _stats_proc = None + if _stats_file: + try: + _stats_file.flush() + _stats_file.close() + except Exception: + pass + _stats_file = None + return _parse_stats_file(_stats_path) if _stats_path else {} + + +# ============================================================================= +# Redis memory timeline (background polling thread) +# ============================================================================= + +# Timeline entries: list of {"elapsed": float, "keys": int, "mem_mib": float, "users": int} +_redis_timeline: list[dict[str, Any]] = [] +_redis_poll_running = False +_redis_poll_thread: threading.Thread | None = None +_active_users = 0 # updated by user on_start/on_stop + + +def _scan_redis_pattern(pattern: str, timeout: int = 20) -> int: + """Count Redis keys matching a pattern using non-blocking SCAN iteration.""" + try: + r = subprocess.run( + ["docker", "exec", DOCKER_REDIS_CONTAINER, "redis-cli", "--scan", "--pattern", pattern], + capture_output=True, + text=True, + timeout=timeout, + ) + if r.returncode != 0: + return 0 + return sum(1 for line in r.stdout.splitlines() if line.strip()) + except Exception: + return 0 + + +def _poll_redis_once() -> dict[str, Any] | None: + """Query Redis for key count and memory usage via docker exec. + + Returns both total_keys (DBSIZE) and rl_keys (user:* + tenant:* only) + so the summary can report RL-specific memory per key without noise from + unrelated keys (session store, user DB cache, etc.). + """ + try: + # DBSIZE — total key count (for reference / sanity check) + r_dbsize = subprocess.run( + ["docker", "exec", DOCKER_REDIS_CONTAINER, "redis-cli", "DBSIZE"], + capture_output=True, + text=True, + timeout=5, + ) + total_keys = int(r_dbsize.stdout.strip()) if r_dbsize.returncode == 0 else 0 + + # RL-specific keys only (rl:user:* = per-user buckets, rl:tenant:* = per-team buckets) + rl_keys = _scan_redis_pattern("rl:user:*") + _scan_redis_pattern("rl:tenant:*") + + # INFO memory — used_memory in bytes + r_mem = subprocess.run( + ["docker", "exec", DOCKER_REDIS_CONTAINER, "redis-cli", "INFO", "memory"], + capture_output=True, + text=True, + timeout=5, + ) + mem_bytes = 0 + for line in r_mem.stdout.splitlines(): + if line.startswith("used_memory:"): + mem_bytes = int(line.split(":")[1].strip()) + break + + return { + "elapsed": time.time() - _test_start_time, + "total_keys": total_keys, + "rl_keys": rl_keys, + "keys": rl_keys, # alias used by display code + "mem_mib": mem_bytes / (1024 * 1024), + "users": _active_users, + } + except Exception as exc: + logger.warning("Redis poll failed: %s", exc) + return None + + +def _detect_algorithm_from_redis() -> str: + """Detect the active rate-limiting algorithm by inspecting Redis key types. + + After steady state there should be user:* keys in Redis. Their data type + reveals which algorithm is running: + string → fixed_window (single integer counter) + zset → sliding_window (sorted set of request timestamps) + hash → token_bucket (tokens + last_refill timestamp) + """ + try: + r_scan = subprocess.run( + ["docker", "exec", DOCKER_REDIS_CONTAINER, "redis-cli", "--scan", "--pattern", "rl:user:*"], + capture_output=True, + text=True, + timeout=15, + ) + sample_keys = [l.strip() for l in r_scan.stdout.splitlines() if l.strip()] + if not sample_keys: + return f"unknown — no rl:user:* keys in Redis (expected for {RL_ALGORITHM}?)" + + r_type = subprocess.run( + ["docker", "exec", DOCKER_REDIS_CONTAINER, "redis-cli", "TYPE", sample_keys[0]], + capture_output=True, + text=True, + timeout=5, + ) + key_type = r_type.stdout.strip().lower() + + mapping = { + "string": "fixed_window", + "zset": "sliding_window", + "hash": "token_bucket", + } + detected = mapping.get(key_type, f"unknown ({key_type})") + match = "✅ matches config" if detected == RL_ALGORITHM else f"⚠️ MISMATCH — config says {RL_ALGORITHM}" + return f"{detected} [Redis key type: {key_type}] {match}" + except Exception as exc: + return f"detection failed: {exc}" + + +def _redis_poll_loop() -> None: + while _redis_poll_running: + entry = _poll_redis_once() + if entry: + _redis_timeline.append(entry) + time.sleep(5) + + +def _start_redis_monitor() -> None: + global _redis_poll_running, _redis_poll_thread # pylint: disable=global-statement + _redis_poll_running = True + _redis_poll_thread = threading.Thread(target=_redis_poll_loop, daemon=True) + _redis_poll_thread.start() + + +def _stop_redis_monitor() -> None: + global _redis_poll_running # pylint: disable=global-statement + _redis_poll_running = False + if _redis_poll_thread: + _redis_poll_thread.join(timeout=6) + + +# ============================================================================= +# Admin JWT — used only for setup/teardown API calls +# ============================================================================= + + +def _admin_jwt() -> str: + """Create a short-lived JWT for the platform admin (setup/teardown only).""" + # First-Party + from mcpgateway.utils.create_jwt_token import _create_jwt_token # pylint: disable=import-outside-toplevel + + admin_email = _cfg("PLATFORM_ADMIN_EMAIL", "admin@example.com") + return _create_jwt_token( + {"sub": admin_email}, + user_data={"email": admin_email, "is_admin": True, "auth_provider": "local"}, + teams=None, + secret=JWT_SECRET_KEY, + ) + + +def _admin_session(host: str) -> Any: + # Third-Party + import requests # pylint: disable=import-outside-toplevel + + s = requests.Session() + s.headers.update( + { + "Authorization": f"Bearer {_admin_jwt()}", + "Accept": "application/json", + "Content-Type": "application/json", + } + ) + s.base_url = host # type: ignore[attr-defined] + return s + + +# ============================================================================= +# User bootstrap and cleanup — follows isolation test pattern +# ============================================================================= + + +def _bootstrap_users(host: str) -> None: + """Register N test users via the admin API and build per-user JWTs. + + Strategy: + 1. Discover a virtual server with tools (admin credentials) + 2. Register N users in the gateway DB with is_admin=True so they can + access public servers without needing team-scoped RBAC assignment + 3. Build a short-lived admin JWT for each user (unique sub → unique + rate-limit key in Redis) + + Tokens are stored in _user_tokens[i] for ScaleComparisonUser to pick up. + """ + global _user_tokens, _registered_state, _server_id, _tool_names # pylint: disable=global-statement + + # Third-Party + import requests # pylint: disable=import-outside-toplevel + + # First-Party + from mcpgateway.utils.create_jwt_token import _create_jwt_token # pylint: disable=import-outside-toplevel + + admin = _admin_session(host) + + # ------------------------------------------------------------------ + # 1. Discover server and tools (using admin credentials) + # ------------------------------------------------------------------ + server_ids_to_try: list[str] = [MCP_SERVER_ID] if MCP_SERVER_ID else [] + if not server_ids_to_try: + resp = admin.get(f"{host}/servers", timeout=10) + all_servers = resp.json() if resp.status_code == 200 else [] + server_ids_to_try = [s.get("id", "") for s in (all_servers if isinstance(all_servers, list) else []) if s.get("id")] + + for sid in server_ids_to_try: + try: + resp = admin.post( + f"{host}/servers/{sid}/mcp", + json={"jsonrpc": "2.0", "id": "1", "method": "tools/list", "params": {}}, + headers={**dict(admin.headers), "Accept": "application/json, text/event-stream"}, + timeout=10, + ) + if resp.status_code == 200: + tools = [t["name"] for t in resp.json().get("result", {}).get("tools", [])] + if tools: + _server_id = sid + _tool_names = tools + break + except Exception as exc: + logger.warning("Tool detect failed for %s: %s", sid, exc) + + # ------------------------------------------------------------------ + # 2. Register N users in the DB (is_admin=True so they can use public servers) + # Build a JWT per user — each unique sub → unique rate-limit key in Redis + # ------------------------------------------------------------------ + registered: list[dict[str, str]] = [] + tokens: list[str] = [] + run_id = uuid.uuid4().hex[:6] + + for i in range(RL_USERS): + email = f"{_USER_PREFIX}-{run_id}-{i:04d}@loadtest.internal" + try: + r = admin.post( + f"{host}/auth/email/admin/users", + json={ + "email": email, + "password": _TEST_PASSWORD, + "full_name": f"Scale Test User {i:04d}", + "is_admin": True, + "is_active": True, + "password_change_required": False, + }, + timeout=10, + ) + if r.status_code not in (200, 201): + logger.warning("User registration failed for %s: %s %s", email, r.status_code, r.text[:200]) + tokens.append("") + registered.append({"email": email}) + continue + + user_jwt = _create_jwt_token( + {"sub": email}, + user_data={"email": email, "is_admin": True, "auth_provider": "local"}, + teams=None, + secret=JWT_SECRET_KEY, + ) + tokens.append(user_jwt) + registered.append({"email": email}) + + except Exception as exc: + logger.warning("Bootstrap failed for user %d (%s): %s", i, email, exc) + tokens.append("") + registered.append({"email": email}) + + _user_tokens = tokens + _registered_state = {"host": host, "users": registered} + + valid = sum(1 for t in tokens if t) + global _valid_users # pylint: disable=global-statement + _valid_users = valid + logger.error("Bootstrap complete: %d/%d users registered with valid tokens", valid, RL_USERS) + + +def _cleanup_users() -> None: + """Delete all test users registered during bootstrap.""" + if not _registered_state: + return + + host = _registered_state.get("host", "") + if not host: + return + + admin = _admin_session(host) + + for user in _registered_state.get("users", []): + email = user.get("email", "") + if email: + try: + admin.delete(f"{host}/auth/email/admin/users/{email}", timeout=10) + except Exception: + pass + + logger.error("Cleanup complete: deleted %d users", len(_registered_state.get("users", []))) + + +# ============================================================================= +# Event handlers +# ============================================================================= + + +@events.init_command_line_parser.add_listener +def set_defaults(parser): + parser.set_defaults(users=RL_USERS, spawn_rate=RL_SPAWN_RATE, run_time=RL_RUN_TIME) + + +@events.test_start.add_listener +def on_test_start(environment, **kwargs): + global _test_start_time, _redis_baseline # pylint: disable=global-statement + _test_start_time = time.time() + + host = environment.host or "http://localhost:8080" + + # Capture Redis state BEFORE bootstrap so we have a true noise-free baseline. + # Bootstrap registers ~N users via the admin API and may create session/auth + # keys of its own — the baseline must be taken first. + _redis_baseline = _poll_redis_once() + if _redis_baseline: + _redis_baseline["elapsed"] = 0.0 # normalise to t=0 + + _bootstrap_users(host) + _start_stats_monitor() + _start_redis_monitor() + + if isinstance(environment.runner, WorkerRunner): + return + + logger.error("=" * 70) + logger.error("RATE LIMITER SCALE TEST — RESOURCE DIVERGENCE") + logger.error("=" * 70) + logger.error(" Host: %s", host) + logger.error(" Algorithm: %s (set in plugins/config.yaml)", RL_ALGORITHM) + logger.error(" Users: %d unique identities (each gets own Redis key)", RL_USERS) + logger.error(" Spawn rate: %d users/s (ramp-up over %ds)", RL_SPAWN_RATE, RL_USERS // RL_SPAWN_RATE) + logger.error(" Limit: %d req/min per user", RL_LIMIT_PER_MIN) + logger.error(" Pace: %d req/s per user (2× the limit)", int(_REQS_PER_SECOND)) + logger.error(" Duration: %s", RL_RUN_TIME) + logger.error("") + logger.error(" Redis memory grows proportionally to unique users:") + logger.error(" fixed_window 1 integer per user (~0.1-0.3 KiB/key)") + logger.error(" sliding_window %d timestamps per user (~1-3 KiB/key, %dx more)", RL_LIMIT_PER_MIN, RL_LIMIT_PER_MIN) + logger.error(" token_bucket 1 hash per user (tokens + last_refill, ~0.2 KiB/key)") + logger.error("=" * 70) + + +@events.test_stop.add_listener +def on_test_stop(environment, **kwargs): + global _detected_algorithm # pylint: disable=global-statement + + # Take a final Redis snapshot before stopping monitors + final = _poll_redis_once() + if final: + _redis_timeline.append(final) + + # Detect the actual algorithm from Redis key types while keys still exist + _detected_algorithm = _detect_algorithm_from_redis() + + resource_data = _stop_stats_monitor() + _stop_redis_monitor() + _cleanup_users() + + if isinstance(environment.runner, WorkerRunner): + return + + stats = environment.stats + total_http = stats.total.num_requests + infra_fails = stats.total.num_failures + + rl_entry = stats.entries.get(("MCP tools/call [rate-limited]", "POST"), None) + rl_count = rl_entry.num_requests if rl_entry else 0 + allowed_entry = stats.entries.get(("MCP tools/call [allowed]", "POST"), None) + allowed_count = allowed_entry.num_requests if allowed_entry else 0 + # Each request is classified in-place: allowed_count and rl_count are already + # mutually exclusive — no subtraction needed. + semantic_allowed = allowed_count + tool_calls = allowed_count + rl_count # excludes infra errors + rl_pct = (rl_count / tool_calls * 100) if tool_calls > 0 else 0 + + print("\n" + "=" * 90) + print(f"RATE LIMITER SCALE TEST — {RL_ALGORITHM.upper()}") + print("=" * 90) + print(f"\n Algorithm (config): {RL_ALGORITHM}") + print(f" Algorithm (live): {_detected_algorithm}") + print(f" Unique users: {_valid_users}/{RL_USERS} with valid tokens") + print(f" Configured limit: {RL_LIMIT_PER_MIN} req/min per user") + print(f" Test pace: {int(_REQS_PER_SECOND * 60)} req/min per user (2× the limit)") + + # Rate accuracy + print(f"\n {'RATE LIMITING ACCURACY':^86}") + print(" " + "-" * 86) + print(f" Tool call attempts: {tool_calls:>8,}") + print(f" Allowed through: {semantic_allowed:>8,}") + print(f" Rate-limited (blocked): {rl_count:>8,} ({rl_pct:.1f}%)") + print(f" Infrastructure failures: {infra_fails:>8,}") + + # 30-second window breakdown + if _bucket_stats: + print(f"\n {'30s WINDOW BREAKDOWN (aggregate across all users)':^86}") + print(" " + "-" * 86) + print(f" {'Window':<14} {'Start':>7} {'Allowed':>10} {'Blocked':>10} {'Block%':>8} Note") + print(" " + "-" * 86) + spawn_end = RL_USERS / RL_SPAWN_RATE + for idx in sorted(_bucket_stats): + b = _bucket_stats[idx] + start_s = idx * 30 + total_b = b["allowed"] + b["blocked"] + pct = (b["blocked"] / total_b * 100) if total_b > 0 else 0.0 + note = "" + if start_s < spawn_end: + note = "ramp-up" + elif (start_s % 60) == 0 and start_s > 0: + note = "<-- window boundary" + print(f" t={start_s:>4}-{start_s+30:<4}s {start_s:>6}s {b['allowed']:>10,} {b['blocked']:>10,} {pct:>7.1f}% {note}") + + # Gateway resource table + gateways = sorted( + [(n, d) for n, d in resource_data.items() if DOCKER_GATEWAY_PATTERN in n], + key=lambda x: x[0], + ) + redis_entries = [(n, d) for n, d in resource_data.items() if DOCKER_REDIS_CONTAINER.replace("mcp-context-forge-", "") in n or "redis" in n.lower()] + + if gateways or redis_entries: + print(f"\n {'GATEWAY RESOURCE USAGE (docker stats, sampled every ~1s)':^86}") + print(" " + "-" * 86) + print(f" {'Container':<38} {'Mem avg':>9} {'Mem peak':>9} {'CPU avg':>8} {'CPU peak':>9} {'Samples':>7}") + print(" " + "-" * 86) + total_mem_avg = total_mem_peak = 0.0 + for name, d in gateways + redis_entries: + short = name.replace("mcp-context-forge-", "") + print(f" {short:<38} {d['mem_avg']:>7.1f}M {d['mem_peak']:>7.1f}M {d['cpu_avg']:>7.1f}% {d['cpu_peak']:>8.1f}% {int(d['samples']):>7}") + if DOCKER_GATEWAY_PATTERN in name: + total_mem_avg += d["mem_avg"] + total_mem_peak += d["mem_peak"] + if len(gateways) > 1: + print(" " + "-" * 86) + print(f" {'All gateways combined':<38} {total_mem_avg:>7.1f}M {total_mem_peak:>7.1f}M") + + # Redis memory timeline — the key comparison metric + all_timeline = ([_redis_baseline] if _redis_baseline else []) + _redis_timeline + if all_timeline: + print(f"\n {'REDIS MEMORY TIMELINE (polled every 5s)':^86}") + print(" " + "-" * 86) + print(f" {'Elapsed':>8} {'Active users':>14} {'RL keys':>10} {'Total keys':>11} {'Redis mem':>12} Note") + print(" " + "-" * 86) + + spawn_duration = RL_USERS / RL_SPAWN_RATE + for entry in all_timeline: + elapsed = entry["elapsed"] + users = entry["users"] + rl_keys = entry.get("rl_keys", entry.get("keys", 0)) + total_keys = entry.get("total_keys", rl_keys) + mem = entry["mem_mib"] + note = "" + if elapsed < 5: + note = "← baseline (before users spawn)" + elif elapsed <= spawn_duration + 5: + note = f"ramping up ({users}/{RL_USERS} users active)" + elif elapsed > spawn_duration + 5: + note = "all users active — steady state" + print(f" {elapsed:>7.0f}s {users:>14} {rl_keys:>10,} {total_keys:>11,} {mem:>10.2f} MiB {note}") + + # Show the key insight: memory per RL key + # Use pre-bootstrap baseline for noise-floor, steady-state sample for peak. + # Math: delta_mem / delta_rl_keys from the SAME sample pair (not mixing max(mem) with max(keys)). + if len(all_timeline) >= 2: + baseline_entry = _redis_baseline if _redis_baseline else all_timeline[0] + baseline_mem = baseline_entry["mem_mib"] + baseline_rl_keys = baseline_entry.get("rl_keys", 0) + + spawn_end = RL_USERS / RL_SPAWN_RATE + steady = [e for e in _redis_timeline if e["elapsed"] > spawn_end + 10] + if not steady: + steady = _redis_timeline[-min(3, len(_redis_timeline)) :] + + if steady: + # Pick the steady-state sample with the most RL keys (most representative) + best = max(steady, key=lambda e: e.get("rl_keys", 0)) + delta_mem = best["mem_mib"] - baseline_mem + delta_rl_keys = best.get("rl_keys", 0) - baseline_rl_keys + per_key = (delta_mem * 1024 / delta_rl_keys) if delta_rl_keys > 0 else 0 + + print(" " + "-" * 86) + print(f" Baseline Redis mem: {baseline_mem:.2f} MiB (pre-bootstrap, {baseline_rl_keys:,} RL keys)") + print(f" Steady-state mem: {best['mem_mib']:.2f} MiB (+{delta_mem:.2f} MiB delta, {best.get('rl_keys', 0):,} RL keys)") + print(f" RL key delta: {delta_rl_keys:,} (baseline-subtracted)") + if delta_rl_keys > 0: + print(f" Mem per RL key: {per_key:.2f} KiB (delta_mem / delta_keys — same sample)") + print(f"\n Expected per-key cost by algorithm:") + print(f" fixed_window: ~0.1–0.3 KiB (single integer + TTL)") + print(f" sliding_window: ~1–3 KiB (sorted set, {RL_LIMIT_PER_MIN} float entries)") + print(f" token_bucket: ~0.2 KiB (hash: tokens + last_refill)") + if per_key < 0.5: + verdict = "✅ consistent with fixed_window" + elif per_key <= 5.0: + verdict = "✅ consistent with sliding_window or token_bucket" + else: + verdict = "⚠️ higher than expected — investigate" + print(f"\n Observed: {per_key:.2f} KiB/key → {verdict}") + + # Latency + if total_http > 0: + print(f"\n Response Times (ms):") + print(f" Average: {stats.total.avg_response_time:>8.1f}") + print(f" p50: {stats.total.get_response_time_percentile(0.50):>8.1f}") + print(f" p90: {stats.total.get_response_time_percentile(0.90):>8.1f}") + print(f" p99: {stats.total.get_response_time_percentile(0.99):>8.1f}") + + print("\n To compare algorithms:") + print(" 1. docker exec mcp-context-forge-redis-1 redis-cli FLUSHDB") + print(" 2. Change algorithm: in plugins/config.yaml, restart gateways") + print(" 3. RL_ALGORITHM= make benchmark-rate-limiter-scale") + + print("\n" + "=" * 90 + "\n") + + +# ============================================================================= +# Helpers +# ============================================================================= + + +def _jsonrpc(method: str, params: dict | None = None) -> dict[str, Any]: + body: dict[str, Any] = {"jsonrpc": "2.0", "id": str(uuid.uuid4()), "method": method} + if params is not None: + body["params"] = params + return body + + +# ============================================================================= +# ScaleComparisonUser — each instance has a unique identity +# ============================================================================= + + +class ScaleComparisonUser(FastHttpUser): + """Each locust user has a unique email identity → unique Redis key per user. + + This is what makes Redis memory diverge across algorithms: + fixed_window → 1 integer key per user + sliding_window → 1 sorted-set key per user (W entries each) + token_bucket → no Redis key (memory-only fallback) + """ + + wait_time = constant_throughput(_REQS_PER_SECOND) + connection_timeout = 30.0 + network_timeout = 30.0 + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + global _user_counter # pylint: disable=global-statement + with _user_counter_lock: + uid = _user_counter + _user_counter += 1 + self._email = f"scale-user-{uid:04d}@loadtest.internal" + self._token = _user_tokens[uid % len(_user_tokens)] if _user_tokens else "" + self._mcp_session_id: str | None = None + self._initialized = False + + def on_start(self) -> None: + global _active_users # pylint: disable=global-statement + with _stats_lock: + _active_users += 1 + self._ensure_initialized() + + def on_stop(self) -> None: + global _active_users # pylint: disable=global-statement + with _stats_lock: + _active_users -= 1 + + def _headers(self) -> dict[str, str]: + h = { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + "Authorization": f"Bearer {self._token}", + } + if self._mcp_session_id: + h["Mcp-Session-Id"] = self._mcp_session_id + return h + + def _mcp_post(self, method: str, params: dict | None, name: str) -> dict | None: + if not _server_id: + return None + try: + with self.client.post( + f"/servers/{_server_id}/mcp", + data=json.dumps(_jsonrpc(method, params)), + headers=self._headers(), + name=name, + catch_response=True, + ) as response: + sid = response.headers.get("Mcp-Session-Id") if response.headers else None + if sid: + self._mcp_session_id = sid + if response.status_code in (502, 503, 504): + response.failure(f"Infrastructure error: {response.status_code}") + return None + if response.status_code != 200: + response.failure(f"HTTP {response.status_code}") + return None + try: + data = response.json() + except Exception: + response.failure("Invalid JSON") + return None + if data is None: + response.failure("Null response") + return None + if "error" in data: + err = data["error"] + response.failure(f"JSON-RPC error {err.get('code', '?')}: {err.get('message', '?')}") + return None + response.success() + return data.get("result") + except Exception as exc: + logger.warning("Request failed (%s): %s", name, exc) + return None + + def _ensure_initialized(self) -> None: + if self._initialized or not _server_id: + return + result = self._mcp_post( + "initialize", + { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": {"name": f"scale-test-{self._email}", "version": "1.0.0"}, + }, + "MCP initialize", + ) + if result is not None: + self._initialized = True + + @task + @tag("rate-limit", "scale", "tools") + def call_tool(self) -> None: + """Call a tool; classify the response in-place as allowed, rate-limited, or infra-error. + + A single tools/call request is sent. The Locust stat name is set based on + the semantic outcome — no second request is fired: + - 'MCP tools/call [allowed]' — gateway processed the call normally + - 'MCP tools/call [rate-limited]' — HTTP 429 or MCP result.isError (plugin rate-limit block) + - 'MCP tools/call [infra-error]' — HTTP error or malformed response + """ + if not _tool_names or not _server_id: + return + + tool = _tool_names[0] + name_lower = tool.lower() + if "time" in name_lower or "timezone" in name_lower: + args: dict[str, Any] = {"timezone": "UTC"} + elif "convert" in name_lower: + args = {"time": "2025-01-01T00:00:00Z", "source_timezone": "UTC", "target_timezone": "Europe/London"} + elif "echo" in name_lower: + args = {"message": "scale-test"} + else: + args = {} + + try: + with self.client.post( + f"/servers/{_server_id}/mcp", + data=json.dumps(_jsonrpc("tools/call", {"name": tool, "arguments": args})), + headers=self._headers(), + name="MCP tools/call", + catch_response=True, + ) as response: + sid = response.headers.get("Mcp-Session-Id") if response.headers else None + if sid: + self._mcp_session_id = sid + + if response.status_code == 429: + response.request_meta["name"] = "MCP tools/call [rate-limited]" + response.success() + return + if response.status_code in (502, 503, 504): + response.request_meta["name"] = "MCP tools/call [infra-error]" + response.failure(f"Infrastructure error: {response.status_code}") + return + if response.status_code != 200: + response.request_meta["name"] = "MCP tools/call [infra-error]" + response.failure(f"HTTP {response.status_code}") + return + try: + data = response.json() + except Exception: + response.request_meta["name"] = "MCP tools/call [infra-error]" + response.failure("Invalid JSON") + return + if data is None: + response.request_meta["name"] = "MCP tools/call [infra-error]" + response.failure("Null response") + return + if "error" in data: + err = data["error"] + err_msg = str(err.get("message", "")).lower() + err_data_str = str(err.get("data", "")).lower() + # Rate-limit violations may arrive as JSON-RPC errors when the + # PluginViolationError is caught by FastAPI's global handler. + if "rate" in err_msg or "rate_limit" in err_data_str or err.get("code") == 429: + response.request_meta["name"] = "MCP tools/call [rate-limited]" + response.success() + bucket = int((time.time() - _test_start_time) / 30) + with _stats_lock: + _bucket_stats[bucket]["blocked"] += 1 + return + response.request_meta["name"] = "MCP tools/call [infra-error]" + response.failure(f"JSON-RPC error {err.get('code', '?')}: {err.get('message', '?')}") + return + + result = data.get("result") + bucket = int((time.time() - _test_start_time) / 30) + if isinstance(result, dict) and result.get("isError"): + response.request_meta["name"] = "MCP tools/call [rate-limited]" + response.success() + with _stats_lock: + _bucket_stats[bucket]["blocked"] += 1 + else: + response.request_meta["name"] = "MCP tools/call [allowed]" + response.success() + with _stats_lock: + _bucket_stats[bucket]["allowed"] += 1 + except Exception as exc: + logger.warning("Request failed (tools/call): %s", exc) diff --git a/tests/performance/plugins/config.yaml b/tests/performance/plugins/config.yaml index 0c12ba9167..49b0282835 100644 --- a/tests/performance/plugins/config.yaml +++ b/tests/performance/plugins/config.yaml @@ -236,12 +236,108 @@ plugins: conditions: [] config: {} - # Rate limiter (fixed window, in-memory) + # Rate limiter (fixed_window algorithm, memory backend — default) - name: RateLimiterPlugin kind: plugins.rate_limiter.rate_limiter.RateLimiterPlugin - description: Per-user/tenant/tool rate limits + description: Per-user/tenant/tool rate limits — fixed window algorithm version: 0.1.0 - author: Mihai Criveti + author: Pratik Gandhi + hooks: [prompt_pre_fetch, tool_pre_invoke] + tags: [limits, throttle] + mode: permissive + priority: 20 + conditions: [] + config: + by_user: 60/m + by_tenant: 600/m + by_tool: + search: 10/m + algorithm: fixed_window + + # Rate limiter (sliding_window algorithm, memory backend) + - name: RateLimiterPlugin_sliding_window + kind: plugins.rate_limiter.rate_limiter.RateLimiterPlugin + description: Per-user/tenant/tool rate limits — sliding window algorithm + version: 0.1.0 + author: Pratik Gandhi + hooks: [prompt_pre_fetch, tool_pre_invoke] + tags: [limits, throttle] + mode: permissive + priority: 20 + conditions: [] + config: + by_user: 60/m + by_tenant: 600/m + by_tool: + search: 10/m + algorithm: sliding_window + + # Rate limiter (token_bucket algorithm, memory backend) + - name: RateLimiterPlugin_token_bucket + kind: plugins.rate_limiter.rate_limiter.RateLimiterPlugin + description: Per-user/tenant/tool rate limits — token bucket algorithm + version: 0.1.0 + author: Pratik Gandhi + hooks: [prompt_pre_fetch, tool_pre_invoke] + tags: [limits, throttle] + mode: permissive + priority: 20 + conditions: [] + config: + by_user: 60/m + by_tenant: 600/m + by_tool: + search: 10/m + algorithm: token_bucket + + # Rate limiter (fixed_window algorithm, Redis backend) + - name: RateLimiterPlugin_redis_fixed_window + kind: plugins.rate_limiter.rate_limiter.RateLimiterPlugin + description: Per-user/tenant/tool rate limits — fixed window algorithm, Redis backend + version: 0.1.0 + author: Pratik Gandhi + hooks: [prompt_pre_fetch, tool_pre_invoke] + tags: [limits, throttle] + mode: permissive + priority: 20 + conditions: [] + config: + by_user: 60/m + by_tenant: 600/m + by_tool: + search: 10/m + algorithm: fixed_window + backend: redis + redis_url: redis://localhost:6379/0 + redis_fallback: true + + # Rate limiter (sliding_window algorithm, Redis backend) + - name: RateLimiterPlugin_redis_sliding_window + kind: plugins.rate_limiter.rate_limiter.RateLimiterPlugin + description: Per-user/tenant/tool rate limits — sliding window algorithm, Redis backend + version: 0.1.0 + author: Pratik Gandhi + hooks: [prompt_pre_fetch, tool_pre_invoke] + tags: [limits, throttle] + mode: permissive + priority: 20 + conditions: [] + config: + by_user: 60/m + by_tenant: 600/m + by_tool: + search: 10/m + algorithm: sliding_window + backend: redis + redis_url: redis://localhost:6379/0 + redis_fallback: true + + # Rate limiter (token_bucket algorithm, Redis backend) + - name: RateLimiterPlugin_redis_token_bucket + kind: plugins.rate_limiter.rate_limiter.RateLimiterPlugin + description: Per-user/tenant/tool rate limits — token bucket algorithm, Redis backend + version: 0.1.0 + author: Pratik Gandhi hooks: [prompt_pre_fetch, tool_pre_invoke] tags: [limits, throttle] mode: permissive @@ -252,6 +348,10 @@ plugins: by_tenant: 600/m by_tool: search: 10/m + algorithm: token_bucket + backend: redis + redis_url: redis://localhost:6379/0 + redis_fallback: true # Schema guard for tool args/results (subset JSONSchema) - name: SchemaGuardPlugin diff --git a/tests/unit/mcpgateway/plugins/plugins/rate_limiter/test_rate_limiter.py b/tests/unit/mcpgateway/plugins/plugins/rate_limiter/test_rate_limiter.py index 49f8f4211a..3b37ebcc18 100644 --- a/tests/unit/mcpgateway/plugins/plugins/rate_limiter/test_rate_limiter.py +++ b/tests/unit/mcpgateway/plugins/plugins/rate_limiter/test_rate_limiter.py @@ -7,13 +7,17 @@ Tests for RateLimiterPlugin. """ +# Standard import asyncio +import os import time from typing import Any, Dict from unittest.mock import patch +# Third-Party import pytest +# First-Party from mcpgateway.plugins.framework import ( GlobalContext, PluginConfig, @@ -27,24 +31,45 @@ from mcpgateway.plugins.framework.errors import PluginViolationError from mcpgateway.plugins.framework.manager import PluginExecutor from mcpgateway.plugins.framework.models import PluginMode -from plugins.rate_limiter.rate_limiter import RateLimiterPlugin, _make_headers, _parse_rate, _select_most_restrictive, _store +from plugins.rate_limiter.rate_limiter import ( + _extract_user_identity, + _make_headers, + _parse_rate, + _select_most_restrictive, + ALGORITHM_FIXED_WINDOW, + ALGORITHM_SLIDING_WINDOW, + ALGORITHM_TOKEN_BUCKET, + FixedWindowAlgorithm, + MemoryBackend, + RateLimiterPlugin, + RedisBackend, + RustRateLimiterEngine, + SlidingWindowAlgorithm, + TokenBucketAlgorithm, +) + + +def _clear_plugin(plugin: RateLimiterPlugin) -> None: + """Clear the algorithm store for a plugin instance.""" + backend = plugin._rate_backend + if isinstance(backend, MemoryBackend): + backend._algorithm._store.clear() @pytest.fixture(autouse=True) def clear_rate_limit_store(): - """Clear the rate limiter store before each test to ensure test isolation.""" - _store.clear() + """No-op: each test creates its own plugin instance with a fresh store. + Individual tests call _clear_plugin() when sharing a plugin across steps.""" yield - _store.clear() -def _mk(rate: str) -> RateLimiterPlugin: +def _mk(rate: str, algorithm: str = ALGORITHM_FIXED_WINDOW) -> RateLimiterPlugin: return RateLimiterPlugin( PluginConfig( name="rl", kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", hooks=[PromptHookType.PROMPT_PRE_FETCH, ToolHookType.TOOL_PRE_INVOKE], - config={"by_user": rate}, + config={"by_user": rate, "algorithm": algorithm}, ) ) @@ -148,6 +173,7 @@ async def test_prompt_pre_fetch_success_includes_headers_without_retry_after(): @pytest.mark.asyncio async def test_tool_pre_invoke_violation_returns_http_429(): """Test that tool_pre_invoke violations return HTTP 429 status code.""" + # First-Party from mcpgateway.plugins.framework import ToolPreInvokePayload plugin = _mk("1/s") @@ -168,6 +194,7 @@ async def test_tool_pre_invoke_violation_returns_http_429(): @pytest.mark.asyncio async def test_tool_pre_invoke_violation_includes_headers(): """Test that tool_pre_invoke violations include rate limit headers.""" + # First-Party from mcpgateway.plugins.framework import ToolPreInvokePayload plugin = _mk("2/s") @@ -194,6 +221,7 @@ async def test_tool_pre_invoke_violation_includes_headers(): @pytest.mark.asyncio async def test_tool_pre_invoke_success_includes_headers_without_retry_after(): """Test that successful tool invocations include headers but not Retry-After.""" + # First-Party from mcpgateway.plugins.framework import ToolPreInvokePayload plugin = _mk("10/s") @@ -215,6 +243,7 @@ async def test_tool_pre_invoke_success_includes_headers_without_retry_after(): @pytest.mark.asyncio async def test_tool_pre_invoke_per_tool_rate_limiting(): """Test per-tool rate limiting configuration.""" + # First-Party from mcpgateway.plugins.framework import ToolPreInvokePayload plugin = RateLimiterPlugin( @@ -222,12 +251,7 @@ async def test_tool_pre_invoke_per_tool_rate_limiting(): name="rl", kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", hooks=[ToolHookType.TOOL_PRE_INVOKE], - config={ - "by_user": "100/s", # High user limit - "by_tool": { - "restricted_tool": "1/s" # Low tool-specific limit - } - }, + config={"by_user": "100/s", "by_tool": {"restricted_tool": "1/s"}}, # High user limit # Low tool-specific limit ) ) @@ -278,6 +302,7 @@ def test_make_headers_without_retry_after(): # _select_most_restrictive TESTS # ============================================================================ + class TestSelectMostRestrictive: """Comprehensive tests for _select_most_restrictive function.""" @@ -341,7 +366,7 @@ def test_multiple_violated_shortest_reset_wins(self): """When multiple violated, select the one with shortest reset time.""" now = 1000 results = [ - (False, 10, now + 30, {"limited": True, "remaining": 0, "reset_in": 30}), # Resets sooner + (False, 10, now + 30, {"limited": True, "remaining": 0, "reset_in": 30}), # Resets sooner (False, 20, now + 60, {"limited": True, "remaining": 0, "reset_in": 60}), (False, 30, now + 120, {"limited": True, "remaining": 0, "reset_in": 120}), ] @@ -357,8 +382,8 @@ def test_violated_with_allowed_dimensions(self): now = 1000 results = [ (True, 100, now + 60, {"limited": True, "remaining": 90, "reset_in": 60}), # Allowed - (False, 50, now + 30, {"limited": True, "remaining": 0, "reset_in": 30}), # Violated (shortest) - (False, 75, now + 90, {"limited": True, "remaining": 0, "reset_in": 90}), # Violated + (False, 50, now + 30, {"limited": True, "remaining": 0, "reset_in": 30}), # Violated (shortest) + (False, 75, now + 90, {"limited": True, "remaining": 0, "reset_in": 90}), # Violated ] allowed, limit, remaining, reset_ts, meta = _select_most_restrictive(results) assert allowed is False @@ -557,7 +582,7 @@ def test_hours_long(self): assert _parse_rate("100/hour") == (100, 3600) def test_unsupported_unit_raises(self): - with pytest.raises(ValueError, match="Unsupported rate unit"): + with pytest.raises(ValueError, match="unsupported unit"): _parse_rate("10/d") def test_whitespace_stripped(self): @@ -598,6 +623,7 @@ async def test_prompt_pre_fetch_unlimited_returns_no_headers(): @pytest.mark.asyncio async def test_tool_pre_invoke_unlimited_returns_no_headers(): """When no limits are configured, tool_pre_invoke returns metadata without http_headers.""" + # First-Party from mcpgateway.plugins.framework import ToolPreInvokePayload plugin = _mk_unlimited() @@ -632,6 +658,7 @@ async def test_redis_backend_shares_state_across_instances(): a live Redis server. The fake client uses its own dict (separate from _store) to simulate shared Redis state. """ + # Standard import time as _time class _FakeRedis: @@ -672,15 +699,14 @@ async def eval(self, script: str, numkeys: int, *args: Any) -> list[int]: assert r1.violation is None assert r2.violation is None - # Simulate Worker 2 starting fresh — clear local _store (has no effect on Redis counter) - _store.clear() + # Simulate Worker 2 starting fresh — clearing the local memory store has no effect + # on the Redis counter (the fake Redis client uses its own dict, not the plugin store) + if isinstance(plugin._rate_backend, MemoryBackend): + plugin._rate_backend._algorithm._store.clear() # Worker 2 shares the same Redis — alice's counter is still 2, next request is blocked r3 = await plugin.tool_pre_invoke(payload, ctx) - assert r3.violation is not None, ( - "alice made 3 requests total (limit is 2). With Redis backend, clearing " - "_store has no effect — the counter persists in Redis across all workers." - ) + assert r3.violation is not None, "alice made 3 requests total (limit is 2). With Redis backend, clearing " "local state has no effect — the counter persists in Redis across all workers." assert r3.violation.http_status_code == 429 @@ -692,28 +718,30 @@ async def test_store_evicts_expired_windows(): MemoryBackend starts a background asyncio task on first use that sweeps expired windows every 0.5s. Entries for users who never return are evicted automatically, bounding memory growth to active windows only. + + The Rust engine does not use the Python MemoryBackend store — this test is + exercising the Python fallback path's sweep behaviour. """ - plugin = _mk("5/s") + # First-Party + import plugins.rate_limiter.rate_limiter as _rl_mod + + with patch.object(_rl_mod, "_RUST_AVAILABLE", False): + plugin = _mk("5/s") + store = plugin._rate_backend._algorithm._store UNIQUE_USERS = 100 - # Each unique user creates one entry in _store + # Each unique user creates one entry in the algorithm store for i in range(UNIQUE_USERS): ctx = PluginContext(global_context=GlobalContext(request_id=f"r{i}", user=f"user_{i}")) payload = ToolPreInvokePayload(name="test_tool", arguments={}) await plugin.tool_pre_invoke(payload, ctx) - assert len(_store) == UNIQUE_USERS # confirm entries were created + assert len(store) == UNIQUE_USERS # confirm entries were created - # Wait for all 1-second windows to expire + # Wait for all 1-second windows to expire and the sweep to run await asyncio.sleep(1.1) - # Expected: expired entries are evicted, _store is empty (or much smaller) - # Actual: _store still holds all UNIQUE_USERS entries indefinitely - assert len(_store) == 0, ( - f"Expected _store to be empty after all windows expired, " - f"but found {len(_store)} stale entries. " - f"No eviction mechanism exists — _store grows without bound." - ) + assert len(store) == 0, f"Expected store to be empty after all windows expired, " f"but found {len(store)} stale entries. " @pytest.mark.asyncio @@ -733,9 +761,7 @@ async def test_concurrent_requests_respect_limit(): ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) payload = ToolPreInvokePayload(name="test_tool", arguments={}) - results = await asyncio.gather(*[ - plugin.tool_pre_invoke(payload, ctx) for _ in range(20) - ]) + results = await asyncio.gather(*[plugin.tool_pre_invoke(payload, ctx) for _ in range(20)]) allowed = sum(1 for r in results if r.violation is None) @@ -746,27 +772,27 @@ async def test_concurrent_requests_respect_limit(): ) -@pytest.mark.xfail( - strict=True, - reason=( - "Gap: fixed window allows 2× the limit at a window boundary. " - "N requests at end of W1 + N requests at start of W2 all succeed." - ), -) @pytest.mark.asyncio -async def test_fixed_window_burst_at_boundary(): - """ - A user can burst at a window boundary: N requests at the end of window W1 - and N requests at the start of W2 both succeed, giving 2× the limit in practice. +async def test_fixed_window_allows_boundary_burst(): + """Empirical proof: fixed_window allows 2× the limit at a window boundary. + + A user sends N requests at the end of window W1 and N more at the start of + W2. All 2N succeed because the counter resets at the boundary. Example with limit=5/s: t=1000: requests 1-5 → allowed (window W1, count=5) t=1001: requests 6-10 → allowed (window W2 resets, count=1..5) Total = 10 requests in ~1 second against a limit of 5/s. - Fix: use a sliding window or token bucket algorithm. + This is the expected behavior of the fixed_window algorithm — not a bug, + but a documented trade-off. Use sliding_window or token_bucket to prevent + boundary bursts (see companion test below). """ - plugin = _mk("5/s") + # First-Party + import plugins.rate_limiter.rate_limiter as _rl_mod + + with patch.object(_rl_mod, "_RUST_AVAILABLE", False): + plugin = _mk("5/s") ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) payload = ToolPreInvokePayload(name="test_tool", arguments={}) @@ -787,13 +813,53 @@ async def test_fixed_window_burst_at_boundary(): if r.violation is None: allowed_total += 1 - # Expected: a sliding window would cap total at ~5-6 across the boundary - # Actual: fixed window allows all 10 (5 in W1 + 5 in W2) - assert allowed_total <= 5, ( - f"Fixed window burst: {allowed_total} requests allowed across the window " - f"boundary. Configured limit is 5/s. " - f"Fix: replace fixed window with a sliding window or token bucket." - ) + # fixed_window: all 10 allowed (5 in W1 + 5 in W2 = 2× limit in ~1 second) + assert allowed_total == 10, f"Expected fixed_window to allow 2× the limit at boundary, got {allowed_total}/10" + + +@pytest.mark.asyncio +async def test_sliding_window_prevents_boundary_burst(): + """Companion proof: sliding_window prevents the boundary burst that fixed_window allows. + + Same scenario as test_fixed_window_allows_boundary_burst but with + sliding_window. The 5 requests from W1 are still within the sliding window + when W2 starts, so the second batch is blocked. + """ + # First-Party + import plugins.rate_limiter.rate_limiter as _rl_mod + + with patch.object(_rl_mod, "_RUST_AVAILABLE", False): + plugin = RateLimiterPlugin( + PluginConfig( + name="rl-sw", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=[ToolHookType.TOOL_PRE_INVOKE], + config={"by_user": "5/s", "algorithm": ALGORITHM_SLIDING_WINDOW}, + ) + ) + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="test_tool", arguments={}) + + allowed_total = 0 + + with patch("plugins.rate_limiter.rate_limiter.time") as mock_time: + # Window W1: fill the limit exactly at t=1000 + mock_time.time.return_value = 1000.0 + for _ in range(5): + r = await plugin.tool_pre_invoke(payload, ctx) + if r.violation is None: + allowed_total += 1 + + # Half a second later: W1 timestamps are still within the 1s sliding window + mock_time.time.return_value = 1000.5 + for _ in range(5): + r = await plugin.tool_pre_invoke(payload, ctx) + if r.violation is None: + allowed_total += 1 + + # sliding_window: only 5 allowed — the W1 timestamps at t=1000 are still + # within the window at t=1000.5, so the second batch is blocked. + assert allowed_total == 5, f"Expected sliding_window to prevent boundary burst, got {allowed_total}/10 allowed" @pytest.mark.asyncio @@ -810,7 +876,7 @@ async def test_prompt_pre_fetch_enforces_by_tool_config(): kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", hooks=[PromptHookType.PROMPT_PRE_FETCH], config={ - "by_user": "100/s", # High — will not trigger + "by_user": "100/s", # High — will not trigger "by_tool": {"search": "2/s"}, # Low — should trigger on 3rd call }, ) @@ -828,9 +894,7 @@ async def test_prompt_pre_fetch_enforces_by_tool_config(): # Expected: blocked because by_tool["search"] = 2/s is exhausted # Actual: allowed — prompt_pre_fetch never reads by_tool assert r3.violation is not None, ( - "Expected 3rd prompt_pre_fetch call to be blocked by by_tool limit (2/s). " - "prompt_pre_fetch does not check by_tool — tool-level limits only apply " - "to tool_pre_invoke." + "Expected 3rd prompt_pre_fetch call to be blocked by by_tool limit (2/s). " "prompt_pre_fetch does not check by_tool — tool-level limits only apply " "to tool_pre_invoke." ) @@ -863,10 +927,10 @@ async def test_empty_string_user_falls_back_to_anonymous(): @pytest.mark.asyncio -async def test_none_tenant_falls_back_to_default_bucket(): +async def test_none_tenant_skips_by_tenant_check(): """ - None tenant_id falls back to 'default'. Multiple users with no tenant - share the same tenant bucket — they can exhaust each other's tenant limit. + None tenant_id must skip the by_tenant dimension entirely — no shared 'default' bucket. + Multiple users with no tenant ID must not cross-throttle each other. """ plugin = RateLimiterPlugin( PluginConfig( @@ -883,12 +947,11 @@ async def test_none_tenant_falls_back_to_default_bucket(): r1 = await plugin.tool_pre_invoke(payload, ctx_alice) r2 = await plugin.tool_pre_invoke(payload, ctx_bob) - r3 = await plugin.tool_pre_invoke(payload, ctx_alice) # tenant "default" exhausted + r3 = await plugin.tool_pre_invoke(payload, ctx_alice) # by_tenant skipped — must not block assert r1.violation is None assert r2.violation is None - assert r3.violation is not None # both users share "default" tenant bucket - assert r3.violation.http_status_code == 429 + assert r3.violation is None # by_tenant is skipped when tenant_id is None @pytest.mark.asyncio @@ -901,7 +964,7 @@ async def test_unicode_user_id_is_rate_limited_correctly(): payload = ToolPreInvokePayload(name="test_tool", arguments={}) for user in ["用户@example.com", "ユーザー@test.jp", "مستخدم@example.com", "user🎉@example.com"]: - _store.clear() + _clear_plugin(plugin) ctx = PluginContext(global_context=GlobalContext(request_id="r1", user=user)) r1 = await plugin.tool_pre_invoke(payload, ctx) @@ -1141,6 +1204,10 @@ async def test_redis_fallback_to_memory_when_redis_unavailable(): When the Redis client raises an exception (simulating Redis being down), and redis_fallback=True, the plugin falls back to MemoryBackend and the request succeeds rather than erroring. + + Forces _RUST_AVAILABLE=False so the Python RedisBackend path is exercised — + the Rust engine owns its own Redis connection and is not affected by + injecting a broken client into _rate_backend._client. """ class _BrokenRedis: @@ -1149,14 +1216,18 @@ class _BrokenRedis: async def eval(self, *args: Any, **kwargs: Any) -> None: raise ConnectionError("Redis is down") - plugin = RateLimiterPlugin( - PluginConfig( - name="rl", - kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", - hooks=[ToolHookType.TOOL_PRE_INVOKE], - config={"by_user": "10/s", "backend": "redis", "redis_url": "redis://localhost:6379/0", "redis_fallback": True}, + # First-Party + import plugins.rate_limiter.rate_limiter as _rl_mod + + with patch.object(_rl_mod, "_RUST_AVAILABLE", False): + plugin = RateLimiterPlugin( + PluginConfig( + name="rl", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=[ToolHookType.TOOL_PRE_INVOKE], + config={"by_user": "10/s", "backend": "redis", "redis_url": "redis://localhost:6379/0", "redis_fallback": True}, + ) ) - ) plugin._rate_backend._client = _BrokenRedis() ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) @@ -1171,20 +1242,28 @@ async def test_redis_fallback_enforces_limit_via_memory(): """ After falling back to memory, the MemoryBackend still enforces the rate limit correctly — the fallback is not a free pass. + + Forces _RUST_AVAILABLE=False so the Python RedisBackend path is exercised — + the Rust engine owns its own Redis connection and is not affected by + injecting a broken client into _rate_backend._client. """ class _BrokenRedis: async def eval(self, *args: Any, **kwargs: Any) -> None: raise ConnectionError("Redis is down") - plugin = RateLimiterPlugin( - PluginConfig( - name="rl", - kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", - hooks=[ToolHookType.TOOL_PRE_INVOKE], - config={"by_user": "2/s", "backend": "redis", "redis_url": "redis://localhost:6379/0", "redis_fallback": True}, + # First-Party + import plugins.rate_limiter.rate_limiter as _rl_mod + + with patch.object(_rl_mod, "_RUST_AVAILABLE", False): + plugin = RateLimiterPlugin( + PluginConfig( + name="rl", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=[ToolHookType.TOOL_PRE_INVOKE], + config={"by_user": "2/s", "backend": "redis", "redis_url": "redis://localhost:6379/0", "redis_fallback": True}, + ) ) - ) plugin._rate_backend._client = _BrokenRedis() ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) @@ -1326,9 +1405,7 @@ async def test_retry_after_is_within_window_duration(): assert result.violation is not None retry_after = int(result.violation.http_headers["Retry-After"]) - assert 1 <= retry_after <= 1, ( - f"For a 1/s limit, Retry-After should be 1 second, got {retry_after}" - ) + assert 1 <= retry_after <= 1, f"For a 1/s limit, Retry-After should be 1 second, got {retry_after}" @pytest.mark.asyncio @@ -1346,9 +1423,7 @@ async def test_retry_after_for_minute_window_is_bounded(): assert result.violation is not None retry_after = int(result.violation.http_headers["Retry-After"]) - assert 1 <= retry_after <= 60, ( - f"For a 1/m limit, Retry-After should be 1–60 seconds, got {retry_after}" - ) + assert 1 <= retry_after <= 60, f"For a 1/m limit, Retry-After should be 1–60 seconds, got {retry_after}" @pytest.mark.asyncio @@ -1389,10 +1464,7 @@ async def test_x_ratelimit_reset_consistent_within_window(): reset2 = r2.http_headers["X-RateLimit-Reset"] reset3 = r3.http_headers["X-RateLimit-Reset"] - assert reset1 == reset2 == reset3, ( - f"X-RateLimit-Reset must be identical across all requests in the same window. " - f"Got {reset1}, {reset2}, {reset3}" - ) + assert reset1 == reset2 == reset3, f"X-RateLimit-Reset must be identical across all requests in the same window. " f"Got {reset1}, {reset2}, {reset3}" @pytest.mark.asyncio @@ -1411,9 +1483,7 @@ async def test_x_ratelimit_remaining_decrements_correctly(): assert r.violation is None results.append(int(r.http_headers["X-RateLimit-Remaining"])) - assert results == [4, 3, 2, 1, 0], ( - f"X-RateLimit-Remaining should count down 4→3→2→1→0, got {results}" - ) + assert results == [4, 3, 2, 1, 0], f"X-RateLimit-Remaining should count down 4→3→2→1→0, got {results}" # ============================================================================ @@ -1445,22 +1515,9 @@ async def test_bypass_none_user_falls_back_to_anonymous_bucket(): assert r1.violation is None assert r2.violation is None - assert r3.violation is not None, ( - "None and empty-string users share the 'anonymous' bucket — " - "a third request must be blocked regardless of which falsy identity sent it" - ) + assert r3.violation is not None, "None and empty-string users share the 'anonymous' bucket — " "a third request must be blocked regardless of which falsy identity sent it" -@pytest.mark.xfail( - strict=True, - reason=( - "Gap: whitespace-only user identity (e.g. ' ') is truthy so it does NOT " - "resolve to 'anonymous'. It creates its own bucket 'user: ', separate from " - "the anonymous bucket and from real users — a caller can exhaust the anonymous " - "bucket and then switch to whitespace strings to get a fresh quota. " - "Fix: strip and normalise user identity before using it as a bucket key." - ), -) @pytest.mark.asyncio async def test_bypass_whitespace_user_shares_anonymous_bucket(): """ @@ -1482,20 +1539,9 @@ async def test_bypass_whitespace_user_shares_anonymous_bucket(): # Whitespace user should be in the same bucket → blocked r = await plugin.tool_pre_invoke(payload, ctx_ws) - assert r.violation is not None, ( - "Whitespace-only user identity should share the 'anonymous' bucket. " - "Currently it creates its own bucket, bypassing the anonymous limit." - ) + assert r.violation is not None, "Whitespace-only user identity should share the 'anonymous' bucket. " "Currently it creates its own bucket, bypassing the anonymous limit." -@pytest.mark.xfail( - strict=True, - reason=( - "Gap: by_tool matching is case-sensitive (exact dict key lookup). " - "A caller can bypass a per-tool limit on 'search' by calling 'Search' or 'SEARCH'. " - "Fix: normalise tool names to lowercase before matching against by_tool keys." - ), -) @pytest.mark.asyncio async def test_bypass_tool_name_case_sensitivity(): """ @@ -1521,21 +1567,9 @@ async def test_bypass_tool_name_case_sensitivity(): # Calling with different casing should still be caught by the same limit r = await plugin.tool_pre_invoke(payload_upper, ctx) - assert r.violation is not None, ( - "'Search' should be subject to the same 1/s limit as 'search'. " - "Case-insensitive matching is not implemented — this is a bypass vector." - ) + assert r.violation is not None, "'Search' should be subject to the same 1/s limit as 'search'. " "Case-insensitive matching is not implemented — this is a bypass vector." -@pytest.mark.xfail( - strict=True, - reason=( - "Gap: by_tool matching uses exact string comparison. A tool name with a " - "leading or trailing space (' search') does not match the configured key " - "('search') and gets an unlimited quota. " - "Fix: strip tool names before matching against by_tool keys." - ), -) @pytest.mark.asyncio async def test_bypass_tool_name_whitespace(): """ @@ -1559,10 +1593,7 @@ async def test_bypass_tool_name_whitespace(): # Whitespace variant should be caught by the same limit r = await plugin.tool_pre_invoke(ToolPreInvokePayload(name=" search", arguments={}), ctx) - assert r.violation is not None, ( - "' search' (leading space) should be subject to the same limit as 'search'. " - "Whitespace stripping is not implemented — this is a bypass vector." - ) + assert r.violation is not None, "' search' (leading space) should be subject to the same limit as 'search'. " "Whitespace stripping is not implemented — this is a bypass vector." @pytest.mark.asyncio @@ -1585,9 +1616,7 @@ async def test_bypass_anonymous_exhaustion_does_not_affect_real_users(): # Alice is a real user — her bucket is untouched r = await plugin.tool_pre_invoke(payload, ctx_alice) - assert r.violation is None, ( - "Exhausting the anonymous bucket must not affect real authenticated users" - ) + assert r.violation is None, "Exhausting the anonymous bucket must not affect real authenticated users" # ============================================================================ @@ -1612,8 +1641,7 @@ async def test_violation_description_does_not_contain_user_identity(): assert result.violation is not None assert "alice@example.com" not in result.violation.description, ( - "User identity must not appear in the violation description — " - "it is logged in permissive mode and embedded in PluginViolationError messages" + "User identity must not appear in the violation description — " "it is logged in permissive mode and embedded in PluginViolationError messages" ) @@ -1635,9 +1663,7 @@ async def test_violation_description_does_not_contain_tenant_identity(): result = await plugin.tool_pre_invoke(payload, ctx) assert result.violation is not None - assert "acme-corp" not in result.violation.description, ( - "Tenant identifier must not appear in the violation description" - ) + assert "acme-corp" not in result.violation.description, "Tenant identifier must not appear in the violation description" @pytest.mark.asyncio @@ -1651,9 +1677,7 @@ async def test_prompt_violation_description_does_not_contain_user_identity(): result = await plugin.prompt_pre_fetch(payload, ctx) assert result.violation is not None - assert "bob@example.com" not in result.violation.description, ( - "User identity must not appear in the prompt violation description" - ) + assert "bob@example.com" not in result.violation.description, "User identity must not appear in the prompt violation description" @pytest.mark.asyncio @@ -1686,7 +1710,2874 @@ async def test_bypass_different_tenants_are_intentionally_independent(): # Same user in tenant-2 is allowed — separate counter, by design r = await plugin.tool_pre_invoke(payload, ctx_t2) assert r.violation is None, ( - "tenant-2 has a separate independent counter — this is intentional. " - "Tenant identity comes from the JWT and is controlled by the auth layer, " - "not bypassable by request content." + "tenant-2 has a separate independent counter — this is intentional. " "Tenant identity comes from the JWT and is controlled by the auth layer, " "not bypassable by request content." + ) + + +# ============================================================================ +# Algorithm Strategy Tests +# +# Tests that are specific to each algorithm: sliding_window and token_bucket. +# fixed_window behaviour is already covered by all existing tests above. +# ============================================================================ + + +# --------------------------------------------------------------------------- +# Algorithm selection and validation +# --------------------------------------------------------------------------- + + +def test_invalid_algorithm_raises_at_init(): + """An unrecognised algorithm name must raise ValueError at startup.""" + with pytest.raises(ValueError, match="RateLimiterPlugin config errors"): + RateLimiterPlugin( + PluginConfig( + name="rl", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=[ToolHookType.TOOL_PRE_INVOKE], + config={"by_user": "10/s", "algorithm": "leaky_bucket"}, + ) + ) + + +def test_default_algorithm_is_fixed_window(): + """When algorithm is not specified, fixed_window is used.""" + plugin = RateLimiterPlugin( + PluginConfig( + name="rl", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=[ToolHookType.TOOL_PRE_INVOKE], + config={"by_user": "10/s"}, + ) + ) + assert isinstance(plugin._rate_backend._algorithm, FixedWindowAlgorithm) + + +def test_sliding_window_algorithm_instantiated(): + """sliding_window config results in a SlidingWindowAlgorithm backend.""" + plugin = _mk("10/s", algorithm=ALGORITHM_SLIDING_WINDOW) + assert isinstance(plugin._rate_backend._algorithm, SlidingWindowAlgorithm) + + +def test_token_bucket_algorithm_instantiated(): + """token_bucket config results in a TokenBucketAlgorithm backend.""" + plugin = _mk("10/s", algorithm=ALGORITHM_TOKEN_BUCKET) + assert isinstance(plugin._rate_backend._algorithm, TokenBucketAlgorithm) + + +# --------------------------------------------------------------------------- +# Sliding window correctness +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_sliding_window_basic_enforcement(): + """Sliding window enforces the limit correctly under steady traffic.""" + plugin = _mk("3/s", algorithm=ALGORITHM_SLIDING_WINDOW) + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="test_tool", arguments={}) + + r1 = await plugin.tool_pre_invoke(payload, ctx) + r2 = await plugin.tool_pre_invoke(payload, ctx) + r3 = await plugin.tool_pre_invoke(payload, ctx) + r4 = await plugin.tool_pre_invoke(payload, ctx) # should be blocked + + assert r1.violation is None + assert r2.violation is None + assert r3.violation is None + assert r4.violation is not None + assert r4.violation.http_status_code == 429 + + +@pytest.mark.asyncio +async def test_sliding_window_prevents_burst_at_boundary(): + """ + Sliding window does NOT allow 2× the limit at a window boundary. + + Unlike fixed window, the sliding window tracks exact timestamps. When + requests straddle a boundary, old timestamps are still within the window + and count against the limit — no burst is possible. + """ + plugin = _mk("5/s", algorithm=ALGORITHM_SLIDING_WINDOW) + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="test_tool", arguments={}) + + allowed_total = 0 + + with patch("plugins.rate_limiter.rate_limiter.time") as mock_time: + # End of window W1: fill the limit + mock_time.time.return_value = 1000.9 + for _ in range(5): + r = await plugin.tool_pre_invoke(payload, ctx) + if r.violation is None: + allowed_total += 1 + + # Start of window W2: timestamps from W1 are still within the 1s window + mock_time.time.return_value = 1001.1 + for _ in range(5): + r = await plugin.tool_pre_invoke(payload, ctx) + if r.violation is None: + allowed_total += 1 + + # Sliding window: only requests older than 1s are evicted + # At t=1001.1, cutoff = 1000.1 — all 5 requests at t=1000.9 are still inside + assert allowed_total <= 6, f"Sliding window should prevent boundary burst. Got {allowed_total} allowed " f"(fixed window would allow 10, sliding window should block most of W2)." + + +@pytest.mark.asyncio +async def test_sliding_window_allows_after_window_passes(): + """After the full window duration passes, the sliding window resets naturally.""" + plugin = _mk("2/s", algorithm=ALGORITHM_SLIDING_WINDOW) + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="test_tool", arguments={}) + + # Exhaust the limit + await plugin.tool_pre_invoke(payload, ctx) + await plugin.tool_pre_invoke(payload, ctx) + blocked = await plugin.tool_pre_invoke(payload, ctx) + assert blocked.violation is not None + + # Wait for the window to pass + await asyncio.sleep(1.1) + + # Should be allowed again + r = await plugin.tool_pre_invoke(payload, ctx) + assert r.violation is None, "Requests should be allowed after the sliding window passes" + + +@pytest.mark.asyncio +async def test_sliding_window_returns_429_and_headers(): + """Sliding window violations return HTTP 429 with rate limit headers.""" + plugin = _mk("1/s", algorithm=ALGORITHM_SLIDING_WINDOW) + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="test_tool", arguments={}) + + await plugin.tool_pre_invoke(payload, ctx) + result = await plugin.tool_pre_invoke(payload, ctx) + + assert result.violation is not None + assert result.violation.http_status_code == 429 + assert result.violation.code == "RATE_LIMIT" + assert "X-RateLimit-Limit" in result.violation.http_headers + assert "Retry-After" in result.violation.http_headers + + +# --------------------------------------------------------------------------- +# Token bucket correctness +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_token_bucket_basic_enforcement(): + """Token bucket enforces the limit — once tokens are exhausted requests are blocked.""" + plugin = _mk("3/s", algorithm=ALGORITHM_TOKEN_BUCKET) + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="test_tool", arguments={}) + + r1 = await plugin.tool_pre_invoke(payload, ctx) + r2 = await plugin.tool_pre_invoke(payload, ctx) + r3 = await plugin.tool_pre_invoke(payload, ctx) + r4 = await plugin.tool_pre_invoke(payload, ctx) # bucket empty + + assert r1.violation is None + assert r2.violation is None + assert r3.violation is None + assert r4.violation is not None + assert r4.violation.http_status_code == 429 + + +@pytest.mark.asyncio +async def test_token_bucket_allows_burst_up_to_capacity(): + """ + Token bucket allows an immediate burst up to the full bucket capacity. + + A user who has been idle accumulates tokens. When they send a burst of + requests they can use all accumulated tokens at once — this is intentional + token_bucket behaviour, unlike fixed or sliding window which always enforce + a per-window ceiling. + """ + plugin = _mk("5/s", algorithm=ALGORITHM_TOKEN_BUCKET) + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="test_tool", arguments={}) + + # Send all 5 requests immediately (burst from a full bucket) + results = [] + for _ in range(5): + r = await plugin.tool_pre_invoke(payload, ctx) + results.append(r) + + allowed = sum(1 for r in results if r.violation is None) + assert allowed == 5, f"Token bucket should allow a burst of 5 from a full bucket, got {allowed} allowed" + + # 6th request: bucket is now empty + r6 = await plugin.tool_pre_invoke(payload, ctx) + assert r6.violation is not None, "Bucket should be empty after a full burst" + + +@pytest.mark.asyncio +async def test_token_bucket_refills_over_time(): + """Tokens refill at the configured rate — requests are allowed again after waiting.""" + plugin = _mk("5/s", algorithm=ALGORITHM_TOKEN_BUCKET) + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="test_tool", arguments={}) + + # Drain the bucket + for _ in range(5): + await plugin.tool_pre_invoke(payload, ctx) + + blocked = await plugin.tool_pre_invoke(payload, ctx) + assert blocked.violation is not None, "Bucket should be empty" + + # Wait for at least 1 token to refill (5 tokens/s → 1 token per 0.2s) + await asyncio.sleep(0.3) + + r = await plugin.tool_pre_invoke(payload, ctx) + assert r.violation is None, "At least 1 token should have refilled after 0.3s" + + +@pytest.mark.asyncio +async def test_token_bucket_returns_429_and_headers(): + """Token bucket violations return HTTP 429 with rate limit headers.""" + plugin = _mk("1/s", algorithm=ALGORITHM_TOKEN_BUCKET) + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="test_tool", arguments={}) + + await plugin.tool_pre_invoke(payload, ctx) + result = await plugin.tool_pre_invoke(payload, ctx) + + assert result.violation is not None + assert result.violation.http_status_code == 429 + assert result.violation.code == "RATE_LIMIT" + assert "X-RateLimit-Limit" in result.violation.http_headers + assert "Retry-After" in result.violation.http_headers + + +# --------------------------------------------------------------------------- +# Algorithm isolation — each instance gets its own independent store +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_two_plugin_instances_different_algorithms_independent(): + """Two plugin instances using different algorithms have completely independent stores.""" + plugin_fw = _mk("2/s", algorithm=ALGORITHM_FIXED_WINDOW) + plugin_sw = _mk("2/s", algorithm=ALGORITHM_SLIDING_WINDOW) + plugin_tb = _mk("2/s", algorithm=ALGORITHM_TOKEN_BUCKET) + + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="test_tool", arguments={}) + + # Exhaust fixed window + await plugin_fw.tool_pre_invoke(payload, ctx) + await plugin_fw.tool_pre_invoke(payload, ctx) + blocked_fw = await plugin_fw.tool_pre_invoke(payload, ctx) + assert blocked_fw.violation is not None, "fixed_window alice should be blocked" + + # sliding_window and token_bucket instances are completely unaffected + r_sw = await plugin_sw.tool_pre_invoke(payload, ctx) + r_tb = await plugin_tb.tool_pre_invoke(payload, ctx) + assert r_sw.violation is None, "sliding_window has its own store — should not be blocked" + assert r_tb.violation is None, "token_bucket has its own store — should not be blocked" + + +# --------------------------------------------------------------------------- +# Redis + token_bucket +# --------------------------------------------------------------------------- + + +def test_token_bucket_with_redis_backend_uses_redis_backend(): + """token_bucket with backend=redis instantiates a RedisBackend, not MemoryBackend.""" + plugin = RateLimiterPlugin( + PluginConfig( + name="rl", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=[ToolHookType.TOOL_PRE_INVOKE], + config={ + "by_user": "10/s", + "algorithm": "token_bucket", + "backend": "redis", + "redis_url": "redis://localhost:6379/0", + }, + ) ) + assert isinstance(plugin._rate_backend, RedisBackend) + assert plugin._rate_backend._algorithm_name == ALGORITHM_TOKEN_BUCKET + + +@pytest.mark.asyncio +async def test_redis_token_bucket_enforces_limit(): + """RedisBackend with token_bucket enforces the limit via the Lua script.""" + # Standard + from unittest.mock import AsyncMock # noqa: PLC0415 + + mock_client = AsyncMock() + # First call: allowed=1, remaining=0, time_to_next=0 + # Second call: allowed=0, remaining=0, time_to_next=5 + mock_client.eval.side_effect = [ + [1, 0, 0], + [0, 0, 5], + ] + + backend = RedisBackend( + redis_url="redis://localhost:6379/0", + algorithm_name=ALGORITHM_TOKEN_BUCKET, + _client=mock_client, + ) + + allowed1, _, _, meta1 = await backend.allow("user:alice", "1/s") + allowed2, _, _, meta2 = await backend.allow("user:alice", "1/s") + + assert allowed1 is True + assert meta1["remaining"] == 0 + assert allowed2 is False + assert meta2["remaining"] == 0 + assert meta2["reset_in"] == 5 + + +@pytest.mark.asyncio +async def test_redis_token_bucket_falls_back_to_memory_on_redis_error(): + """RedisBackend token_bucket falls back to MemoryBackend when Redis is unavailable.""" + # Standard + from unittest.mock import AsyncMock # noqa: PLC0415 + + mock_client = AsyncMock() + mock_client.eval.side_effect = ConnectionError("Redis unavailable") + + fallback = MemoryBackend(TokenBucketAlgorithm()) + backend = RedisBackend( + redis_url="redis://localhost:6379/0", + algorithm_name=ALGORITHM_TOKEN_BUCKET, + fallback=fallback, + _client=mock_client, + ) + + allowed, _, _, _ = await backend.allow("user:alice", "5/s") + assert allowed is True + + +# ============================================================================ +# Concurrency Stress Tests +# ============================================================================ + + +@pytest.mark.asyncio +async def test_concurrent_stress_same_key_does_not_over_allow(): + """ + 100 concurrent tasks hitting the same user key with a limit of 10/s. + + The asyncio.Lock in MemoryBackend serialises all allow() calls so the + count increments atomically. Exactly 10 requests must be allowed — no + more, no fewer. + + This is a stronger version of test_concurrent_requests_respect_limit: + 5× more load to surface any lock-ordering or double-increment bugs that + a small gather might miss. + """ + plugin = _mk("10/s") + ctx = PluginContext(global_context=GlobalContext(request_id="stress", user="alice")) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + results = await asyncio.gather(*[plugin.tool_pre_invoke(payload, ctx) for _ in range(100)]) + + allowed = sum(1 for r in results if r.violation is None) + assert allowed == 10, f"Expected exactly 10 allowed, got {allowed} — lock may not be serialising correctly" + + +@pytest.mark.asyncio +@pytest.mark.parametrize("algorithm", [ALGORITHM_FIXED_WINDOW, ALGORITHM_SLIDING_WINDOW, ALGORITHM_TOKEN_BUCKET]) +async def test_concurrent_stress_all_algorithms_do_not_over_allow(algorithm: str): + """ + 100 concurrent tasks against a limit of 15/s, run for each algorithm. + + Each algorithm must allow at most 15 requests. Sliding window and token + bucket may allow fewer due to their stricter enforcement; none may allow + more. This confirms the asyncio.Lock path holds regardless of which + algorithm is selected. + """ + plugin = _mk("15/s", algorithm=algorithm) + ctx = PluginContext(global_context=GlobalContext(request_id="algo-stress", user="bob")) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + results = await asyncio.gather(*[plugin.tool_pre_invoke(payload, ctx) for _ in range(100)]) + + allowed = sum(1 for r in results if r.violation is None) + assert allowed <= 15, f"[{algorithm}] Over-allowed: {allowed} > 15 — algorithm may not be thread-safe" + + +@pytest.mark.asyncio +async def test_concurrent_stress_window_boundary_total_does_not_exceed_double_limit(): + """ + Fixed window burst-at-boundary under concurrent load. + + 50 tasks fire before the window resets, 50 fire after. The documented + worst case for fixed_window is 2× the limit (N requests at end of W1 + + N at start of W2). Under concurrent asyncio load the total allowed must + never exceed 2× the limit — if it does, the lock is broken. + + Note: sliding_window and token_bucket are not subject to this bound; + this test is intentionally fixed_window only. + """ + limit = 10 + plugin = _mk(f"{limit}/s") + ctx = PluginContext(global_context=GlobalContext(request_id="boundary", user="carol")) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + # First burst — within the current window + first_wave = await asyncio.gather(*[plugin.tool_pre_invoke(payload, ctx) for _ in range(50)]) + + # Advance time past the window boundary + backend = plugin._rate_backend + if isinstance(backend, MemoryBackend) and hasattr(backend._algorithm, "_store"): + backend._algorithm._store.clear() + + # Second burst — new window + second_wave = await asyncio.gather(*[plugin.tool_pre_invoke(payload, ctx) for _ in range(50)]) + + total_allowed = sum(1 for r in first_wave + second_wave if r.violation is None) + assert total_allowed <= 2 * limit, f"Total allowed {total_allowed} exceeds 2× limit ({2 * limit}) — " f"fixed_window boundary burst is worse than documented" + + +# ============================================================================ +# Sweep Task Lifecycle Tests +# ============================================================================ + + +@pytest.mark.asyncio +async def test_sweep_evicts_expired_fixed_window_keys(): + """ + After a fixed-window expires, the sweep task removes the key from the store. + + We exhaust the limit, then manually back-date the window start so the sweep + sees the window as expired, run one sweep cycle, and confirm the store is + empty. A subsequent request must be allowed again (fresh window). + """ + backend = MemoryBackend(FixedWindowAlgorithm(), sweep_interval=999) + # Exhaust a 1/s limit + await backend.allow("user:dave", "1/s") + await backend.allow("user:dave", "1/s") + + assert len(backend._algorithm._store) == 1 + + # Back-date the window start by 2 seconds so sweep sees it as expired + for wnd in backend._algorithm._store.values(): + wnd.window_start -= 2 + + await backend._algorithm.sweep(backend._lock) + + assert len(backend._algorithm._store) == 0, "Expired window key was not evicted by sweep" + + # A fresh request should be allowed now + allowed, *_ = await backend.allow("user:dave", "1/s") + assert allowed is True, "Request after sweep eviction should start a fresh window" + + +@pytest.mark.asyncio +async def test_sweep_task_restarts_after_cancellation(): + """ + If the background sweep task is cancelled (e.g. during a test teardown or + event loop churn), the next call to allow() must recreate it via + _ensure_sweep_task(). + """ + backend = MemoryBackend(FixedWindowAlgorithm(), sweep_interval=999) + + # Trigger task creation + await backend.allow("user:eve", "5/s") + task = backend._sweep_task + assert task is not None and not task.done() + + # Cancel the task — simulates teardown or loop restart + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + + assert backend._sweep_task.done() + + # Next allow() call must recreate the sweep task + await backend.allow("user:eve", "5/s") + assert backend._sweep_task is not None + assert not backend._sweep_task.done(), "Sweep task was not recreated after cancellation" + + +@pytest.mark.asyncio +async def test_sweep_does_not_evict_active_keys(): + """ + Keys with recent activity must survive a sweep cycle. + + We make a request (creating a live window), run the sweep immediately + without back-dating the window, and confirm the key is still present. + """ + backend = MemoryBackend(FixedWindowAlgorithm(), sweep_interval=999) + await backend.allow("user:frank", "10/s") + + assert len(backend._algorithm._store) == 1 + + # Run sweep — window is fresh, should NOT be evicted + await backend._algorithm.sweep(backend._lock) + + assert len(backend._algorithm._store) == 1, "Active window key was incorrectly evicted by sweep" + + +# ============================================================================ +# Clock / Timing Edge Case Tests +# ============================================================================ + + +@pytest.mark.asyncio +async def test_token_bucket_caps_at_capacity_after_long_inactivity(): + """ + A token bucket that has been inactive for a very long time must not + accumulate more tokens than its capacity. + + Without a cap, `tokens = min(count, tokens + elapsed * refill_rate)` + would overflow. This test back-dates last_refill by 24 hours and confirms + the bucket holds exactly `count` tokens — not more. + """ + algorithm = TokenBucketAlgorithm() + lock = asyncio.Lock() + + # First request — creates the bucket with count-1 tokens + await algorithm.allow(lock, "user:grace", 10, 60) + + # Back-date last_refill by 24 hours to simulate long inactivity + bucket = algorithm._store["user:grace"] + bucket.last_refill -= 86400 + + # Next request should be allowed and tokens must not exceed capacity (10) + allowed, limit, _, meta = await algorithm.allow(lock, "user:grace", 10, 60) + assert allowed is True + assert meta["remaining"] <= limit, f"Token bucket overflowed: remaining={meta['remaining']} > limit={limit}" + + +@pytest.mark.asyncio +async def test_fixed_window_resets_after_window_duration_elapses(): + """ + Once a fixed window's duration has elapsed, the next request must open a + fresh window and be allowed — even if the limit was previously exhausted. + + We exhaust a 2/s limit, then advance the window start backward by 2 seconds + (simulating time passing), and confirm the next request is allowed. + """ + algorithm = FixedWindowAlgorithm() + lock = asyncio.Lock() + + await algorithm.allow(lock, "user:henry", 2, 1) + await algorithm.allow(lock, "user:henry", 2, 1) + blocked, *_ = await algorithm.allow(lock, "user:henry", 2, 1) + assert blocked is False, "Limit should be exhausted at this point" + + # Simulate 2 seconds passing by back-dating the window start + for wnd in algorithm._store.values(): + wnd.window_start -= 2 + + allowed, *_ = await algorithm.allow(lock, "user:henry", 2, 1) + assert allowed is True, "Request after window expiry should open a fresh window and be allowed" + + +@pytest.mark.asyncio +async def test_sliding_window_enforces_correctly_with_duplicate_timestamps(): + """ + When multiple requests arrive within the same millisecond, time.time() + may return identical float values. The sliding window must still enforce + the limit correctly — duplicate timestamps must each count as a distinct + request. + """ + algorithm = SlidingWindowAlgorithm() + lock = asyncio.Lock() + fixed_time = time.time() + + with patch("plugins.rate_limiter.rate_limiter.time") as mock_time: + mock_time.time.return_value = fixed_time + + # Send limit+1 requests all at the same timestamp + limit = 3 + results = [] + for _ in range(limit + 1): + result = await algorithm.allow(lock, "user:iris", limit, 60) + results.append(result) + + allowed = sum(1 for r, *_ in results if r is True) + assert allowed == limit, f"Expected exactly {limit} allowed with duplicate timestamps, got {allowed}" + + +# ============================================================================ +# Redis Error Mode Tests +# ============================================================================ + + +@pytest.mark.asyncio +async def test_redis_timeout_falls_back_to_memory(): + """ + A transient TimeoutError from the Redis client must trigger the memory + fallback when redis_fallback=True. The request must be allowed — a Redis + timeout must never silently block traffic. + """ + # Standard + from unittest.mock import AsyncMock # noqa: PLC0415 + + mock_client = AsyncMock() + mock_client.eval.side_effect = TimeoutError("Redis timed out") + + fallback = MemoryBackend(FixedWindowAlgorithm()) + backend = RedisBackend( + redis_url="redis://localhost:6379/0", + algorithm_name=ALGORITHM_FIXED_WINDOW, + fallback=fallback, + _client=mock_client, + ) + + allowed, *_ = await backend.allow("user:jack", "5/s") + assert allowed is True, "Transient Redis timeout must fall back to memory and allow the request" + + +@pytest.mark.asyncio +async def test_redis_lua_script_error_fails_open_without_fallback(): + """ + If the Redis Lua script raises a ResponseError (e.g. after a Redis restart + that flushed cached scripts), and no fallback is configured, the request + must be allowed — fail-open is the documented behaviour when + redis_fallback=False. + """ + # Standard + from unittest.mock import AsyncMock # noqa: PLC0415 + + try: + # Third-Party + from redis.exceptions import ResponseError # noqa: PLC0415 + except ImportError: + pytest.skip("redis package not installed") + + mock_client = AsyncMock() + mock_client.eval.side_effect = ResponseError("NOSCRIPT No matching script") + + backend = RedisBackend( + redis_url="redis://localhost:6379/0", + algorithm_name=ALGORITHM_FIXED_WINDOW, + fallback=None, + _client=mock_client, + ) + + allowed, *_ = await backend.allow("user:kate", "5/s") + assert allowed is True, "Lua script error without fallback must fail open (allow request)" + + +@pytest.mark.asyncio +async def test_redis_fallback_and_redis_counters_are_independent(): + """ + When Redis is down, the memory fallback tracks its own counter. When Redis + recovers, the Redis counter starts fresh — the fallback counter must not + bleed into Redis or vice versa. + + We exhaust the fallback limit during the outage, then restore Redis and + confirm the first Redis-backed request is allowed (fresh Redis counter). + """ + # Standard + from unittest.mock import AsyncMock # noqa: PLC0415 + + mock_client = AsyncMock() + + # Phase 1: Redis is down — all calls go to fallback + mock_client.eval.side_effect = ConnectionError("Redis down") + fallback = MemoryBackend(FixedWindowAlgorithm()) + backend = RedisBackend( + redis_url="redis://localhost:6379/0", + algorithm_name=ALGORITHM_FIXED_WINDOW, + fallback=fallback, + _client=mock_client, + ) + + # Exhaust the fallback limit (2/s) + await backend.allow("user:leo", "2/s") + await backend.allow("user:leo", "2/s") + fallback_blocked, *_ = await backend.allow("user:leo", "2/s") + assert fallback_blocked is False, "Fallback must enforce limit during Redis outage" + + # Phase 2: Redis recovers — return a valid fixed-window result ([1, 60]) + mock_client.eval.side_effect = None + mock_client.eval.return_value = [1, 60] # count=1, ttl=60 → fresh window + + redis_allowed, *_ = await backend.allow("user:leo", "2/s") + assert redis_allowed is True, "Redis counter must start fresh after recovery — fallback state must not carry over" + + +# ============================================================================ +# Configuration Edge Case Tests +# ============================================================================ + + +@pytest.mark.asyncio +async def test_very_large_rate_limit_does_not_overflow(): + """ + A rate limit of 1,000,000/min must initialise without error and correctly + allow the first request. This guards against integer overflow in the counter + or remaining calculation. + """ + plugin = _mk("1000000/m") + ctx = PluginContext(global_context=GlobalContext(request_id="large", user="user-large")) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + result = await plugin.tool_pre_invoke(payload, ctx) + assert result.violation is None, "First request under a very large limit must be allowed" + + headers = result.http_headers or {} + remaining = int(headers.get("X-RateLimit-Remaining", -1)) + assert remaining == 999999, f"Remaining should be limit-1=999999, got {remaining}" + + +@pytest.mark.asyncio +async def test_very_small_rate_limit_allows_first_request(): + """ + A rate limit of 1/hour must allow the first request and block the second. + + This exercises the token bucket and fixed window at an extremely low refill + rate (1/3600 tokens per second) — floating-point precision must not cause + the first request to be incorrectly blocked. + """ + for algorithm in [ALGORITHM_FIXED_WINDOW, ALGORITHM_SLIDING_WINDOW, ALGORITHM_TOKEN_BUCKET]: + plugin = _mk("1/h", algorithm=algorithm) + ctx = PluginContext(global_context=GlobalContext(request_id="small", user=f"user-small-{algorithm}")) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + first = await plugin.tool_pre_invoke(payload, ctx) + assert first.violation is None, f"[{algorithm}] First request under 1/h limit must be allowed" + + second = await plugin.tool_pre_invoke(payload, ctx) + assert second.violation is not None, f"[{algorithm}] Second request under 1/h limit must be blocked" + + +def test_by_tool_with_special_character_tool_names(): + """ + Tool names containing spaces, slashes, and unicode characters must be + accepted by _validate_config and stored as-is. The rate limiter must + match by exact key — no normalisation or stripping. + """ + plugin = RateLimiterPlugin( + PluginConfig( + name="rl", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=[ToolHookType.TOOL_PRE_INVOKE], + config={ + "by_tool": { + "my tool/v2": "5/m", + "outil-résumé": "10/m", + "工具": "3/m", + } + }, + ) + ) + # Config must be accepted without errors + assert plugin._cfg.by_tool is not None + assert "my tool/v2" in plugin._cfg.by_tool + assert "outil-résumé" in plugin._cfg.by_tool + assert "工具" in plugin._cfg.by_tool + + +# ============================================================================ +# P0 Unit Tests — Redis/Memory Correctness +# ============================================================================ + + +@pytest.mark.asyncio +async def test_redis_sliding_window_counts_multiple_requests_with_same_timestamp(): + """ + The fixed sliding window Lua script uses a unique member per request + (ARGV[4] = uuid), so concurrent requests at the same timestamp each occupy + their own sorted-set slot. Three requests at an identical timestamp against + a limit of 2/s: first two allowed, third blocked. + """ + # Standard + from unittest.mock import AsyncMock # noqa: PLC0415 + + fixed_ts = 1_700_000_000.0 + + # Simulate the FIXED Lua behaviour: unique member per request, check before ZADD + store: dict[str, dict] = {} + + async def fake_eval(script, numkeys, key, *args): + if "ZREMRANGEBYSCORE" in script: + now = float(args[0]) + window = float(args[1]) + limit_val = int(args[2]) + member = str(args[3]) # unique member (uuid hex from _allow_sliding) + cutoff = now - window + if key not in store: + store[key] = {} + store[key] = {m: s for m, s in store[key].items() if s > cutoff} + count = len(store[key]) + oldest_ts = min(store[key].values()) if store[key] else 0 + if count >= limit_val: + return [0, count, oldest_ts] # [allowed=0, count, oldest_ts] + store[key][member] = now + count += 1 + oldest_ts = min(store[key].values()) if store[key] else 0 + return [1, count, oldest_ts] # [allowed=1, count, oldest_ts] + return [0, 0, 0] + + mock_client = AsyncMock() + mock_client.eval.side_effect = fake_eval + + backend = RedisBackend( + redis_url="redis://localhost:6379/0", + algorithm_name=ALGORITHM_SLIDING_WINDOW, + fallback=None, + _client=mock_client, + ) + + limit = "2/s" + with patch("plugins.rate_limiter.rate_limiter.time") as mock_time: + mock_time.time.return_value = fixed_ts + r1, *_ = await backend.allow("user:test", limit) + r2, *_ = await backend.allow("user:test", limit) + r3, *_ = await backend.allow("user:test", limit) + + assert r1 is True + assert r2 is True + assert r3 is False, "Third request at same timestamp must be blocked — " "each request now occupies its own sorted-set slot via unique member" + + +@pytest.mark.asyncio +async def test_sliding_window_memory_evicts_idle_keys_after_window_expires(): + """ + When a sliding window key has no activity for longer than the window duration, + the next allow() call must naturally evict stale timestamps and treat the key + as fresh — allowing requests up to the full limit again. + + This tests the natural eviction path via allow() itself, not the sweep task. + """ + algorithm = SlidingWindowAlgorithm() + lock = asyncio.Lock() + now = time.time() + + with patch("plugins.rate_limiter.rate_limiter.time") as mock_time: + # Exhaust the limit now + mock_time.time.return_value = now + await algorithm.allow(lock, "user:test", 2, 1) + await algorithm.allow(lock, "user:test", 2, 1) + blocked, *_ = await algorithm.allow(lock, "user:test", 2, 1) + assert blocked is False + + # Advance time past the window — all previous timestamps are now stale + mock_time.time.return_value = now + 2.0 + + # Next call must see an empty window and allow the request + allowed, *_ = await algorithm.allow(lock, "user:test", 2, 1) + assert allowed is True, "After window expires, allow() must evict stale timestamps and allow fresh requests" + + +@pytest.mark.asyncio +async def test_memory_and_redis_sliding_window_have_same_allow_block_sequence(): + """ + Memory backend and Redis backend must produce identical allow/block decisions + for the same request timeline. This parity test uses an in-process Redis + simulator that faithfully implements the fixed sliding window Lua script logic: + unique member per request (ARGV[4]) and count check before ZADD. + """ + # Standard + from unittest.mock import AsyncMock # noqa: PLC0415 + + # In-process Redis simulator for the FIXED sliding window Lua script + sim_store: dict[str, dict] = {} + + async def sliding_sim(script, numkeys, key, *args): + now = float(args[0]) + window = float(args[1]) + limit_val = int(args[2]) + member = str(args[3]) # unique member from _allow_sliding + cutoff = now - window + if key not in sim_store: + sim_store[key] = {} + sim_store[key] = {m: s for m, s in sim_store[key].items() if s > cutoff} + count = len(sim_store[key]) + oldest_ts = min(sim_store[key].values()) if sim_store[key] else now + # Check before ZADD — blocked requests do NOT inflate the set + if count >= limit_val: + return [0, count, oldest_ts] # [allowed=0, count, oldest_ts] + sim_store[key][member] = now + count += 1 + oldest_ts = min(sim_store[key].values()) if sim_store[key] else now + return [1, count, oldest_ts] # [allowed=1, count, oldest_ts] + + mock_client = AsyncMock() + mock_client.eval.side_effect = sliding_sim + + redis_backend = RedisBackend( + redis_url="redis://localhost:6379/0", + algorithm_name=ALGORITHM_SLIDING_WINDOW, + fallback=None, + _client=mock_client, + ) + memory_backend = MemoryBackend(SlidingWindowAlgorithm()) + + limit = "3/s" + base = time.time() + offsets = [0.0, 0.1, 0.2, 0.5, 0.8, 1.1, 1.2, 1.5] + + redis_decisions = [] + memory_decisions = [] + + for offset in offsets: + t = base + offset + with patch("plugins.rate_limiter.rate_limiter.time") as mock_time: + mock_time.time.return_value = t + r_allowed, *_ = await redis_backend.allow("user:test", limit) + m_allowed, *_ = await memory_backend.allow("user:test", limit) + redis_decisions.append(r_allowed) + memory_decisions.append(m_allowed) + + assert redis_decisions == memory_decisions, f"Memory and Redis sliding window diverged:\n" f" Redis: {redis_decisions}\n" f" Memory: {memory_decisions}" + + +@pytest.mark.asyncio +async def test_memory_and_redis_token_bucket_have_same_allow_block_sequence(): + """ + Memory backend and Redis backend must produce identical allow/block decisions + for the token bucket algorithm across a fixed request timeline. + Uses an in-process simulator of the token bucket Lua script. + """ + # Standard + from unittest.mock import AsyncMock # noqa: PLC0415 + + # In-process Redis simulator for token bucket + sim_bucket: dict[str, dict] = {} + + async def token_bucket_sim(script, numkeys, key, *args): + capacity = float(args[0]) + rate = float(args[1]) + now = float(args[2]) + + if key not in sim_bucket: + tokens = capacity - 1 + sim_bucket[key] = {"tokens": tokens, "last_refill": now} + return [1, int(tokens), 0] + + b = sim_bucket[key] + elapsed = now - b["last_refill"] + tokens = min(capacity, b["tokens"] + elapsed * rate) + + if tokens >= 1.0: + tokens -= 1.0 + allowed = 1 + time_to_next = 0 + else: + allowed = 0 + time_to_next = int((1.0 - tokens) / rate) + 1 + + sim_bucket[key] = {"tokens": tokens, "last_refill": now} + return [allowed, int(tokens), time_to_next] + + mock_client = AsyncMock() + mock_client.eval.side_effect = token_bucket_sim + + redis_backend = RedisBackend( + redis_url="redis://localhost:6379/0", + algorithm_name=ALGORITHM_TOKEN_BUCKET, + fallback=None, + _client=mock_client, + ) + memory_backend = MemoryBackend(TokenBucketAlgorithm()) + + limit = "3/s" + base = time.time() + offsets = [0.0, 0.1, 0.2, 0.4, 0.8, 1.0, 1.2, 1.6, 2.0] + + redis_decisions = [] + memory_decisions = [] + + for offset in offsets: + t = base + offset + with patch("plugins.rate_limiter.rate_limiter.time") as mock_time: + mock_time.time.return_value = t + r_allowed, *_ = await redis_backend.allow("user:test", limit) + m_allowed, *_ = await memory_backend.allow("user:test", limit) + redis_decisions.append(r_allowed) + memory_decisions.append(m_allowed) + + assert redis_decisions == memory_decisions, f"Memory and Redis token bucket diverged:\n" f" Redis: {redis_decisions}\n" f" Memory: {memory_decisions}" + + +# --------------------------------------------------------------------------- +# P1 Unit Tests — header consistency and correctness +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_token_bucket_success_headers_are_consistent_between_memory_and_redis(): + """ + For allowed token bucket requests, both memory and Redis backends must + produce the same X-RateLimit-Remaining value and X-RateLimit-Limit == configured limit. + """ + # Standard + from unittest.mock import AsyncMock # noqa: PLC0415 + + sim_bucket: dict[str, dict] = {} + + async def token_bucket_sim(script, numkeys, key, *args): + capacity = float(args[0]) + rate = float(args[1]) + now = float(args[2]) + if key not in sim_bucket: + tokens = capacity - 1 + sim_bucket[key] = {"tokens": tokens, "last_refill": now} + return [1, int(tokens), 0] + b = sim_bucket[key] + elapsed = now - b["last_refill"] + tokens = min(capacity, b["tokens"] + elapsed * rate) + if tokens >= 1.0: + tokens -= 1.0 + allowed = 1 + time_to_next = 0 + else: + allowed = 0 + time_to_next = int((1.0 - tokens) / rate) + 1 + sim_bucket[key] = {"tokens": tokens, "last_refill": now} + return [allowed, int(tokens), time_to_next] + + mock_client = AsyncMock() + mock_client.eval.side_effect = token_bucket_sim + + limit = "5/s" + t0 = time.time() + + memory_backend = MemoryBackend(TokenBucketAlgorithm()) + redis_backend = RedisBackend( + redis_url="redis://localhost:6379/0", + algorithm_name=ALGORITHM_TOKEN_BUCKET, + fallback=None, + _client=mock_client, + ) + + with patch("plugins.rate_limiter.rate_limiter.time") as mock_time: + mock_time.time.return_value = t0 + m_allowed, m_limit, m_reset, m_meta = await memory_backend.allow("user:x", limit) + r_allowed, r_limit, r_reset, r_meta = await redis_backend.allow("user:x", limit) + + assert m_allowed is True + assert r_allowed is True + # Both must report the configured limit + assert m_limit == 5 + assert r_limit == 5 + # Remaining should be 4 (one token consumed from a full bucket of 5) + m_remaining = m_meta.get("remaining", 0) + r_remaining = r_meta.get("remaining", 0) + assert m_remaining == 4 + assert r_remaining == 4 + # Reset timestamp should be >= now + assert m_reset >= t0 + assert r_reset >= t0 + + +@pytest.mark.asyncio +async def test_token_bucket_memory_reset_timestamp_always_in_future(): + """Token bucket memory backend must never produce a past/present reset timestamp. + + When tokens_needed / refill_rate < 1, int() truncates to 0, placing + reset_timestamp at now rather than in the future. max(1, ...) guards + against this — mirroring the same protection already present in the + Redis path. + + Regression test: with limit="3/s", after consuming 1 token from a full + bucket, tokens_needed=1 and refill_rate=3, so 1/3 ≈ 0.33 → int() = 0 + without the fix. + """ + backend = MemoryBackend(TokenBucketAlgorithm()) + t0 = 1_000_000.0 + + with patch("plugins.rate_limiter.rate_limiter.time") as mock_time: + mock_time.time.return_value = t0 + allowed, _, reset_timestamp, _ = await backend.allow("user:test", "3/s") + + assert allowed is True + assert reset_timestamp > t0, ( + f"reset_timestamp ({reset_timestamp}) must be strictly greater than now ({t0}). " "int(tokens_needed / refill_rate) rounds to 0 for fast refill rates without max(1, ...)." + ) + + +@pytest.mark.asyncio +async def test_sliding_window_reset_header_tracks_oldest_request_expiry(): + """ + For sliding_window, X-RateLimit-Reset must equal the timestamp of the + oldest request in the current window plus the window duration — i.e. + when that request ages out and a new slot opens. + + Forces the Python fallback path because the test relies on mocking + time.time() to control both now_unix and internal rate-math timing. + The Rust engine's monotonic clock is not affected by Python time mocks, + so real elapsed time between requests causes the nanos-to-seconds + integer division to diverge from the mocked expectations. + """ + with patch("plugins.rate_limiter.rate_limiter._RUST_AVAILABLE", False): + plugin = _mk("3/s", ALGORITHM_SLIDING_WINDOW) + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="u1")) + payload = ToolPreInvokePayload(name="t", arguments={}) + t0 = 1_000_000.0 + + # First request at t0 + with patch("plugins.rate_limiter.rate_limiter.time") as mt: + mt.time.return_value = t0 + r1 = await plugin.tool_pre_invoke(payload, ctx) + assert r1.violation is None + reset_after_first = (r1.http_headers or {}).get("X-RateLimit-Reset") + assert reset_after_first is not None + # Reset should be t0 + 1s (window = 1s, oldest entry = t0) + assert float(reset_after_first) == pytest.approx(t0 + 1.0, abs=0.1) + + # Second request at t0 + 0.3s — oldest is still t0 + with patch("plugins.rate_limiter.rate_limiter.time") as mt: + mt.time.return_value = t0 + 0.3 + r2 = await plugin.tool_pre_invoke(payload, ctx) + assert r2.violation is None + reset_after_second = (r2.http_headers or {}).get("X-RateLimit-Reset") + # Reset still anchored to t0 (oldest request) + assert float(reset_after_second) == pytest.approx(t0 + 1.0, abs=0.1) + + +@pytest.mark.asyncio +async def test_token_bucket_retry_after_matches_time_to_next_token(): + """ + When a token bucket request is blocked, Retry-After must be > 0 and + reflect the time until the next token is available (roughly 1/rate seconds). + """ + plugin = _mk("2/s", ALGORITHM_TOKEN_BUCKET) + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="u1")) + payload = ToolPreInvokePayload(name="t", arguments={}) + t0 = 1_000_000.0 + + # Exhaust both tokens + with patch("plugins.rate_limiter.rate_limiter.time") as mt: + mt.time.return_value = t0 + r1 = await plugin.tool_pre_invoke(payload, ctx) + r2 = await plugin.tool_pre_invoke(payload, ctx) + assert r1.violation is None + assert r2.violation is None + + # Third request at same instant — bucket empty + with patch("plugins.rate_limiter.rate_limiter.time") as mt: + mt.time.return_value = t0 + r3 = await plugin.tool_pre_invoke(payload, ctx) + assert r3.violation is not None + retry_after = (r3.violation.http_headers or {}).get("Retry-After") + assert retry_after is not None + retry_secs = int(retry_after) + # With rate 2/s, one token refills in 0.5s — Retry-After should be 1s (integer ceiling) + assert 1 <= retry_secs <= 2 + + +@pytest.mark.asyncio +@pytest.mark.parametrize("algorithm", [ALGORITHM_FIXED_WINDOW, ALGORITHM_SLIDING_WINDOW, ALGORITHM_TOKEN_BUCKET]) +async def test_remaining_header_never_goes_negative_for_any_algorithm(algorithm: str): + """ + X-RateLimit-Remaining must never be negative, regardless of algorithm, + even when requests arrive after the limit is exhausted. + """ + plugin = _mk("2/s", algorithm) + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="u1")) + payload = ToolPreInvokePayload(name="t", arguments={}) + t0 = 1_000_000.0 + + for _ in range(5): # send 5 requests against a limit of 2 + with patch("plugins.rate_limiter.rate_limiter.time") as mt: + mt.time.return_value = t0 + result = await plugin.tool_pre_invoke(payload, ctx) + # Headers are on result.http_headers for allowed requests, + # and on result.violation.http_headers for blocked requests. + if result.violation is not None: + headers = result.violation.http_headers or {} + else: + headers = result.http_headers or {} + remaining_str = headers.get("X-RateLimit-Remaining") + assert remaining_str is not None, "X-RateLimit-Remaining header must always be present" + remaining = int(remaining_str) + assert remaining >= 0, f"Remaining went negative ({remaining}) for algorithm={algorithm}" + + +# ============================================================================= +# P1 Tests — SlidingWindowAlgorithm sweep() correctness +# ============================================================================= + + +@pytest.mark.asyncio +async def test_sliding_window_sweep_evicts_keys_with_fully_stale_timestamps(): + """sweep() must remove keys whose entire timestamp list is outside the window. + + After a burst of activity, a key's timestamps age out over time. The + background sweep must remove such keys so memory does not grow without bound + in long-lived gateways with transient users. + + This is a regression test: the previous implementation only removed keys + with empty lists, leaving stale-but-non-empty entries alive indefinitely. + """ + algorithm = SlidingWindowAlgorithm() + lock = asyncio.Lock() + + with patch("plugins.rate_limiter.rate_limiter.time") as mock_time: + # t=0: user makes requests that fill the window + mock_time.time.return_value = 0.0 + await algorithm.allow(lock, "user:alice", 3, 1) + await algorithm.allow(lock, "user:alice", 3, 1) + + # Confirm the key is present in the store + assert any("user:alice" in k for k in algorithm._store), "Key must exist in store after allow() calls" + + # t=5: well past the 1-second window — all timestamps are stale + mock_time.time.return_value = 5.0 + await algorithm.sweep(lock) + + # sweep() must have evicted the key — no stale entry should remain + assert not any("user:alice" in k for k in algorithm._store), "sweep() must evict keys with fully stale timestamps, not just empty lists — " "idle users must not accumulate memory indefinitely" + + +@pytest.mark.asyncio +async def test_sliding_window_sweep_does_not_evict_active_keys(): + """sweep() must not remove keys that still have timestamps within the window.""" + algorithm = SlidingWindowAlgorithm() + lock = asyncio.Lock() + + with patch("plugins.rate_limiter.rate_limiter.time") as mock_time: + mock_time.time.return_value = 0.0 + await algorithm.allow(lock, "user:bob", 3, 60) # 60-second window + + # t=10: still well within the 60-second window + mock_time.time.return_value = 10.0 + await algorithm.sweep(lock) + + # Key must still be present — it has active timestamps + assert any("user:bob" in k for k in algorithm._store), "sweep() must not evict keys whose timestamps are still within the window" + + +@pytest.mark.asyncio +async def test_sliding_window_allow_after_sweep_starts_fresh(): + """After sweep() evicts a stale key, a subsequent allow() treats it as a new key. + + This validates that eviction and re-admission work together correctly: + a user who was rate-limited, goes idle (key swept), and returns should + start with a full quota — not inherit leftover state. + """ + algorithm = SlidingWindowAlgorithm() + lock = asyncio.Lock() + + with patch("plugins.rate_limiter.rate_limiter.time") as mock_time: + # Exhaust the limit at t=0 + mock_time.time.return_value = 0.0 + await algorithm.allow(lock, "user:carol", 2, 1) + await algorithm.allow(lock, "user:carol", 2, 1) + blocked, *_ = await algorithm.allow(lock, "user:carol", 2, 1) + assert blocked is False, "Third request must be blocked" + + # t=5: window expired — sweep evicts the stale key + mock_time.time.return_value = 5.0 + await algorithm.sweep(lock) + + # t=5: allow() must treat carol as a fresh key with full quota + allowed, *_ = await algorithm.allow(lock, "user:carol", 2, 1) + assert allowed is True, "After sweep() evicts the stale key, the next allow() must start fresh " "with a full quota — stale state must not persist" + + +# --------------------------------------------------------------------------- +# Rust engine architecture tests +# --------------------------------------------------------------------------- +# These tests assert the Python↔Rust seam properties required by the spec: +# ARCH-01 check()/check_async() called exactly once per hook invocation +# ARCH-03 Python wrapper contains no rate math (structural — the wrapper +# delegates to check() which returns (allowed, headers, meta)) +# ARCH-04 Rust engine error / exception → fail-open (request allowed) +# ARCH-05 _RUST_AVAILABLE = False path exercises the Python backend +# --------------------------------------------------------------------------- + + +def _mk_rust(rate: str, algorithm: str = ALGORITHM_FIXED_WINDOW) -> RateLimiterPlugin: + """Create a plugin instance that is guaranteed to use the Rust engine.""" + # First-Party + import plugins.rate_limiter.rate_limiter as _rl_mod + + with patch.object(_rl_mod, "_RUST_AVAILABLE", True): + # If the real Rust extension is not installed this will silently fall + # back to Python; the architecture tests skip in that case. + plugin = RateLimiterPlugin( + PluginConfig( + name="rl", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=[PromptHookType.PROMPT_PRE_FETCH, ToolHookType.TOOL_PRE_INVOKE], + config={"by_user": rate, "algorithm": algorithm}, + ) + ) + return plugin + + +# First-Party +import plugins.rate_limiter.rate_limiter as _rate_limiter_module # noqa: E402 + +_RUST_ENGINE_PRESENT = _rate_limiter_module._RUST_AVAILABLE +_skip_no_rust = pytest.mark.skipif(not _RUST_ENGINE_PRESENT, reason="Rust engine not installed") + + +@_skip_no_rust +@pytest.mark.asyncio +async def test_arch01_evaluate_many_called_once_per_tool_hook(): + """ARCH-01: Python wrapper makes exactly one check() call per hook. + + The seam between Python and Rust must be a single PyO3 call regardless of + how many active dimensions (user, tenant, tool) the request touches. + Multiple calls would compound the bridge-crossing overhead under concurrency. + """ + plugin = _mk_rust("10/s") + assert plugin._rust_engine is not None, "Rust engine must be active for this test" + + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice", tenant_id="acme")) + payload = ToolPreInvokePayload(name="test_tool", arguments={}) + + with patch.object(plugin._rust_engine, "check", wraps=plugin._rust_engine.check) as mock_check: + await plugin.tool_pre_invoke(payload, ctx) + assert mock_check.call_count == 1, f"check() must be called exactly once per hook invocation, got {mock_check.call_count}" + + +@_skip_no_rust +@pytest.mark.asyncio +async def test_arch01_evaluate_many_called_once_per_prompt_hook(): + """ARCH-01: Same single-call guarantee for prompt_pre_fetch via check().""" + plugin = _mk_rust("10/s") + assert plugin._rust_engine is not None + + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = PromptPrehookPayload(prompt_id="my_prompt") + + with patch.object(plugin._rust_engine, "check", wraps=plugin._rust_engine.check) as mock_check: + await plugin.prompt_pre_fetch(payload, ctx) + assert mock_check.call_count == 1 + + +@_skip_no_rust +@pytest.mark.asyncio +async def test_arch01_redis_rust_path_uses_async_entrypoint(): + """Redis-backed Rust path should await check_async exactly once.""" + # Standard + from unittest.mock import AsyncMock # noqa: PLC0415 + + plugin = RateLimiterPlugin( + PluginConfig( + name="rl", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=[ToolHookType.TOOL_PRE_INVOKE], + config={"by_user": "10/s", "backend": "redis", "redis_url": "redis://localhost:6379/0"}, + ) + ) + if plugin._rust_engine is None: + pytest.skip("Rust engine not active") + + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="search", arguments={}) + + sync_mock = patch.object(plugin._rust_engine, "check", wraps=plugin._rust_engine.check) + async_mock = patch.object(plugin._rust_engine, "check_async", AsyncMock(wraps=plugin._rust_engine.check_async)) + with sync_mock as mock_sync, async_mock as mock_async: + await plugin.tool_pre_invoke(payload, ctx) + assert mock_async.await_count == 1 + assert mock_sync.call_count == 0 + + +@_skip_no_rust +@pytest.mark.asyncio +async def test_arch01_memory_rust_path_keeps_sync_entrypoint(): + """Memory-backed Rust path should continue using the sync check entrypoint.""" + # Standard + from unittest.mock import AsyncMock # noqa: PLC0415 + + plugin = _mk_rust("10/s") + assert plugin._rust_engine is not None + + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="search", arguments={}) + + sync_mock = patch.object(plugin._rust_engine, "check", wraps=plugin._rust_engine.check) + async_mock = patch.object(plugin._rust_engine, "check_async", AsyncMock(wraps=plugin._rust_engine.check_async)) + with sync_mock as mock_sync, async_mock as mock_async: + await plugin.tool_pre_invoke(payload, ctx) + assert mock_sync.call_count == 1 + assert mock_async.await_count == 0 + + +@_skip_no_rust +@pytest.mark.asyncio +async def test_arch01_single_call_covers_all_active_dimensions(): + """ARCH-01: The single check() call receives all active dimensions. + + When user + tenant + tool are all configured, check() receives them as + separate arguments and builds the checks internally — not split across + multiple calls. + """ + plugin = RateLimiterPlugin( + PluginConfig( + name="rl", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=[ToolHookType.TOOL_PRE_INVOKE], + config={ + "by_user": "30/m", + "by_tenant": "300/m", + "by_tool": {"search": "10/m"}, + "algorithm": ALGORITHM_FIXED_WINDOW, + }, + ) + ) + if plugin._rust_engine is None: + pytest.skip("Rust engine not active") + + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice", tenant_id="acme")) + payload = ToolPreInvokePayload(name="search", arguments={}) + + with patch.object(plugin._rust_engine, "check", wraps=plugin._rust_engine.check) as mock_check: + await plugin.tool_pre_invoke(payload, ctx) + assert mock_check.call_count == 1 + # check() receives (user, tenant, tool, now_unix, include_retry_after) + args = mock_check.call_args[0] + assert args[0] == "alice", f"user must be passed; got {args[0]}" + assert args[1] == "acme", f"tenant must be passed; got {args[1]}" + assert args[2] == "search", f"tool must be passed; got {args[2]}" + + +@_skip_no_rust +@pytest.mark.asyncio +async def test_arch02_rust_tool_success_preserves_metadata_shape(): + """Rust fast path should preserve success metadata on the Python wrapper contract. + + The check() API returns (allowed, headers, meta) directly; the Python + wrapper passes meta through as-is. + """ + plugin = _mk_rust("10/s") + assert plugin._rust_engine is not None + + fake_meta = { + "limited": True, + "remaining": 7, + "reset_in": 60, + "dimensions": { + "allowed": [ + {"limited": True, "remaining": 9, "reset_in": 60}, + {"limited": True, "remaining": 7, "reset_in": 60}, + ] + }, + } + fake_headers = { + "X-RateLimit-Limit": "10", + "X-RateLimit-Remaining": "7", + "X-RateLimit-Reset": "1700000060", + "Retry-After": "0", + } + + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="search", arguments={}) + + with patch.object(plugin._rust_engine, "check", return_value=(True, fake_headers, fake_meta)): + result = await plugin.tool_pre_invoke(payload, ctx) + + assert result.violation is None + assert result.metadata == fake_meta + + +@_skip_no_rust +@pytest.mark.asyncio +async def test_arch02_rust_prompt_block_preserves_details_shape(): + """Rust fast path should preserve blocked details on the Python wrapper contract. + + The check() API returns (allowed, headers, meta) directly; on a block the + Python wrapper uses meta as violation.details. + """ + plugin = _mk_rust("1/s") + assert plugin._rust_engine is not None + + fake_meta = { + "limited": True, + "remaining": 0, + "reset_in": 30, + "dimensions": { + "violated": [{"limited": True, "remaining": 0, "reset_in": 30}], + "allowed": [{"limited": True, "remaining": 8, "reset_in": 60}], + }, + } + fake_headers = { + "X-RateLimit-Limit": "1", + "X-RateLimit-Remaining": "0", + "X-RateLimit-Reset": "1700000030", + "Retry-After": "30", + } + + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = PromptPrehookPayload(prompt_id="search") + + with patch.object(plugin._rust_engine, "check", return_value=(False, fake_headers, fake_meta)): + result = await plugin.prompt_pre_fetch(payload, ctx) + + assert result.violation is not None + assert result.violation.details == fake_meta + + +@_skip_no_rust +@pytest.mark.asyncio +async def test_arch04_rust_exception_is_fail_open(): + """ARCH-04: Rust engine exception → request is allowed (fail-open). + + The fail-open policy lives in Python, not Rust. If check() raises + any exception, the hook must return an allow result — never block the caller. + """ + plugin = _mk_rust("10/s") + if plugin._rust_engine is None: + pytest.skip("Rust engine not active") + + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="test_tool", arguments={}) + + with patch.object(plugin._rust_engine, "check", side_effect=RuntimeError("simulated Rust panic")): + result = await plugin.tool_pre_invoke(payload, ctx) + + assert result.violation is None, "A Rust engine exception must not block the request — fail-open policy " "requires the hook to allow through on any unexpected error" + assert result.continue_processing is True + + +@_skip_no_rust +@pytest.mark.asyncio +async def test_arch04_rust_exception_fail_open_prompt_hook(): + """ARCH-04: Same fail-open guarantee for prompt_pre_fetch via check().""" + plugin = _mk_rust("10/s") + if plugin._rust_engine is None: + pytest.skip("Rust engine not active") + + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = PromptPrehookPayload(prompt_id="my_prompt") + + with patch.object(plugin._rust_engine, "check", side_effect=RuntimeError("simulated Rust panic")): + result = await plugin.prompt_pre_fetch(payload, ctx) + + assert result.violation is None + assert result.continue_processing is True + + +@_skip_no_rust +@pytest.mark.asyncio +async def test_arch04_rust_redis_exception_uses_python_fallback_when_enabled(): + """Rust Redis runtime failure should honor redis_fallback=True via Python backend.""" + # Standard + from unittest.mock import AsyncMock # noqa: PLC0415 + + plugin = RateLimiterPlugin( + PluginConfig( + name="rl", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=[ToolHookType.TOOL_PRE_INVOKE], + config={"by_user": "2/s", "backend": "redis", "redis_url": "redis://localhost:6379/0", "redis_fallback": True}, + ) + ) + if plugin._rust_engine is None: + pytest.skip("Rust engine not active") + + class _BrokenRedis: + async def eval(self, *args: Any, **kwargs: Any) -> None: + raise ConnectionError("Redis is down") + + async def evalsha(self, *args: Any, **kwargs: Any) -> None: + raise ConnectionError("Redis is down") + + async def script_load(self, *args: Any, **kwargs: Any) -> None: + raise ConnectionError("Redis is down") + + plugin._rate_backend._client = _BrokenRedis() + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="test_tool", arguments={}) + + with patch.object( + plugin._rust_engine, + "check_async", + AsyncMock(side_effect=RuntimeError("simulated Rust panic")), + ): + r1 = await plugin.tool_pre_invoke(payload, ctx) + r2 = await plugin.tool_pre_invoke(payload, ctx) + r3 = await plugin.tool_pre_invoke(payload, ctx) + + assert r1.violation is None + assert r2.violation is None + assert r3.violation is not None, "Python fallback must still enforce the configured limit" + assert r3.violation.http_status_code == 429 + + +@_skip_no_rust +@pytest.mark.asyncio +async def test_arch04_rust_redis_exception_fail_open_when_fallback_disabled(): + """Rust Redis runtime failure should remain fail-open when redis_fallback=False.""" + # Standard + from unittest.mock import AsyncMock # noqa: PLC0415 + + plugin = RateLimiterPlugin( + PluginConfig( + name="rl", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=[ToolHookType.TOOL_PRE_INVOKE], + config={"by_user": "2/s", "backend": "redis", "redis_url": "redis://localhost:6379/0", "redis_fallback": False}, + ) + ) + if plugin._rust_engine is None: + pytest.skip("Rust engine not active") + + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="test_tool", arguments={}) + + with patch.object( + plugin._rust_engine, + "check_async", + AsyncMock(side_effect=RuntimeError("simulated Rust panic")), + ): + result = await plugin.tool_pre_invoke(payload, ctx) + + assert result.violation is None + assert result.continue_processing is True + + +@pytest.mark.asyncio +async def test_arch05_python_backend_used_when_rust_unavailable(): + """ARCH-05: When _RUST_AVAILABLE is False the Python MemoryBackend is used. + + The Rust engine is an acceleration path; Python memory backend must remain + fully functional as a drop-in fallback when the extension is not installed. + """ + # First-Party + import plugins.rate_limiter.rate_limiter as _rl_mod + + with patch.object(_rl_mod, "_RUST_AVAILABLE", False): + plugin = RateLimiterPlugin( + PluginConfig( + name="rl", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=[ToolHookType.TOOL_PRE_INVOKE], + config={"by_user": "3/s"}, + ) + ) + + assert plugin._rust_engine is None, "Python fallback must not activate Rust engine" + assert isinstance(plugin._rate_backend, MemoryBackend), "Python fallback must use MemoryBackend" + + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="tool", arguments={}) + + for _ in range(3): + r = await plugin.tool_pre_invoke(payload, ctx) + assert r.violation is None + + blocked = await plugin.tool_pre_invoke(payload, ctx) + assert blocked.violation is not None + assert blocked.violation.http_status_code == 429 + + +@_skip_no_rust +@pytest.mark.asyncio +async def test_arch05_rust_engine_active_when_available(): + """ARCH-05 complement: when Rust is available, engine is wired in for memory backend.""" + plugin = _mk_rust("10/s") + assert plugin._rust_engine is not None, "Rust engine must be active when _RUST_AVAILABLE=True and backend=memory" + + +@pytest.mark.asyncio +async def test_arch05_redis_backend_rust_owns_redis_when_available(): + """ARCH-06: When Rust is available and backend=redis, Rust owns the Redis connection. + + The Rust engine handles both memory and Redis backends. When _RUST_AVAILABLE=True + and backend=redis, _rust_engine is set and the Rust extension communicates with + Redis directly. The Python RedisBackend is still present for the Python fallback + path (when Rust is unavailable). + """ + # First-Party + import plugins.rate_limiter.rate_limiter as _rl_mod + + if not _rl_mod._RUST_AVAILABLE: + pytest.skip("Rust extension not available in this environment") + + plugin = RateLimiterPlugin( + PluginConfig( + name="rl", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=[ToolHookType.TOOL_PRE_INVOKE], + config={"by_user": "10/s", "backend": "redis", "redis_url": "redis://localhost:6379/0"}, + ) + ) + assert plugin._rust_engine is not None, "Rust engine must be active for Redis backend when Rust is available" + assert isinstance(plugin._rate_backend, RedisBackend) + + +# ============================================================================= +# Redis Batching Tests (REDIS-01, REDIS-03) +# +# REDIS-01: All dimension checks (user, tenant, tool) for a single hook +# invocation must be batched into exactly ONE Redis eval call. +# Current impl makes up to 3 sequential calls — these tests drive +# the implementation of allow_many() and a multi-dimension Lua script. +# +# REDIS-03: The single Lua script call accepts all active dimensions and +# returns all results in one reply. +# ============================================================================= + + +def _mk_redis_plugin(config: dict) -> RateLimiterPlugin: + """Create a Redis-backed plugin with a mock client injected. + + Forces _RUST_AVAILABLE=False so the Python RedisBackend path is exercised — + these tests verify Python-level batching semantics (REDIS-01/03). + The Rust+Redis path is validated by the load test. + """ + # Standard + from unittest.mock import AsyncMock # noqa: PLC0415 + + # First-Party + import plugins.rate_limiter.rate_limiter as _rl_mod # noqa: PLC0415 + + with patch.object(_rl_mod, "_RUST_AVAILABLE", False): + plugin = RateLimiterPlugin( + PluginConfig( + name="rl", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=[PromptHookType.PROMPT_PRE_FETCH, ToolHookType.TOOL_PRE_INVOKE], + config={"backend": "redis", "redis_url": "redis://localhost:6379/0", **config}, + ) + ) + mock_client = AsyncMock() + plugin._rate_backend._client = mock_client + return plugin + + +@pytest.mark.asyncio +async def test_redis01_single_eval_call_per_tool_hook_one_dimension(): + """REDIS-01: With only by_user configured, tool_pre_invoke makes exactly 1 eval call.""" + # Standard + from unittest.mock import AsyncMock # noqa: PLC0415 + + plugin = _mk_redis_plugin({"by_user": "10/s"}) + mock_client = plugin._rate_backend._client + mock_client.eval = AsyncMock(return_value=[1, 60]) # fixed window: [count, ttl] + + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="test_tool", arguments={}) + + await plugin.tool_pre_invoke(payload, ctx) + + assert mock_client.eval.call_count == 1, f"REDIS-01: expected exactly 1 eval call for 1 active dimension, " f"got {mock_client.eval.call_count}" + + +@pytest.mark.asyncio +async def test_redis01_single_eval_call_per_tool_hook_three_dimensions(): + """REDIS-01: With user + tenant + tool all configured, tool_pre_invoke must + still make exactly 1 eval call — all dimensions batched into one round-trip.""" + # Standard + from unittest.mock import AsyncMock # noqa: PLC0415 + + plugin = _mk_redis_plugin( + { + "by_user": "30/m", + "by_tenant": "300/m", + "by_tool": {"search": "10/m"}, + "algorithm": ALGORITHM_FIXED_WINDOW, + } + ) + mock_client = plugin._rate_backend._client + # Batched response: one result per dimension — [count, ttl] per dim + mock_client.eval = AsyncMock(return_value=[[1, 60], [1, 60], [1, 60]]) + + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice", tenant_id="acme")) + payload = ToolPreInvokePayload(name="search", arguments={}) + + await plugin.tool_pre_invoke(payload, ctx) + + assert mock_client.eval.call_count == 1, ( + f"REDIS-01: expected exactly 1 eval call for 3 active dimensions (user+tenant+tool), " f"got {mock_client.eval.call_count} — dimensions must be batched into one round-trip" + ) + + +@pytest.mark.asyncio +async def test_redis01_single_eval_call_per_prompt_hook(): + """REDIS-01: prompt_pre_fetch also makes exactly 1 eval call regardless of active dims.""" + # Standard + from unittest.mock import AsyncMock # noqa: PLC0415 + + plugin = _mk_redis_plugin( + { + "by_user": "10/s", + "by_tenant": "100/s", + "algorithm": ALGORITHM_FIXED_WINDOW, + } + ) + mock_client = plugin._rate_backend._client + mock_client.eval = AsyncMock(return_value=[[1, 60], [1, 60]]) + + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice", tenant_id="acme")) + payload = PromptPrehookPayload(prompt_id="my_prompt") + + await plugin.prompt_pre_fetch(payload, ctx) + + assert mock_client.eval.call_count == 1, f"REDIS-01: prompt_pre_fetch must batch all dimensions into 1 eval call, " f"got {mock_client.eval.call_count}" + + +@pytest.mark.asyncio +async def test_redis03_batched_script_returns_result_per_dimension(): + """REDIS-03: The single eval call must pass all active dimensions to the script + and receive back one result per dimension.""" + # Standard + from unittest.mock import AsyncMock # noqa: PLC0415 + + plugin = _mk_redis_plugin( + { + "by_user": "30/m", + "by_tenant": "300/m", + "by_tool": {"search": "10/m"}, + "algorithm": ALGORITHM_FIXED_WINDOW, + } + ) + mock_client = plugin._rate_backend._client + # Simulate all three dimensions allowed + mock_client.eval = AsyncMock(return_value=[[1, 60], [1, 60], [1, 60]]) + + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice", tenant_id="acme")) + payload = ToolPreInvokePayload(name="search", arguments={}) + + result = await plugin.tool_pre_invoke(payload, ctx) + + assert mock_client.eval.call_count == 1 + # The single call must have received all 3 dimension keys + call_args = mock_client.eval.call_args + # NUMKEYS should be 3 (one key per dimension) + numkeys = call_args[0][1] if call_args[0] else call_args[1].get("numkeys", 0) + assert numkeys == 3, f"REDIS-03: batched script must receive 3 keys (one per dimension), got {numkeys}" + assert result.violation is None + + +@pytest.mark.asyncio +async def test_redis03_batched_script_block_when_any_dimension_violated(): + """REDIS-03: If any dimension result is blocked, the hook must return 429.""" + # Standard + from unittest.mock import AsyncMock # noqa: PLC0415 + + plugin = _mk_redis_plugin( + { + "by_user": "30/m", + "by_tenant": "2/m", # tenant exhausted + "algorithm": ALGORITHM_FIXED_WINDOW, + } + ) + mock_client = plugin._rate_backend._client + # user: allowed, tenant: blocked + mock_client.eval = AsyncMock(return_value=[[1, 60], [3, 60]]) # count > limit for tenant + + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice", tenant_id="acme")) + payload = ToolPreInvokePayload(name="test_tool", arguments={}) + + result = await plugin.tool_pre_invoke(payload, ctx) + + assert mock_client.eval.call_count == 1 + assert result.violation is not None + assert result.violation.http_status_code == 429 + + +@pytest.mark.asyncio +async def test_redis01_no_eval_calls_when_no_limits_configured(): + """REDIS-01: When no dimensions are configured, no eval call is made.""" + # Standard + from unittest.mock import AsyncMock # noqa: PLC0415 + + plugin = _mk_redis_plugin({}) # no limits + mock_client = plugin._rate_backend._client + mock_client.eval = AsyncMock() + + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="test_tool", arguments={}) + + result = await plugin.tool_pre_invoke(payload, ctx) + + assert mock_client.eval.call_count == 0, "No eval calls expected when no limits are configured" + assert result.violation is None + + +# --------------------------------------------------------------------------- +# CORR-01: Rust and Python produce identical allow/block decisions +# --------------------------------------------------------------------------- +# +# Golden-file contract tests: for the same input sequence and the same +# algorithm, both engines must agree on every allow/block decision and on the +# remaining-token count. Time-dependent fields (reset_timestamp, retry_after) +# are not compared because the two engines use different clock sources. +# --------------------------------------------------------------------------- + + +def _python_sequence(algorithm: str, limit: int, n_requests: int) -> list[bool]: + """Run n_requests through the Python MemoryBackend; return allow decisions.""" + # First-Party + from plugins.rate_limiter.rate_limiter import ( # noqa: PLC0415 + FixedWindowAlgorithm, + MemoryBackend, + SlidingWindowAlgorithm, + TokenBucketAlgorithm, + ) + + algo_map = { + ALGORITHM_FIXED_WINDOW: FixedWindowAlgorithm, + ALGORITHM_SLIDING_WINDOW: SlidingWindowAlgorithm, + ALGORITHM_TOKEN_BUCKET: TokenBucketAlgorithm, + } + backend = MemoryBackend(algorithm=algo_map[algorithm]()) + rate_str = f"{limit}/h" # large window so it never resets during test + + async def _run(): + results = [] + for _ in range(n_requests): + allowed, *_ = await backend.allow("user:test", rate_str) + results.append(allowed) + return results + + return asyncio.run(_run()) + + +def _rust_sequence(algorithm: str, limit: int, n_requests: int) -> list[bool]: + """Run n_requests through the Rust RateLimiterEngine; return allow decisions.""" + # First-Party + from plugins.rate_limiter.rate_limiter import RustRateLimiterEngine # noqa: PLC0415 + + engine = RustRateLimiterEngine({"by_user": f"{limit}/h", "algorithm": algorithm}) + window_nanos = 3600 * 1_000_000_000 # 1 hour in nanos + now_unix = int(time.time()) + results = [] + for _ in range(n_requests): + r = engine.evaluate_many([("user:test", limit, window_nanos)], now_unix) + results.append(r.allowed) + return results + + +@_skip_no_rust +def test_corr01_fixed_window_parity(): + """CORR-01: Rust fixed_window allow/block sequence matches Python.""" + limit = 5 + n = 8 # 5 allowed + 3 blocked + py = _python_sequence(ALGORITHM_FIXED_WINDOW, limit, n) + rs = _rust_sequence(ALGORITHM_FIXED_WINDOW, limit, n) + assert py == rs, f"Parity failure fixed_window: Python={py} Rust={rs}" + + +@_skip_no_rust +def test_corr01_token_bucket_parity(): + """CORR-01: Rust token_bucket allow/block sequence matches Python.""" + limit = 4 + n = 6 # 4 allowed + 2 blocked + py = _python_sequence(ALGORITHM_TOKEN_BUCKET, limit, n) + rs = _rust_sequence(ALGORITHM_TOKEN_BUCKET, limit, n) + assert py == rs, f"Parity failure token_bucket: Python={py} Rust={rs}" + + +@_skip_no_rust +def test_corr01_sliding_window_parity(): + """CORR-01: Rust sliding_window allow/block sequence matches Python.""" + limit = 3 + n = 5 # 3 allowed + 2 blocked + py = _python_sequence(ALGORITHM_SLIDING_WINDOW, limit, n) + rs = _rust_sequence(ALGORITHM_SLIDING_WINDOW, limit, n) + assert py == rs, f"Parity failure sliding_window: Python={py} Rust={rs}" + + +@_skip_no_rust +def test_corr01_remaining_count_parity_fixed_window(): + """CORR-01: remaining token count matches between Python and Rust (fixed_window).""" + # First-Party + from plugins.rate_limiter.rate_limiter import FixedWindowAlgorithm, MemoryBackend # noqa: PLC0415 + from plugins.rate_limiter.rate_limiter import RustRateLimiterEngine # noqa: PLC0415 + + limit = 10 + window_nanos = 3600 * 1_000_000_000 + now_unix = int(time.time()) + + py_backend = MemoryBackend(algorithm=FixedWindowAlgorithm()) + rust_engine = RustRateLimiterEngine({"by_user": f"{limit}/h", "algorithm": ALGORITHM_FIXED_WINDOW}) + + async def _py_remaining(n: int) -> int: + remaining = 0 + for _ in range(n): + _, _, _, meta = await py_backend.allow("user:test", f"{limit}/h") + remaining = meta.get("remaining", 0) + return remaining + + n_requests = 4 + py_remaining = asyncio.run(_py_remaining(n_requests)) + rs_result = None + for _ in range(n_requests): + rs_result = rust_engine.evaluate_many([("user:test", limit, window_nanos)], now_unix) + rs_remaining = rs_result.remaining + + assert py_remaining == rs_remaining, f"remaining mismatch after {n_requests} requests: Python={py_remaining} Rust={rs_remaining}" + + +@_skip_no_rust +@pytest.mark.parametrize("algorithm", [ALGORITHM_SLIDING_WINDOW, ALGORITHM_TOKEN_BUCKET]) +def test_corr01_remaining_count_parity_all_algorithms(algorithm): + """CORR-01: remaining count matches between Python and Rust for all algorithms.""" + # First-Party + from plugins.rate_limiter.rate_limiter import FixedWindowAlgorithm, MemoryBackend, RustRateLimiterEngine, SlidingWindowAlgorithm, TokenBucketAlgorithm # noqa: PLC0415 + + algo_map = { + ALGORITHM_FIXED_WINDOW: FixedWindowAlgorithm, + ALGORITHM_SLIDING_WINDOW: SlidingWindowAlgorithm, + ALGORITHM_TOKEN_BUCKET: TokenBucketAlgorithm, + } + limit = 10 + window_nanos = 3600 * 1_000_000_000 + now_unix = int(time.time()) + n_requests = 4 + + py_backend = MemoryBackend(algorithm=algo_map[algorithm]()) + rust_engine = RustRateLimiterEngine({"by_user": f"{limit}/h", "algorithm": algorithm}) + + async def _py_remaining() -> int: + remaining = 0 + for _ in range(n_requests): + _, _, _, meta = await py_backend.allow("user:test", f"{limit}/h") + remaining = meta.get("remaining", 0) + return remaining + + py_remaining = asyncio.run(_py_remaining()) + rs_result = None + for _ in range(n_requests): + rs_result = rust_engine.evaluate_many([("user:test", limit, window_nanos)], now_unix) + rs_remaining = rs_result.remaining + + assert py_remaining == rs_remaining, f"remaining mismatch ({algorithm}) after {n_requests} requests: Python={py_remaining} Rust={rs_remaining}" + + +@_skip_no_rust +def test_corr01_multi_dimension_parity(): + """CORR-01: Rust check() with 3 dimensions produces the same allow/block sequence as Python.""" + plugin_py = RateLimiterPlugin( + PluginConfig( + name="rl-parity-py", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=[ToolHookType.TOOL_PRE_INVOKE], + config={ + "by_user": "5/h", + "by_tenant": "10/h", + "by_tool": {"test_tool": "3/h"}, + "algorithm": ALGORITHM_FIXED_WINDOW, + }, + ) + ) + plugin_py._rust_engine = None # force Python path + + plugin_rs = RateLimiterPlugin( + PluginConfig( + name="rl-parity-rs", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=[ToolHookType.TOOL_PRE_INVOKE], + config={ + "by_user": "5/h", + "by_tenant": "10/h", + "by_tool": {"test_tool": "3/h"}, + "algorithm": ALGORITHM_FIXED_WINDOW, + }, + ) + ) + if plugin_rs._rust_engine is None: + pytest.skip("Rust engine not active") + + payload = ToolPreInvokePayload(name="test_tool", arguments={}) + py_sequence: list[bool] = [] + rs_sequence: list[bool] = [] + + async def _run(): + # Tool limit is 3/h — requests 4+ should be blocked by the tool dimension + for i in range(6): + ctx = PluginContext(global_context=GlobalContext(request_id=f"parity-{i}", user="alice@example.com", tenant_id="acme")) + py_result = await plugin_py.tool_pre_invoke(payload, ctx) + rs_result = await plugin_rs.tool_pre_invoke(payload, ctx) + py_sequence.append(py_result.continue_processing) + rs_sequence.append(rs_result.continue_processing) + + asyncio.run(_run()) + assert py_sequence == rs_sequence, f"Multi-dimension parity failure: Python={py_sequence} Rust={rs_sequence}" + # First 3 allowed (tool limit), then 3 blocked + assert py_sequence == [True, True, True, False, False, False] + + +# --------------------------------------------------------------------------- +# Redis key format parity — Python RedisBackend vs Rust engine key generation +# +# These tests guard the dual Lua-script invariant: Python and Rust must +# produce identical Redis keys so that mixed deployments share counters. +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + "dimension,key,rate,expected_suffix", + [ + ("user", "user:alice@example.com", "30/m", "user:alice@example.com:60"), + ("tenant", "tenant:acme", "3000/m", "tenant:acme:60"), + ("tool", "tool:my_tool", "10/s", "tool:my_tool:1"), + ("user", "user:bob", "100/h", "user:bob:3600"), + ], + ids=["user-per-minute", "tenant-per-minute", "tool-per-second", "user-per-hour"], +) +def test_redis_key_format_parity_python_backend(dimension, key, rate, expected_suffix): + """Python RedisBackend key format matches the documented pattern: {prefix}:{dim_key}:{window_seconds}.""" + count, window_seconds = _parse_rate(rate) + prefix = "rl" + redis_key = f"{prefix}:{key}:{window_seconds}" + assert redis_key == f"rl:{expected_suffix}" + + +@pytest.mark.parametrize( + "user,tenant,tool,by_user,by_tenant,by_tool_cfg,expected_keys", + [ + ( + "alice@example.com", + "acme", + "summarize", + "30/m", + "3000/m", + {"summarize": "10/m"}, + ["user:alice@example.com", "tenant:acme", "tool:summarize"], + ), + ( + "bob", + None, + "search", + "30/m", + "3000/m", + {}, + ["user:bob"], + ), + ], + ids=["three-dimensions", "user-only-no-tenant-no-tool"], +) +def test_redis_key_format_parity_rust_dimension_keys(user, tenant, tool, by_user, by_tenant, by_tool_cfg, expected_keys): + """Rust engine dimension keys (built by _build_rust_checks) match Python path dimension keys.""" + plugin = RateLimiterPlugin( + PluginConfig( + name="key-parity", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=[ToolHookType.TOOL_PRE_INVOKE], + config={ + "by_user": by_user, + "by_tenant": by_tenant, + "by_tool": by_tool_cfg, + "algorithm": ALGORITHM_FIXED_WINDOW, + }, + ) + ) + + # Rust path dimension keys (built by Python wrapper, consumed by Rust engine) + if plugin._rust_engine is not None: + rust_checks = plugin._build_rust_checks(user, tenant, tool) + rust_dim_keys = [key for key, _count, _window in rust_checks] + else: + # If Rust engine not built, manually replicate the key construction + # to verify the pattern is consistent. + rust_dim_keys = [] + if plugin._cfg.by_user: + rust_dim_keys.append(f"user:{user}") + if tenant and plugin._cfg.by_tenant: + rust_dim_keys.append(f"tenant:{tenant}") + normalised = {k.strip().lower(): v for k, v in (by_tool_cfg or {}).items()} + if tool in normalised: + rust_dim_keys.append(f"tool:{tool}") + + # Python path dimension keys (built inside _check_rate_limit) + python_dim_keys = [] + if plugin._cfg.by_user: + python_dim_keys.append(f"user:{user}") + if tenant and plugin._cfg.by_tenant: + python_dim_keys.append(f"tenant:{tenant}") + if plugin._normalised_by_tool and tool in plugin._normalised_by_tool: + python_dim_keys.append(f"tool:{tool}") + + assert rust_dim_keys == python_dim_keys, f"Dimension key mismatch: Rust={rust_dim_keys} Python={python_dim_keys}" + assert rust_dim_keys == expected_keys + + +def test_redis_key_format_parity_window_seconds(): + """Both paths derive identical window_seconds from the same rate string.""" + for rate, expected_window in [("10/s", 1), ("30/m", 60), ("100/h", 3600)]: + count, window_secs = _parse_rate(rate) + # Python RedisBackend uses window_seconds directly from _parse_rate + python_window = window_secs + # Rust engine receives window_nanos and divides back to seconds for the key + window_nanos = window_secs * 1_000_000_000 + rust_window = window_nanos // 1_000_000_000 + assert python_window == rust_window, f"Window mismatch for {rate}: Python={python_window} Rust={rust_window}" + assert python_window == expected_window + + +def _normalise_lua(script: str) -> str: + """Collapse whitespace in a Lua script for content-level comparison.""" + return " ".join(script.split()) + + +@pytest.mark.parametrize( + "py_attr,rust_const_name", + [ + ("_LUA_BATCH_FIXED", "LUA_BATCH_FIXED"), + ("_LUA_BATCH_SLIDING", "LUA_BATCH_SLIDING"), + ("_LUA_BATCH_TOKEN_BUCKET", "LUA_BATCH_TOKEN_BUCKET"), + ], + ids=["batch-fixed", "batch-sliding", "batch-token-bucket"], +) +def test_redis_lua_script_content_parity(py_attr, rust_const_name): + """Batch Lua scripts in Python RedisBackend and Rust redis_backend.rs must be functionally identical. + + This prevents silent divergence: the key-format parity tests verify key naming + but not the Lua logic that runs inside Redis. If a script is changed in one + implementation it must be changed in the other for rolling-upgrade safety. + """ + # Standard + import pathlib # noqa: PLC0415 + import re # noqa: PLC0415 + + py_script = getattr(RedisBackend, py_attr) + + rust_src = pathlib.Path(__file__).resolve().parents[6] / "plugins_rust" / "rate_limiter" / "src" / "redis_backend.rs" + if not rust_src.exists(): + pytest.skip(f"Rust source not found at {rust_src}") + + rust_content = rust_src.read_text() + + # Extract the Rust constant by finding `const {name}: &str = r#"...content..."#;` + pattern = rf'const {rust_const_name}:\s*&str\s*=\s*r#"(.*?)"#;' + match = re.search(pattern, rust_content, re.DOTALL) + assert match is not None, f"Could not find const {rust_const_name} in {rust_src}" + rust_script = match.group(1) + + assert _normalise_lua(py_script) == _normalise_lua(rust_script), ( + f"Lua script content mismatch between Python RedisBackend.{py_attr} and Rust {rust_const_name}. " "Both must stay in sync for rolling-upgrade compatibility." + ) + + +# --------------------------------------------------------------------------- +# REDIS-02: EVALSHA used after SCRIPT LOAD; EVAL only as NOSCRIPT fallback +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_redis02_evalsha_used_after_script_load(): + """REDIS-02: script_load called once at first use; evalsha used on request path.""" + # Standard + from unittest.mock import AsyncMock # noqa: PLC0415 + + mock_client = AsyncMock() + mock_client.script_load.return_value = "abc123sha" + mock_client.evalsha.return_value = [1, 60] # fixed window: count=1, ttl=60 + + backend = RedisBackend( + redis_url="redis://localhost:6379/0", + algorithm_name=ALGORITHM_FIXED_WINDOW, + _client=mock_client, + ) + + await backend.allow("user:alice", "10/s") + + # script_load must have been called (at least for _sha_fixed) + assert mock_client.script_load.called, "script_load must be called to cache SHA" + # evalsha must be used on the request path, not eval + assert mock_client.evalsha.called, "evalsha must be used after SHA is cached" + assert not mock_client.eval.called, "eval must NOT be called on the happy path" + + +@pytest.mark.asyncio +async def test_redis02_script_load_called_only_once_across_requests(): + """REDIS-02: script_load is called at most once — SHAs are cached after first load.""" + # Standard + from unittest.mock import AsyncMock # noqa: PLC0415 + + mock_client = AsyncMock() + mock_client.script_load.return_value = "deadbeef" + mock_client.evalsha.return_value = [1, 60] + + backend = RedisBackend( + redis_url="redis://localhost:6379/0", + algorithm_name=ALGORITHM_FIXED_WINDOW, + _client=mock_client, + ) + + for _ in range(5): + await backend.allow("user:alice", "10/s") + + # script_load call count should be equal to the number of scripts (6), + # not 5 × 6 — it only runs until all SHAs are populated. + load_count = mock_client.script_load.call_count + assert load_count <= 6, f"script_load should be called at most once per script, got {load_count} calls" + + +@pytest.mark.asyncio +async def test_redis02_noscript_fallback_to_eval(): + """REDIS-02: NOSCRIPT error causes fallback to EVAL and SHA reload.""" + # Standard + from unittest.mock import AsyncMock # noqa: PLC0415 + + # Third-Party + from redis.exceptions import ResponseError # noqa: PLC0415 + + mock_client = AsyncMock() + mock_client.script_load.return_value = "abc123" + # First evalsha raises NOSCRIPT; eval succeeds + mock_client.evalsha.side_effect = ResponseError("NOSCRIPT No matching script") + mock_client.eval.return_value = [1, 60] + + backend = RedisBackend( + redis_url="redis://localhost:6379/0", + algorithm_name=ALGORITHM_FIXED_WINDOW, + _client=mock_client, + ) + + result = await backend.allow("user:alice", "10/s") + allowed, *_ = result + + assert allowed is True, "NOSCRIPT fallback must still return a valid result" + assert mock_client.eval.called, "eval must be used as NOSCRIPT fallback" + + +# --------------------------------------------------------------------------- +# REDIS-04: Redis connection failure → fallback to MemoryBackend, no exception +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_redis04_connection_failure_falls_back_to_memory_allow(): + """REDIS-04: allow() falls back to MemoryBackend on Redis connection failure.""" + # First-Party + from plugins.rate_limiter.rate_limiter import FixedWindowAlgorithm # noqa: PLC0415 + + memory = MemoryBackend(algorithm=FixedWindowAlgorithm()) + backend = RedisBackend( + redis_url="redis://localhost:6379/0", + algorithm_name=ALGORITHM_FIXED_WINDOW, + fallback=memory, + ) + + # Inject a broken client — script_load raises immediately + class _Dead: + async def script_load(self, *a: Any, **kw: Any) -> None: + raise ConnectionError("Redis is down") + + async def eval(self, *a: Any, **kw: Any) -> None: + raise ConnectionError("Redis is down") + + async def evalsha(self, *a: Any, **kw: Any) -> None: + raise ConnectionError("Redis is down") + + backend._client = _Dead() + + allowed, *_ = await backend.allow("user:alice", "10/s") + assert allowed is True, "Connection failure + fallback must allow the request" + + +@pytest.mark.asyncio +async def test_redis04_connection_failure_no_fallback_allows_gracefully(): + """REDIS-04: allow() fails open (allow) when Redis is down and no fallback is configured.""" + backend = RedisBackend( + redis_url="redis://localhost:6379/0", + algorithm_name=ALGORITHM_FIXED_WINDOW, + fallback=None, + ) + + class _Dead: + async def script_load(self, *a: Any, **kw: Any) -> None: + raise ConnectionError("Redis is down") + + async def eval(self, *a: Any, **kw: Any) -> None: + raise ConnectionError("Redis is down") + + async def evalsha(self, *a: Any, **kw: Any) -> None: + raise ConnectionError("Redis is down") + + backend._client = _Dead() + + result = await backend.allow("user:alice", "10/s") + assert result is not None, "allow() must not raise on Redis failure" + allowed, *_ = result + assert allowed is True, "No-fallback path must fail open" + + +@pytest.mark.asyncio +async def test_redis04_allow_many_falls_back_to_memory_on_connection_failure(): + """REDIS-04: allow_many() falls back to per-call MemoryBackend when Redis is down.""" + # First-Party + from plugins.rate_limiter.rate_limiter import FixedWindowAlgorithm # noqa: PLC0415 + + memory = MemoryBackend(algorithm=FixedWindowAlgorithm()) + backend = RedisBackend( + redis_url="redis://localhost:6379/0", + algorithm_name=ALGORITHM_FIXED_WINDOW, + fallback=memory, + ) + + class _Dead: + async def script_load(self, *a: Any, **kw: Any) -> None: + raise ConnectionError("Redis is down") + + async def eval(self, *a: Any, **kw: Any) -> None: + raise ConnectionError("Redis is down") + + async def evalsha(self, *a: Any, **kw: Any) -> None: + raise ConnectionError("Redis is down") + + backend._client = _Dead() + + checks = [("user:alice", "10/s"), ("tenant:acme", "100/s")] + results = await backend.allow_many(checks) + + assert len(results) == 2, "allow_many must return one result per check" + assert all(r[0] is True for r in results), "All dimensions must be allowed via memory fallback" + + +# --------------------------------------------------------------------------- +# PERF-05: at most one Redis network round-trip per hook invocation +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_perf05_single_round_trip_per_hook_one_dim(): + """PERF-05: one dimension → one evalsha call.""" + # Standard + from unittest.mock import AsyncMock # noqa: PLC0415 + + mock_client = AsyncMock() + mock_client.script_load.return_value = "sha1" + mock_client.evalsha.return_value = [[1, 60]] + + plugin = _mk_redis_plugin({"by_user": "10/s"}) + plugin._rate_backend._client = mock_client + # Pre-populate SHAs so evalsha is used directly + plugin._rate_backend._sha_batch_fixed = "sha1" + + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = ToolPreInvokePayload(name="search", arguments={}) + await plugin.tool_pre_invoke(payload, ctx) + + total_calls = mock_client.evalsha.call_count + mock_client.eval.call_count + assert total_calls <= 1, f"PERF-05: expected ≤1 Redis call for 1 dimension, got {total_calls}" + + +@pytest.mark.asyncio +async def test_perf05_single_round_trip_per_hook_three_dims(): + """PERF-05: three dimensions (user + tenant + tool) → still one evalsha call.""" + # Standard + from unittest.mock import AsyncMock # noqa: PLC0415 + + mock_client = AsyncMock() + mock_client.script_load.return_value = "sha1" + mock_client.evalsha.return_value = [[1, 60], [1, 60], [1, 60]] + + plugin = _mk_redis_plugin({"by_user": "10/s", "by_tenant": "100/s", "by_tool": {"search": "5/s"}}) + plugin._rate_backend._client = mock_client + plugin._rate_backend._sha_batch_fixed = "sha1" + + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice", tenant_id="acme")) + payload = ToolPreInvokePayload(name="search", arguments={}) + await plugin.tool_pre_invoke(payload, ctx) + + total_calls = mock_client.evalsha.call_count + mock_client.eval.call_count + assert total_calls <= 1, f"PERF-05: expected ≤1 Redis call for 3 dimensions, got {total_calls} — " f"all dimensions must be batched into a single round-trip" + + +# --------------------------------------------------------------------------- +# PERF-03: p99 latency — Rust path must not regress vs Python memory backend +# --------------------------------------------------------------------------- + + +@_skip_no_rust +@pytest.mark.asyncio +async def test_perf03_rust_p99_does_not_regress_vs_python(): + """PERF-03: p99 latency of Rust evaluate_many() must be ≤ Python MemoryBackend.allow() p99. + + Runs 1000 requests through each path concurrently (100 at a time) and + compares p99 wall-clock latency. The Rust path is expected to be faster; + if it is somehow slower the test fails with a diagnostic message. + """ + # First-Party + from plugins.rate_limiter.rate_limiter import FixedWindowAlgorithm # noqa: PLC0415 + + CONCURRENCY = 100 + TOTAL = 1000 + LIMIT = TOTAL * 10 # never block during the benchmark + WINDOW_NANOS = 3600 * 1_000_000_000 + + # --- Python path --- + py_backend = MemoryBackend(algorithm=FixedWindowAlgorithm()) + + async def _py_call() -> float: + t0 = time.perf_counter() + await py_backend.allow("user:bench", f"{LIMIT}/h") + return time.perf_counter() - t0 + + sem = asyncio.Semaphore(CONCURRENCY) + + async def _bounded_py() -> float: + async with sem: + return await _py_call() + + py_times = await asyncio.gather(*[_bounded_py() for _ in range(TOTAL)]) + py_p99 = sorted(py_times)[int(0.99 * TOTAL)] + + # --- Rust path --- + rust_engine = RustRateLimiterEngine({"by_user": f"{LIMIT}/h", "algorithm": ALGORITHM_FIXED_WINDOW}) + now_unix = int(time.time()) + + async def _rust_call() -> float: + t0 = time.perf_counter() + rust_engine.evaluate_many([("user:bench", LIMIT, WINDOW_NANOS)], now_unix) + return time.perf_counter() - t0 + + async def _bounded_rust() -> float: + async with sem: + return await _rust_call() + + rust_times = await asyncio.gather(*[_bounded_rust() for _ in range(TOTAL)]) + rust_p99 = sorted(rust_times)[int(0.99 * TOTAL)] + + # Rust p99 must be ≤ Python p99 (Rust should be faster, never slower) + assert rust_p99 <= py_p99, f"PERF-03: Rust p99 ({rust_p99*1e6:.1f} µs) regressed vs Python p99 ({py_p99*1e6:.1f} µs)" + + +# --------------------------------------------------------------------------- +# PERF-02: Python wrapper overhead is small relative to Rust engine time +# --------------------------------------------------------------------------- + + +@_skip_no_rust +def test_perf02_wrapper_overhead_is_small(): + """PERF-02: Python wrapper overhead (context extraction + PyO3 call) must be < 10× Rust engine time. + + Measures wrapper-only cost by mocking evaluate_many() to return instantly, + then compares against real Rust engine time. The wrapper must not dominate. + """ + ITERATIONS = 10_000 + LIMIT = 1_000_000 + WINDOW_NANOS = 3600 * 1_000_000_000 + now_unix = int(time.time()) + + class _FakeEvalResult: + allowed = True + limit = LIMIT + remaining = LIMIT - 1 + reset_timestamp = now_unix + 3600 + retry_after = None + + fake_result = _FakeEvalResult() + + # --- Wrapper-only overhead (mocked Rust engine) --- + plugin = _mk_rust(f"{LIMIT}/h") + assert plugin._rust_engine is not None + + wrapper_times = [] + original_evaluate_many = plugin._rust_engine.evaluate_many + plugin._rust_engine.evaluate_many = lambda checks, ts: fake_result + try: + checks = plugin._build_rust_checks("alice", None, "search") + for _ in range(ITERATIONS): + t0 = time.perf_counter_ns() + plugin._rust_engine.evaluate_many(checks, now_unix) + wrapper_times.append(time.perf_counter_ns() - t0) + finally: + plugin._rust_engine.evaluate_many = original_evaluate_many + + # --- Real Rust engine (no wrapper) --- + engine = RustRateLimiterEngine({"by_user": f"{LIMIT}/h", "algorithm": ALGORITHM_FIXED_WINDOW}) + rust_times = [] + for _ in range(ITERATIONS): + t0 = time.perf_counter_ns() + engine.evaluate_many([("user:alice", LIMIT, WINDOW_NANOS)], now_unix) + rust_times.append(time.perf_counter_ns() - t0) + + wrapper_median = sorted(wrapper_times)[ITERATIONS // 2] + rust_median = sorted(rust_times)[ITERATIONS // 2] + + # Wrapper overhead must be < 10× the Rust engine time + assert wrapper_median < rust_median * 10, f"PERF-02: wrapper overhead ({wrapper_median} ns median) is ≥10× Rust engine " f"({rust_median} ns median) — wrapper is dominating" + + +# --------------------------------------------------------------------------- +# MEM-06: Dimension keys are distinct — same name in different dims never collide +# --------------------------------------------------------------------------- + + +@_skip_no_rust +def test_mem06_user_tenant_tool_keys_are_distinct(): + """MEM-06: 'alice' as user, tenant, and tool must produce independent counters. + + Verifies that the key namespace (user:, tenant:, tool:) prevents hash collision + between the same identifier used across different dimensions. + """ + LIMIT = 2 + WINDOW_NANOS = 3600 * 1_000_000_000 + now_unix = int(time.time()) + engine = RustRateLimiterEngine({"by_user": f"{LIMIT}/h", "algorithm": ALGORITHM_FIXED_WINDOW}) + + # Exhaust the user:alice counter + engine.evaluate_many([("user:alice", LIMIT, WINDOW_NANOS)], now_unix) + engine.evaluate_many([("user:alice", LIMIT, WINDOW_NANOS)], now_unix) + blocked = engine.evaluate_many([("user:alice", LIMIT, WINDOW_NANOS)], now_unix) + assert not blocked.allowed, "user:alice counter should be exhausted" + + # tenant:alice and tool:alice must still have independent counters + r_tenant = engine.evaluate_many([("tenant:alice", LIMIT, WINDOW_NANOS)], now_unix) + r_tool = engine.evaluate_many([("tool:alice", LIMIT, WINDOW_NANOS)], now_unix) + + assert r_tenant.allowed, "tenant:alice must be independent from user:alice" + assert r_tool.allowed, "tool:alice must be independent from user:alice" + + +# --------------------------------------------------------------------------- +# TokenBucketAlgorithm.sweep() (14a) +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_token_bucket_sweep_evicts_inactive_buckets(): + """TokenBucketAlgorithm.sweep() should evict buckets that have been inactive for >1 hour.""" + algo = TokenBucketAlgorithm() + lock = asyncio.Lock() + + # Create a bucket by issuing a request. + await algo.allow(lock, "user:stale", 10, 60) + assert "user:stale" in algo._store + + # Manually backdate last_refill to >1 hour ago. + algo._store["user:stale"].last_refill -= 3601 + + await algo.sweep(lock) + assert "user:stale" not in algo._store, "Bucket inactive for >1 hour must be evicted by sweep" + + +@pytest.mark.asyncio +async def test_token_bucket_sweep_keeps_active_buckets(): + """TokenBucketAlgorithm.sweep() should keep recently-used buckets.""" + algo = TokenBucketAlgorithm() + lock = asyncio.Lock() + + await algo.allow(lock, "user:active", 10, 60) + assert "user:active" in algo._store + + await algo.sweep(lock) + assert "user:active" in algo._store, "Recently-used bucket must not be evicted" + + +# --------------------------------------------------------------------------- +# _extract_user_identity dict fallback chain (14d) +# --------------------------------------------------------------------------- + + +def test_extract_user_identity_dict_email(): + """Dict with 'email' key should use email as identity.""" + assert _extract_user_identity({"email": "alice@example.com"}) == "alice@example.com" + + +def test_extract_user_identity_dict_id_fallback(): + """Dict without 'email' should fall back to 'id'.""" + assert _extract_user_identity({"id": "user-123"}) == "user-123" + + +def test_extract_user_identity_dict_sub_fallback(): + """Dict without 'email' or 'id' should fall back to 'sub'.""" + assert _extract_user_identity({"sub": "sub-456"}) == "sub-456" + + +def test_extract_user_identity_dict_empty_email_falls_to_id(): + """Dict with empty 'email' should fall back to 'id'.""" + assert _extract_user_identity({"email": "", "id": "user-789"}) == "user-789" + + +def test_extract_user_identity_dict_all_empty_is_anonymous(): + """Dict with all falsy identity fields should return 'anonymous'.""" + assert _extract_user_identity({"email": "", "id": "", "sub": ""}) == "anonymous" + + +def test_extract_user_identity_dict_no_keys_is_anonymous(): + """Dict with no identity keys should return 'anonymous'.""" + assert _extract_user_identity({"roles": ["admin"]}) == "anonymous" + + +def test_extract_user_identity_colons_replaced(): + """Colons in identities must be replaced to prevent key-namespace collisions.""" + assert _extract_user_identity({"sub": "auth0|user:12345"}) == "auth0|user_12345" + assert _extract_user_identity({"email": "urn:user:alice"}) == "urn_user_alice" + assert _extract_user_identity("colon:in:string") == "colon_in_string" + + +# --------------------------------------------------------------------------- +# prompt_pre_fetch Rust async Redis path (14f) +# --------------------------------------------------------------------------- + + +@_skip_no_rust +@pytest.mark.asyncio +async def test_arch01_redis_rust_prompt_uses_async_entrypoint(): + """Redis-backed Rust path should await check_async for prompt_pre_fetch.""" + # Standard + from unittest.mock import AsyncMock # noqa: PLC0415 + + plugin = RateLimiterPlugin( + PluginConfig( + name="rl", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=[PromptHookType.PROMPT_PRE_FETCH], + config={"by_user": "10/s", "backend": "redis", "redis_url": "redis://localhost:6379/0"}, + ) + ) + if plugin._rust_engine is None: + pytest.skip("Rust engine not active") + + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="alice")) + payload = PromptPrehookPayload(prompt_id="search") + + sync_mock = patch.object(plugin._rust_engine, "check", wraps=plugin._rust_engine.check) + async_mock = patch.object(plugin._rust_engine, "check_async", AsyncMock(wraps=plugin._rust_engine.check_async)) + with sync_mock as mock_sync, async_mock as mock_async: + await plugin.prompt_pre_fetch(payload, ctx) + assert mock_async.await_count == 1, "prompt_pre_fetch must use async entrypoint for Redis" + assert mock_sync.call_count == 0, "prompt_pre_fetch must not use sync entrypoint for Redis" + + +# ============================================================================ +# Sliding window Retry-After regression +# ============================================================================ + + +@pytest.mark.asyncio +async def test_sliding_window_retry_after_never_zero_when_blocked(): + """Retry-After (reset_in) must be >= 1 when the request is blocked. + + Regression: int truncation of (oldest_ts + window - now) could produce 0 + when the oldest timestamp + window rounded down to int(now). + """ + algorithm = SlidingWindowAlgorithm() + lock = asyncio.Lock() + + with patch("plugins.rate_limiter.rate_limiter.time") as mock_time: + # Place a request at a fractional timestamp + mock_time.time.return_value = 1000.1 + await algorithm.allow(lock, "user:x", 1, 1) # consume limit + + # At t=1000.9: oldest=1000.1, reset_timestamp=int(1001.1)=1001, + # reset_in = int(1001 - 1000.9) = int(0.1) = 0 WITHOUT the fix. + mock_time.time.return_value = 1000.9 + allowed, _, _, meta = await algorithm.allow(lock, "user:x", 1, 1) + + assert allowed is False + assert meta["reset_in"] >= 1, f"Retry-After must be >= 1 when blocked, got {meta['reset_in']}" + + +# ============================================================================ +# Token bucket first-request memory/Redis parity +# ============================================================================ + + +@pytest.mark.asyncio +async def test_token_bucket_first_request_reset_in_matches_refill_rate(): + """First-request reset_in must reflect tokens_needed/refill_rate, not the full window. + + Regression: memory path hard-coded time_to_full=window on first request, + while Redis derived it from tokens_needed/refill_rate, causing metadata + divergence between backends. + """ + algorithm = TokenBucketAlgorithm() + lock = asyncio.Lock() + + # 10/m → refill_rate = 10/60 ≈ 0.167 tok/s + # After first request: tokens_needed = 1, time_to_full = 1/0.167 ≈ 6 + allowed, count, reset_ts, meta = await algorithm.allow(lock, "user:y", 10, 60) + + assert allowed is True + assert meta["remaining"] == 9 + # Must NOT be 60 (the full window) — should be ~6 (1 token / refill_rate) + assert meta["reset_in"] < 60, f"First-request reset_in should reflect tokens_needed/refill_rate, " f"not the full window. Got {meta['reset_in']}, expected ~6" + assert meta["reset_in"] >= 1, "reset_in must be at least 1" + + +# --------------------------------------------------------------------------- +# RATE_LIMITER_FORCE_PYTHON env var (review finding #17) +# --------------------------------------------------------------------------- + + +def test_force_python_env_var_disables_rust(): + """Setting RATE_LIMITER_FORCE_PYTHON=1 must force _RUST_AVAILABLE to False.""" + # Standard + import importlib # noqa: PLC0415 + + # First-Party + import plugins.rate_limiter.rate_limiter as rl_mod # noqa: PLC0415 + + with patch.dict(os.environ, {"RATE_LIMITER_FORCE_PYTHON": "1"}): + importlib.reload(rl_mod) + assert rl_mod._RUST_AVAILABLE is False + + # Restore: reload without the env override so other tests are unaffected. + with patch.dict(os.environ, {}, clear=False): + os.environ.pop("RATE_LIMITER_FORCE_PYTHON", None) + importlib.reload(rl_mod) + + +# --------------------------------------------------------------------------- +# Edge-case rate string validation (review findings) +# --------------------------------------------------------------------------- + + +def test_parse_rate_zero_count_raises(): + """Zero-count rate string must raise ValueError — ambiguous semantics.""" + with pytest.raises(ValueError): + _parse_rate("0/s") + + +def test_parse_rate_negative_count_raises(): + """Negative count rate string must raise ValueError.""" + with pytest.raises(ValueError): + _parse_rate("-5/s") + + +def test_parse_rate_missing_slash_raises(): + """Malformed rate string without a slash must raise ValueError.""" + with pytest.raises(ValueError): + _parse_rate("10m") + + +def test_parse_rate_empty_string_raises(): + """Empty rate string must raise ValueError.""" + with pytest.raises(ValueError): + _parse_rate("") + + +def test_parse_rate_slash_only_raises(): + """Slash-only rate string must raise ValueError.""" + with pytest.raises(ValueError): + _parse_rate("/s") + + +def test_validate_config_redis_url_required(): + """backend='redis' without redis_url must raise ValueError at init.""" + with pytest.raises(ValueError, match="redis_url is required"): + RateLimiterPlugin( + PluginConfig( + name="rl", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=[PromptHookType.PROMPT_PRE_FETCH], + config={"by_user": "10/s", "backend": "redis"}, + ) + ) + + +# --------------------------------------------------------------------------- +# Rust tenant_id=None skips tenant dimension (review finding) +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +@patch("plugins.rate_limiter.rate_limiter._RUST_AVAILABLE", False) +async def test_tenant_none_skips_by_tenant_dimension(): + """When tenant_id is None, the by_tenant dimension must be skipped entirely.""" + plugin = RateLimiterPlugin( + PluginConfig( + name="rl", + kind="plugins.rate_limiter.rate_limiter.RateLimiterPlugin", + hooks=[PromptHookType.PROMPT_PRE_FETCH], + config={"by_user": "100/s", "by_tenant": "1/s"}, + ) + ) + # tenant_id=None — by_tenant should be skipped, so 2 requests should both pass + ctx = PluginContext(global_context=GlobalContext(request_id="r1", user="u1", tenant_id=None)) + payload = PromptPrehookPayload(prompt_id="p", args={}) + r1 = await plugin.prompt_pre_fetch(payload, ctx) + assert r1.violation is None + r2 = await plugin.prompt_pre_fetch(payload, ctx) + assert r2.violation is None diff --git a/tests/unit/mcpgateway/test_auth.py b/tests/unit/mcpgateway/test_auth.py index eb9c0c3369..afd95a7142 100644 --- a/tests/unit/mcpgateway/test_auth.py +++ b/tests/unit/mcpgateway/test_auth.py @@ -1282,12 +1282,10 @@ def test_update_api_token_last_used_sync_falls_back_to_memory_cache(self): import sys # First-Party + from mcpgateway import auth from mcpgateway.auth import _update_api_token_last_used_sync from mcpgateway.db import EmailApiToken - # First-Party - from mcpgateway import auth - # Clear the module-level in-memory cache auth._LAST_USED_CACHE.clear() @@ -1332,12 +1330,10 @@ def test_update_api_token_last_used_sync_falls_back_to_memory_cache(self): def test_update_api_token_last_used_sync_redis_exception_falls_back_to_memory(self): """Test that _update_api_token_last_used_sync falls back to memory cache when Redis operations fail.""" # First-Party + from mcpgateway import auth from mcpgateway.auth import _update_api_token_last_used_sync from mcpgateway.db import EmailApiToken - # First-Party - from mcpgateway import auth - # Clear the module-level in-memory cache auth._LAST_USED_CACHE.clear() @@ -3790,3 +3786,323 @@ def _unexpected_lookup(_team_id): ) assert resolved == "Batched Team" + + +# ============================================================================= +# P0/P1 Tests — tenant_id propagation from auth layer to GlobalContext +# ============================================================================= + + +class TestTenantIdPropagation: + """Verify that _inject_userinfo_instate and auth paths propagate team_id → tenant_id. + + The auth middleware resolves request.state.team_id from the JWT teams claim + (single-team tokens only). These tests verify that this value is propagated + into GlobalContext.tenant_id so that the rate limiter plugin can enforce + per-tenant limits correctly. + + These tests verify that _propagate_tenant_id() correctly propagates + team_id into GlobalContext.tenant_id at every return path in + get_current_user(). + """ + + def _make_request(self, team_id=None, existing_global_context=None): + """Build a minimal mock request with configurable state.""" + state = SimpleNamespace( + team_id=team_id, + plugin_global_context=existing_global_context, + ) + return SimpleNamespace(state=state, client=None, headers={}) + + def _make_user(self, email="alice@example.com"): + # Standard + from datetime import datetime, timezone # noqa: PLC0415 + + # First-Party + from mcpgateway.db import EmailUser # noqa: PLC0415 + + return EmailUser( + email=email, + password_hash="h", + full_name="Alice", + is_admin=False, + is_active=True, + email_verified_at=datetime.now(timezone.utc), + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc), + ) + + def test_single_team_propagates_tenant_id(self): + """P0: the get_current_user() calling sequence must propagate team_id → tenant_id. + + In production, _inject_userinfo_instate() runs first (may create + GlobalContext), then _propagate_tenant_id() fills in tenant_id. + """ + # First-Party + import mcpgateway.auth as auth_module # noqa: PLC0415 + from mcpgateway.plugins.framework import GlobalContext # noqa: PLC0415 + + global_context = GlobalContext(request_id="r1") + request = self._make_request(team_id="team-acme", existing_global_context=global_context) + user = self._make_user() + + # Mirror the actual calling sequence in get_current_user(): + auth_module._inject_userinfo_instate(request=request, user=user) + auth_module._propagate_tenant_id(request) + + assert request.state.plugin_global_context.tenant_id == "team-acme", "_propagate_tenant_id must propagate request.state.team_id into " "global_context.tenant_id for single-team tokens" + + def test_no_team_id_leaves_tenant_id_none(self): + """P1: when request.state.team_id is None (multi-team or no team), tenant_id stays None. + + Multi-team tokens have team_id=None because there is no single authoritative tenant. + The plugin must receive tenant_id=None and skip by_tenant — not invent a 'default'. + """ + # First-Party + import mcpgateway.auth as auth_module # noqa: PLC0415 + from mcpgateway.plugins.framework import GlobalContext # noqa: PLC0415 + + global_context = GlobalContext(request_id="r1") + request = self._make_request(team_id=None, existing_global_context=global_context) + user = self._make_user() + + auth_module._inject_userinfo_instate(request=request, user=user) + auth_module._propagate_tenant_id(request) + + assert request.state.plugin_global_context.tenant_id is None, "When team_id is None (multi-team or no-team token), " "tenant_id must remain None — no fake 'default' tenant should be invented" + + def test_existing_tenant_id_is_not_overwritten(self): + """P1: if global_context.tenant_id is already set (e.g. by an auth plugin), do not overwrite it. + + _propagate_tenant_id() must not clobber an explicit tenant identity. + """ + # First-Party + import mcpgateway.auth as auth_module # noqa: PLC0415 + from mcpgateway.plugins.framework import GlobalContext # noqa: PLC0415 + + global_context = GlobalContext(request_id="r1", tenant_id="existing-tenant") + request = self._make_request(team_id="different-team", existing_global_context=global_context) + user = self._make_user() + + auth_module._inject_userinfo_instate(request=request, user=user) + auth_module._propagate_tenant_id(request) + + assert request.state.plugin_global_context.tenant_id == "existing-tenant", "An already-set tenant_id must not be overwritten by team_id resolution" + + @pytest.mark.asyncio + async def test_get_current_user_fallback_propagates_team_id_to_tenant_id(self): + """get_current_user() fallback constructs GlobalContext with tenant_id=team_id. + + When request.state has no plugin_global_context (i.e. middleware did not + pre-populate it), get_current_user() must construct a GlobalContext with + tenant_id set from request.state.team_id so the rate limiter can enforce + per-tenant limits on this request path. + """ + # First-Party + from mcpgateway.plugins.framework import PluginResult # noqa: PLC0415 + + credentials = HTTPAuthorizationCredentials(scheme="Bearer", credentials="tok") + request = SimpleNamespace( + state=SimpleNamespace(team_id="team-acme"), # no plugin_global_context set + client=None, + headers={}, + ) + + mock_pm = MagicMock() + mock_pm.has_hooks_for = MagicMock(return_value=True) + mock_pm.config.plugin_settings.include_user_info = False + plugin_result = PluginResult(modified_payload={"email": "alice@example.com"}, continue_processing=False) + mock_pm.invoke_hook = AsyncMock(return_value=(plugin_result, None)) + db_user = self._make_user("alice@example.com") + + with ( + patch("mcpgateway.auth.get_plugin_manager", return_value=mock_pm), + patch("mcpgateway.auth.get_correlation_id", return_value="req-1"), + patch("mcpgateway.auth._inject_userinfo_instate"), + patch("mcpgateway.auth._get_user_by_email_sync", return_value=db_user), + ): + await get_current_user(credentials=credentials, request=request) + + call_kwargs = mock_pm.invoke_hook.call_args + global_context = call_kwargs.kwargs.get("global_context") + assert global_context is not None + assert global_context.tenant_id == "team-acme", "get_current_user() fallback must propagate request.state.team_id " "into GlobalContext.tenant_id for by_tenant rate limiting" + + @pytest.mark.asyncio + async def test_get_current_user_fallback_tenant_id_none_when_no_team(self): + """get_current_user() fallback sets tenant_id=None when team_id is absent. + + When request.state.team_id is None (multi-team or admin token), the + constructed GlobalContext must have tenant_id=None so the rate limiter + skips by_tenant enforcement rather than inventing a phantom tenant. + """ + # First-Party + from mcpgateway.plugins.framework import PluginResult # noqa: PLC0415 + + credentials = HTTPAuthorizationCredentials(scheme="Bearer", credentials="tok") + request = SimpleNamespace( + state=SimpleNamespace(team_id=None), # no plugin_global_context set + client=None, + headers={}, + ) + + mock_pm = MagicMock() + mock_pm.has_hooks_for = MagicMock(return_value=True) + mock_pm.config.plugin_settings.include_user_info = False + plugin_result = PluginResult(modified_payload={"email": "alice@example.com"}, continue_processing=False) + mock_pm.invoke_hook = AsyncMock(return_value=(plugin_result, None)) + db_user = self._make_user("alice@example.com") + + with ( + patch("mcpgateway.auth.get_plugin_manager", return_value=mock_pm), + patch("mcpgateway.auth.get_correlation_id", return_value="req-1"), + patch("mcpgateway.auth._inject_userinfo_instate"), + patch("mcpgateway.auth._get_user_by_email_sync", return_value=db_user), + ): + await get_current_user(credentials=credentials, request=request) + + call_kwargs = mock_pm.invoke_hook.call_args + global_context = call_kwargs.kwargs.get("global_context") + assert global_context is not None + assert global_context.tenant_id is None, "When team_id is None, GlobalContext.tenant_id must be None — " "no phantom tenant should be invented" + + def test_propagate_tenant_id_on_middleware_seeded_context(self): + """_propagate_tenant_id must work when middleware has already created GlobalContext. + + Regression: the original fix only propagated team_id → tenant_id inside + _inject_userinfo_instate (gated by include_user_info, default False) or + in the get_current_user fallback (skipped when plugin_global_context exists). + On the normal middleware path, tenant_id stayed None. + """ + # First-Party + import mcpgateway.auth as auth_module # noqa: PLC0415 + from mcpgateway.plugins.framework import GlobalContext # noqa: PLC0415 + + # Simulate middleware pre-creating context with tenant_id=None + global_context = GlobalContext(request_id="r1", tenant_id=None) + request = self._make_request(team_id="team-acme", existing_global_context=global_context) + + auth_module._propagate_tenant_id(request) + + assert request.state.plugin_global_context.tenant_id == "team-acme", ( + "_propagate_tenant_id must fill tenant_id even when middleware " "has already created plugin_global_context with tenant_id=None" + ) + + def test_propagate_tenant_id_no_overwrite(self): + """_propagate_tenant_id must not overwrite an already-set tenant_id.""" + # First-Party + import mcpgateway.auth as auth_module # noqa: PLC0415 + from mcpgateway.plugins.framework import GlobalContext # noqa: PLC0415 + + global_context = GlobalContext(request_id="r1", tenant_id="plugin-set-tenant") + request = self._make_request(team_id="different-team", existing_global_context=global_context) + + auth_module._propagate_tenant_id(request) + + assert request.state.plugin_global_context.tenant_id == "plugin-set-tenant", "_propagate_tenant_id must not overwrite an already-set tenant_id" + + def test_propagate_tenant_id_none_request_is_noop(self): + """_propagate_tenant_id with None request must not raise.""" + # First-Party + import mcpgateway.auth as auth_module # noqa: PLC0415 + + # Should not raise + auth_module._propagate_tenant_id(None) + + def test_propagate_tenant_id_missing_team_id_attribute(self): + """_propagate_tenant_id must handle request.state without team_id attribute. + + Deny-path: request.state may not have team_id (e.g. unauthenticated + requests or middleware that doesn't set it). The function uses getattr + fallback — verify it leaves tenant_id as None rather than raising. + """ + # First-Party + import mcpgateway.auth as auth_module # noqa: PLC0415 + from mcpgateway.plugins.framework import GlobalContext # noqa: PLC0415 + + global_context = GlobalContext(request_id="r1", tenant_id=None) + # State has plugin_global_context but NO team_id attribute + state = SimpleNamespace(plugin_global_context=global_context) + request = SimpleNamespace(state=state, client=None, headers={}) + + auth_module._propagate_tenant_id(request) + + assert global_context.tenant_id is None, "When request.state has no team_id attribute, tenant_id must remain None" + + @pytest.mark.asyncio + async def test_propagate_tenant_id_on_cache_hit_path(self): + """_propagate_tenant_id must be called on the auth cache hit return path. + + Regression: if _propagate_tenant_id(request) is accidentally removed + from the cache-hit branch of get_current_user(), by_tenant rate limiting + would silently stop working for cached auth requests. + """ + with patch("mcpgateway.auth._propagate_tenant_id") as mock_prop: + credentials = HTTPAuthorizationCredentials(scheme="Bearer", credentials="tok") + payload = { + "sub": "test@example.com", + "exp": (datetime.now(timezone.utc) + timedelta(hours=1)).timestamp(), + "jti": "jti-prop-cache", + "teams": ["team-acme"], + "user": {"email": "test@example.com", "full_name": "T", "is_admin": False, "auth_provider": "local"}, + } + cached_ctx = SimpleNamespace( + is_token_revoked=False, + user={"email": "test@example.com", "full_name": "T", "is_admin": False, "is_active": True}, + personal_team_id="team_123", + ) + request = SimpleNamespace(state=SimpleNamespace(), client=None, headers={}) + + with ( + patch("mcpgateway.auth.settings") as mock_settings, + patch("mcpgateway.auth.get_plugin_manager", return_value=None), + patch("mcpgateway.auth.verify_jwt_token_cached", AsyncMock(return_value=payload)), + patch("mcpgateway.cache.auth_cache.auth_cache.get_auth_context", AsyncMock(return_value=cached_ctx)), + ): + mock_settings.auth_cache_enabled = True + mock_settings.auth_required = True + mock_settings.jwt_secret = "secret" + mock_settings.admin_api_enabled = True + mock_settings.require_user_in_db = False + await get_current_user(credentials=credentials, request=request) + + assert mock_prop.called, "_propagate_tenant_id must be called on the cache-hit return path" + + @pytest.mark.asyncio + async def test_propagate_tenant_id_on_batched_query_path(self): + """_propagate_tenant_id must be called on the batched DB query return path. + + Regression: if _propagate_tenant_id(request) is accidentally removed + from the batched-query branch of get_current_user(), by_tenant rate + limiting would silently stop working for batched-auth requests. + """ + with patch("mcpgateway.auth._propagate_tenant_id") as mock_prop: + credentials = HTTPAuthorizationCredentials(scheme="Bearer", credentials="tok") + payload = { + "sub": "test@example.com", + "exp": (datetime.now(timezone.utc) + timedelta(hours=1)).timestamp(), + "jti": "jti-prop-batch", + "teams": ["team-acme"], + "user": {"email": "test@example.com", "full_name": "T", "is_admin": False, "auth_provider": "local"}, + } + auth_ctx = { + "user": {"email": "test@example.com", "full_name": "T", "is_admin": False, "is_active": True}, + "personal_team_id": "team_123", + "is_token_revoked": False, + } + request = SimpleNamespace(state=SimpleNamespace(), client=None, headers={}) + + with ( + patch("mcpgateway.auth.settings") as mock_settings, + patch("mcpgateway.auth.get_plugin_manager", return_value=None), + patch("mcpgateway.auth.verify_jwt_token_cached", AsyncMock(return_value=payload)), + patch("mcpgateway.auth._get_auth_context_batched_sync", return_value=auth_ctx), + ): + mock_settings.auth_cache_enabled = False + mock_settings.auth_cache_batch_queries = True + mock_settings.auth_required = True + mock_settings.jwt_secret = "secret" + mock_settings.admin_api_enabled = True + await get_current_user(credentials=credentials, request=request) + + assert mock_prop.called, "_propagate_tenant_id must be called on the batched-query return path"