From 1b1b9346da2c144cb3293b9de0a1d52f71e8d45e Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Wed, 31 Dec 2025 12:16:54 +0100 Subject: [PATCH 1/9] feat(gs): add debug endpoint for secure database queries Add new DEBUG user role for developer database access with POST /gs/debug endpoint for executing read-only SQL queries. Security layers: - Role-based access (DEBUG/ADMIN/SUPER_ADMIN only) - SQL parsing with node-sql-parser (AST validation) - Only single SELECT statements allowed - Blocked: UNION/INTERSECT/EXCEPT, SELECT INTO, FOR XML/JSON - Blocked: OPENROWSET, OPENQUERY, OPENDATASOURCE (external connections) - Pre-execution column checking (blocks alias bypass) - Input validation with MaxLength(10000) - Post-execution PII column masking (defense in depth) - Full audit trail with user identification Blocked columns: mail, email, firstname, surname, iban, ip, apiKey, etc. --- package-lock.json | 661 +++--------------- package.json | 1 + src/shared/auth/role.guard.ts | 1 + src/shared/auth/user-role.enum.ts | 1 + .../generic/gs/dto/debug-query.dto.ts | 8 + src/subdomains/generic/gs/gs.controller.ts | 14 + src/subdomains/generic/gs/gs.service.ts | 199 ++++++ 7 files changed, 333 insertions(+), 552 deletions(-) create mode 100644 src/subdomains/generic/gs/dto/debug-query.dto.ts diff --git a/package-lock.json b/package-lock.json index c0ec54620b..09c50dc5d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,6 @@ "@arbitrum/sdk": "^3.7.3", "@azure/storage-blob": "^12.29.1", "@blockfrost/blockfrost-js": "^6.1.0", - "@btc-vision/bitcoin-rpc": "^1.0.6", "@cardano-foundation/cardano-verify-datasignature": "^1.0.11", "@deuro/eurocoin": "^1.0.16", "@dhedge/v2-sdk": "^1.11.1", @@ -89,6 +88,7 @@ "nestjs-i18n": "^10.5.1", "nestjs-real-ip": "^2.2.0", "node-2fa": "^2.0.3", + "node-sql-parser": "^5.3.13", "nodemailer": "^6.10.1", "passport": "^0.6.0", "passport-jwt": "^4.0.1", @@ -1431,6 +1431,7 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1440,6 +1441,7 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", @@ -1470,6 +1472,7 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.28.5", @@ -1486,6 +1489,7 @@ "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.27.2", @@ -1502,6 +1506,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, "license": "ISC", "dependencies": { "yallist": "^3.0.2" @@ -1511,12 +1516,14 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, "license": "ISC" }, "node_modules/@babel/helper-globals": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1526,6 +1533,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.27.1", @@ -1539,6 +1547,7 @@ "version": "7.28.3", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", @@ -1584,6 +1593,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1593,6 +1603,7 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", @@ -1869,6 +1880,7 @@ "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", @@ -1883,6 +1895,7 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", @@ -2265,214 +2278,6 @@ "url": "https://github.com/sponsors/eemeli" } }, - "node_modules/@btc-vision/bitcoin-rpc": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@btc-vision/bitcoin-rpc/-/bitcoin-rpc-1.0.6.tgz", - "integrity": "sha512-w8Y0KIMg9iSH6f8dRJJQ+HzArQXsZpIexGZdjBssvZ+vK5NV+pMdpHC3/pzxzZ+DOrKZLI+CsmeSjF82g56rUw==", - "license": "MIT", - "dependencies": { - "@btc-vision/bsi-common": "^1.2.1", - "@eslint/js": "^9.39.1", - "rpc-request": "^9.0.0", - "ts-node": "^10.9.2", - "undici": "^7.15.0" - } - }, - "node_modules/@btc-vision/bitcoin-rpc/node_modules/undici": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", - "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", - "license": "MIT", - "engines": { - "node": ">=20.18.1" - } - }, - "node_modules/@btc-vision/bsi-common": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@btc-vision/bsi-common/-/bsi-common-1.2.1.tgz", - "integrity": "sha512-BWFJVJ+RqnQbAiRNfV2iM+pyPhYMp91NhWytM6uaAMeVoaDiNAy3FEasqdloCydOUvcGP+3wnNzBMZzdILhSyg==", - "license": "LICENSE.MD", - "dependencies": { - "@btc-vision/logger": "^1.0.8", - "@eslint/js": "^9.39.1", - "babel-plugin-transform-import-meta": "^2.3.3", - "mongodb": "^7.0.0", - "toml": "^3.0.0", - "ts-node": "^10.9.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@btc-vision/bsi-common/node_modules/@types/whatwg-url": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz", - "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==", - "license": "MIT", - "dependencies": { - "@types/webidl-conversions": "*" - } - }, - "node_modules/@btc-vision/bsi-common/node_modules/bson": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/bson/-/bson-7.0.0.tgz", - "integrity": "sha512-Kwc6Wh4lQ5OmkqqKhYGKIuELXl+EPYSCObVE6bWsp1T/cGkOCBN0I8wF/T44BiuhHyNi1mmKVPXk60d41xZ7kw==", - "license": "Apache-2.0", - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/@btc-vision/bsi-common/node_modules/mongodb": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.0.0.tgz", - "integrity": "sha512-vG/A5cQrvGGvZm2mTnCSz1LUcbOPl83hfB6bxULKQ8oFZauyox/2xbZOoGNl+64m8VBrETkdGCDBdOsCr3F3jg==", - "license": "Apache-2.0", - "dependencies": { - "@mongodb-js/saslprep": "^1.3.0", - "bson": "^7.0.0", - "mongodb-connection-string-url": "^7.0.0" - }, - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@aws-sdk/credential-providers": "^3.806.0", - "@mongodb-js/zstd": "^7.0.0", - "gcp-metadata": "^7.0.1", - "kerberos": "^7.0.0", - "mongodb-client-encryption": ">=7.0.0 <7.1.0", - "snappy": "^7.3.2", - "socks": "^2.8.6" - }, - "peerDependenciesMeta": { - "@aws-sdk/credential-providers": { - "optional": true - }, - "@mongodb-js/zstd": { - "optional": true - }, - "gcp-metadata": { - "optional": true - }, - "kerberos": { - "optional": true - }, - "mongodb-client-encryption": { - "optional": true - }, - "snappy": { - "optional": true - }, - "socks": { - "optional": true - } - } - }, - "node_modules/@btc-vision/bsi-common/node_modules/mongodb-connection-string-url": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.0.tgz", - "integrity": "sha512-irhhjRVLE20hbkRl4zpAYLnDMM+zIZnp0IDB9akAFFUZp/3XdOfwwddc7y6cNvF2WCEtfTYRwYbIfYa2kVY0og==", - "license": "Apache-2.0", - "dependencies": { - "@types/whatwg-url": "^13.0.0", - "whatwg-url": "^14.1.0" - }, - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/@btc-vision/bsi-common/node_modules/tr46": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", - "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@btc-vision/bsi-common/node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/@btc-vision/bsi-common/node_modules/whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", - "license": "MIT", - "dependencies": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@btc-vision/logger": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@btc-vision/logger/-/logger-1.0.8.tgz", - "integrity": "sha512-XncePlqNlY7603eF9xRExF5Fdbhj89AeGdSjNh6psgf3Q55/KjCD1MECEqicf/FN6CGf3xRVnMC951D+qfj0SA==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.25.2", - "@eslint/js": "9.38.0", - "assert": "^2.1.0", - "babel-loader": "^9.1.3", - "babel-plugin-transform-import-meta": "^2.2.1", - "babel-preset-react": "^6.24.1", - "babelify": "^10.0.0", - "chalk": "^5.3.0", - "supports-color": "^9.4.0", - "ts-loader": "^9.5.1", - "ts-node": "^10.9.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@btc-vision/logger/node_modules/@eslint/js": { - "version": "9.38.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz", - "integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==", - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@btc-vision/logger/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@btc-vision/logger/node_modules/supports-color": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", - "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/@cardano-foundation/cardano-verify-datasignature": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@cardano-foundation/cardano-verify-datasignature/-/cardano-verify-datasignature-1.0.11.tgz", @@ -3077,6 +2882,7 @@ "version": "9.39.2", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5490,6 +5296,7 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -5500,6 +5307,7 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -5519,6 +5327,7 @@ "version": "0.3.11", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -5535,6 +5344,7 @@ "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -5761,6 +5571,8 @@ "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.4.tgz", "integrity": "sha512-p7X/ytJDIdwUfFL/CLOhKgdfJe1Fa8uw9seJYvdOmnP9JBWGWHW69HkOixXS6Wy9yvGf1MbhcS6lVmrhy4jm2g==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "sparse-bitfield": "^3.0.3" } @@ -9033,6 +8845,7 @@ "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, "license": "MIT", "dependencies": { "@types/estree": "*", @@ -9043,6 +8856,7 @@ "version": "3.7.7", "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, "license": "MIT", "dependencies": { "@types/eslint": "*", @@ -9053,6 +8867,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, "license": "MIT" }, "node_modules/@types/express": { @@ -9163,6 +8978,7 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, "license": "MIT" }, "node_modules/@types/jsonwebtoken": { @@ -9308,6 +9124,12 @@ "@types/node": "*" } }, + "node_modules/@types/pegjs": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@types/pegjs/-/pegjs-0.10.6.tgz", + "integrity": "sha512-eLYXDbZWXh2uxf+w8sXS8d6KSoXTswfps6fvCUuVAGN8eRpfe7h9eSRydxiSJvo9Bf+GzifsDOr9TMQlmJdmkw==", + "license": "MIT" + }, "node_modules/@types/pug": { "version": "2.0.10", "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz", @@ -9469,7 +9291,9 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/@types/whatwg-url": { "version": "11.0.5", @@ -10167,6 +9991,7 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", @@ -10177,24 +10002,28 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", @@ -10206,12 +10035,14 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", @@ -10224,6 +10055,7 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, "license": "MIT", "dependencies": { "@xtuc/ieee754": "^1.2.0" @@ -10233,6 +10065,7 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@xtuc/long": "4.2.2" @@ -10242,12 +10075,14 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", @@ -10264,6 +10099,7 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", @@ -10277,6 +10113,7 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", @@ -10289,6 +10126,7 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", @@ -10303,6 +10141,7 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", @@ -10331,12 +10170,14 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, "license": "BSD-3-Clause" }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, "license": "Apache-2.0" }, "node_modules/@zano-project/zano-utils-js": { @@ -10549,6 +10390,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, "license": "MIT", "peer": true, "engines": { @@ -10659,6 +10501,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, "license": "MIT", "dependencies": { "ajv": "^8.0.0" @@ -10676,6 +10519,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3" @@ -11226,17 +11070,6 @@ } } }, - "node_modules/babel-helper-builder-react-jsx": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.26.0.tgz", - "integrity": "sha512-02I9jDjnVEuGy2BR3LRm9nPRb/+Ja0pvZVLr1eI5TYAA/dB0Xoc+WBo50+aDfhGDLhlBY1+QURjn9uvcFd8gzg==", - "license": "MIT", - "dependencies": { - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "esutils": "^2.0.2" - } - }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -11259,42 +11092,6 @@ "@babel/core": "^7.8.0" } }, - "node_modules/babel-loader": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz", - "integrity": "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==", - "license": "MIT", - "dependencies": { - "find-cache-dir": "^4.0.0", - "schema-utils": "^4.0.0" - }, - "engines": { - "node": ">= 14.15.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0", - "webpack": ">=5" - } - }, - "node_modules/babel-loader/node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/babel-plugin-istanbul": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", @@ -11345,81 +11142,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/babel-plugin-syntax-flow": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz", - "integrity": "sha512-HbTDIoG1A1op7Tl/wIFQPULIBA61tsJ8Ntq2FAhLwuijrzosM/92kAfgU1Q3Kc7DH/cprJg5vDfuTY4QUL4rDA==", - "license": "MIT" - }, - "node_modules/babel-plugin-syntax-jsx": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", - "integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==", - "license": "MIT" - }, - "node_modules/babel-plugin-transform-flow-strip-types": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz", - "integrity": "sha512-TxIM0ZWNw9oYsoTthL3lvAK3+eTujzktoXJg4ubGvICGbVuXVYv5hHv0XXpz8fbqlJaGYY4q5SVzaSmsg3t4Fg==", - "license": "MIT", - "dependencies": { - "babel-plugin-syntax-flow": "^6.18.0", - "babel-runtime": "^6.22.0" - } - }, - "node_modules/babel-plugin-transform-import-meta": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-import-meta/-/babel-plugin-transform-import-meta-2.3.3.tgz", - "integrity": "sha512-bbh30qz1m6ZU1ybJoNOhA2zaDvmeXMnGNBMVMDOJ1Fni4+wMBoy/j7MTRVmqAUCIcy54/rEnr9VEBsfcgbpm3Q==", - "license": "BSD-3-Clause", - "dependencies": { - "@babel/template": "^7.25.9", - "tslib": "^2.8.1" - }, - "peerDependencies": { - "@babel/core": "^7.10.0" - } - }, - "node_modules/babel-plugin-transform-react-display-name": { - "version": "6.25.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.25.0.tgz", - "integrity": "sha512-QLYkLiZeeED2PKd4LuXGg5y9fCgPB5ohF8olWUuETE2ryHNRqqnXlEVP7RPuef89+HTfd3syptMGVHeoAu0Wig==", - "license": "MIT", - "dependencies": { - "babel-runtime": "^6.22.0" - } - }, - "node_modules/babel-plugin-transform-react-jsx": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.24.1.tgz", - "integrity": "sha512-s+q/Y2u2OgDPHRuod3t6zyLoV8pUHc64i/O7ZNgIOEdYTq+ChPeybcKBi/xk9VI60VriILzFPW+dUxAEbTxh2w==", - "license": "MIT", - "dependencies": { - "babel-helper-builder-react-jsx": "^6.24.1", - "babel-plugin-syntax-jsx": "^6.8.0", - "babel-runtime": "^6.22.0" - } - }, - "node_modules/babel-plugin-transform-react-jsx-self": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx-self/-/babel-plugin-transform-react-jsx-self-6.22.0.tgz", - "integrity": "sha512-Y3ZHP1nunv0U1+ysTNwLK39pabHj6cPVsfN4TRC7BDBfbgbyF4RifP5kd6LnbuMV9wcfedQMe7hn1fyKc7IzTQ==", - "license": "MIT", - "dependencies": { - "babel-plugin-syntax-jsx": "^6.8.0", - "babel-runtime": "^6.22.0" - } - }, - "node_modules/babel-plugin-transform-react-jsx-source": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx-source/-/babel-plugin-transform-react-jsx-source-6.22.0.tgz", - "integrity": "sha512-pcDNDsZ9q/6LJmujQ/OhjeoIlp5Nl546HJ2yiFIJK3mYpgNXhI5/S9mXfVxu5yqWAi7HdI7e/q6a9xtzwL69Vw==", - "license": "MIT", - "dependencies": { - "babel-plugin-syntax-jsx": "^6.8.0", - "babel-runtime": "^6.22.0" - } - }, "node_modules/babel-preset-current-node-syntax": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", @@ -11447,15 +11169,6 @@ "@babel/core": "^7.0.0 || ^8.0.0-0" } }, - "node_modules/babel-preset-flow": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-preset-flow/-/babel-preset-flow-6.23.0.tgz", - "integrity": "sha512-PQZFJXnM3d80Vq4O67OE6EMVKIw2Vmzy8UXovqulNogCtblWU8rzP7Sm5YgHiCg4uejUxzCkHfNXQ4Z6GI+Dhw==", - "license": "MIT", - "dependencies": { - "babel-plugin-transform-flow-strip-types": "^6.22.0" - } - }, "node_modules/babel-preset-jest": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", @@ -11473,48 +11186,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/babel-preset-react": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-preset-react/-/babel-preset-react-6.24.1.tgz", - "integrity": "sha512-phQe3bElbgF887UM0Dhz55d22ob8czTL1kbhZFwpCE6+R/X9kHktfwmx9JZb+bBSVRGphP5tZ9oWhVhlgjrX3Q==", - "license": "MIT", - "dependencies": { - "babel-plugin-syntax-jsx": "^6.3.13", - "babel-plugin-transform-react-display-name": "^6.23.0", - "babel-plugin-transform-react-jsx": "^6.24.1", - "babel-plugin-transform-react-jsx-self": "^6.22.0", - "babel-plugin-transform-react-jsx-source": "^6.22.0", - "babel-preset-flow": "^6.23.0" - } - }, - "node_modules/babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==", - "license": "MIT", - "dependencies": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" - } - }, - "node_modules/babel-runtime/node_modules/regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", - "license": "MIT" - }, - "node_modules/babel-types": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", - "integrity": "sha512-zhe3V/26rCWsEZK8kZN+HaQj5yQ1CilTObixFzKW1UWjqG7618Twz6YEsCnjfg5gBcJh02DrpCkS9h98ZqDY+g==", - "license": "MIT", - "dependencies": { - "babel-runtime": "^6.26.0", - "esutils": "^2.0.2", - "lodash": "^4.17.4", - "to-fast-properties": "^1.0.3" - } - }, "node_modules/babel-walk": { "version": "3.0.0-canary-5", "resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz", @@ -11527,18 +11198,6 @@ "node": ">= 10.0.0" } }, - "node_modules/babelify": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/babelify/-/babelify-10.0.0.tgz", - "integrity": "sha512-X40FaxyH7t3X+JFAKvb1H9wooWKLRCi8pg3m8poqtdZaIng+bjzp9RvKQCvRjF9isHiPkXspbbXT/zwXLtwgwg==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -11584,6 +11243,7 @@ "version": "2.8.21", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.21.tgz", "integrity": "sha512-JU0h5APyQNsHOlAM7HnQnPToSDQoEBZqzu/YBlqDnEeymPnZDREeXJA3KBMQee+dKteAxZ2AtvQEvVYdZf241Q==", + "dev": true, "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.js" @@ -12217,6 +11877,7 @@ "version": "4.27.0", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", + "dev": true, "funding": [ { "type": "opencollective", @@ -12561,6 +12222,7 @@ "version": "1.0.30001751", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz", "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==", + "dev": true, "funding": [ { "type": "opencollective", @@ -12810,6 +12472,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.0" @@ -13180,12 +12843,6 @@ "node": ">= 6" } }, - "node_modules/common-path-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", - "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", - "license": "ISC" - }, "node_modules/component-emitter": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-2.0.0.tgz", @@ -13319,6 +12976,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, "license": "MIT" }, "node_modules/cookie": { @@ -13347,14 +13005,6 @@ "dev": true, "license": "MIT" }, - "node_modules/core-js": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", - "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", - "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", - "hasInstallScript": true, - "license": "MIT" - }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -14337,6 +13987,7 @@ "version": "1.5.243", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.243.tgz", "integrity": "sha512-ZCphxFW3Q1TVhcgS9blfut1PX8lusVi2SvXQgmEEnK4TCmE1JhH2JkjJN+DNt0pJJwfBri5AROBnz2b/C+YU9g==", + "dev": true, "license": "ISC" }, "node_modules/elliptic": { @@ -14456,6 +14107,7 @@ "version": "5.18.3", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -14648,6 +14300,7 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, "license": "MIT" }, "node_modules/es-object-atoms": { @@ -14905,6 +14558,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", @@ -15089,6 +14743,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" @@ -15101,6 +14756,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -15110,6 +14766,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -15119,6 +14776,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -16001,119 +15659,6 @@ "node": ">= 0.8" } }, - "node_modules/find-cache-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", - "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", - "license": "MIT", - "dependencies": { - "common-path-prefix": "^3.0.0", - "pkg-dir": "^7.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/find-cache-dir/node_modules/find-up": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", - "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", - "license": "MIT", - "dependencies": { - "locate-path": "^7.1.0", - "path-exists": "^5.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/find-cache-dir/node_modules/locate-path": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", - "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", - "license": "MIT", - "dependencies": { - "p-locate": "^6.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/find-cache-dir/node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", - "license": "MIT", - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/find-cache-dir/node_modules/p-locate": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", - "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", - "license": "MIT", - "dependencies": { - "p-limit": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/find-cache-dir/node_modules/path-exists": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", - "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/find-cache-dir/node_modules/pkg-dir": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", - "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", - "license": "MIT", - "dependencies": { - "find-up": "^6.3.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/find-cache-dir/node_modules/yocto-queue": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", - "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", - "license": "MIT", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -16565,6 +16110,7 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -16852,6 +16398,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, "license": "BSD-2-Clause" }, "node_modules/glob/node_modules/brace-expansion": { @@ -19783,6 +19330,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -19851,6 +19399,7 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -20708,6 +20257,7 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.11.5" @@ -21043,7 +20593,9 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/memorystream": { "version": "0.3.1", @@ -21077,6 +20629,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, "license": "MIT" }, "node_modules/merkletreejs": { @@ -21168,6 +20721,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -22592,6 +22146,7 @@ "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, "license": "MIT" }, "node_modules/node-rsa": { @@ -22603,6 +22158,19 @@ "asn1": "^0.2.4" } }, + "node_modules/node-sql-parser": { + "version": "5.3.13", + "resolved": "https://registry.npmjs.org/node-sql-parser/-/node-sql-parser-5.3.13.tgz", + "integrity": "sha512-heyWv3lLjKHpcBDMUSR+R0DohRYZTYq+Ro3hJ4m9Ia8ccdKbL5UijIaWr2L4co+bmmFuvBVZ4v23QW2PqvBFAA==", + "license": "Apache-2.0", + "dependencies": { + "@types/pegjs": "^0.10.0", + "big-integer": "^1.6.48" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/nodemailer": { "version": "6.10.1", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", @@ -24890,20 +24458,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/rpc-request": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/rpc-request/-/rpc-request-9.0.0.tgz", - "integrity": "sha512-umPKR8Ymue35XIQH7SQTKxlZnqoDAZNI/2layPfP/G/Z5OGmseignevpUPCvdW4FkYY8FmVMr1tqgmb4jKFE2g==", - "license": "MIT", - "engines": { - "node": ">=22.12.0", - "npm": ">=10.9.0" - }, - "funding": { - "type": "Coinbase Commerce", - "url": "https://commerce.coinbase.com/checkout/3ad2d84d-8417-4f33-bfbb-64d0239d4309" - } - }, "node_modules/rpc-websockets": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-9.2.0.tgz", @@ -26255,6 +25809,7 @@ "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">= 8" @@ -26293,6 +25848,8 @@ "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "memory-pager": "^1.0.2" } @@ -27242,6 +26799,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -27379,6 +26937,7 @@ "version": "5.44.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -27397,6 +26956,7 @@ "version": "5.3.14", "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", @@ -27431,6 +26991,7 @@ "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -27445,6 +27006,7 @@ "version": "4.3.3", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", @@ -27464,6 +27026,7 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -27479,6 +27042,7 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, "license": "MIT" }, "node_modules/test-exclude": { @@ -27684,15 +27248,6 @@ ], "license": "MIT" }, - "node_modules/to-fast-properties": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", - "integrity": "sha512-lxrWP8ejsq+7E3nNjwYmUBMAgjMTZoTI+sdBOpvNyijeDLa29LUn9QaoXAHv4+Z578hbmHHJKZknzxVtvo77og==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -27726,12 +27281,6 @@ "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==", "license": "MIT" }, - "node_modules/toml": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", - "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", - "license": "MIT" - }, "node_modules/tough-cookie": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", @@ -28071,6 +27620,7 @@ "version": "9.5.4", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.4.tgz", "integrity": "sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ==", + "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.1.0", @@ -28968,6 +28518,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "dev": true, "funding": [ { "type": "opencollective", @@ -29241,6 +28792,7 @@ "version": "2.4.4", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "dev": true, "license": "MIT", "dependencies": { "glob-to-regexp": "^0.4.1", @@ -30003,6 +29555,7 @@ "version": "5.102.1", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz", "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", + "dev": true, "license": "MIT", "peer": true, "dependencies": { @@ -30062,6 +29615,7 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "dev": true, "license": "MIT", "engines": { "node": ">=10.13.0" @@ -30071,6 +29625,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, "license": "MIT", "peer": true, "engines": { @@ -30081,6 +29636,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, "license": "MIT", "peer": true, "dependencies": { @@ -30094,6 +29650,7 @@ "version": "4.3.3", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, "license": "MIT", "peer": true, "dependencies": { diff --git a/package.json b/package.json index 6327ae6531..f642a71f00 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,7 @@ "nestjs-i18n": "^10.5.1", "nestjs-real-ip": "^2.2.0", "node-2fa": "^2.0.3", + "node-sql-parser": "^5.3.13", "nodemailer": "^6.10.1", "passport": "^0.6.0", "passport-jwt": "^4.0.1", diff --git a/src/shared/auth/role.guard.ts b/src/shared/auth/role.guard.ts index 5830a2a98a..8bdaf9a6b4 100644 --- a/src/shared/auth/role.guard.ts +++ b/src/shared/auth/role.guard.ts @@ -19,6 +19,7 @@ class RoleGuardClass implements CanActivate { [UserRole.COMPLIANCE]: [UserRole.ADMIN, UserRole.SUPER_ADMIN], [UserRole.BANKING_BOT]: [UserRole.ADMIN, UserRole.SUPER_ADMIN], [UserRole.ADMIN]: [UserRole.SUPER_ADMIN], + [UserRole.DEBUG]: [UserRole.ADMIN, UserRole.SUPER_ADMIN], }; constructor(private readonly entryRole: UserRole) {} diff --git a/src/shared/auth/user-role.enum.ts b/src/shared/auth/user-role.enum.ts index c56210a77f..db5c9b4dea 100644 --- a/src/shared/auth/user-role.enum.ts +++ b/src/shared/auth/user-role.enum.ts @@ -9,6 +9,7 @@ export enum UserRole { SUPPORT = 'Support', COMPLIANCE = 'Compliance', CUSTODY = 'Custody', + DEBUG = 'Debug', // service roles BANKING_BOT = 'BankingBot', diff --git a/src/subdomains/generic/gs/dto/debug-query.dto.ts b/src/subdomains/generic/gs/dto/debug-query.dto.ts new file mode 100644 index 0000000000..9385b762de --- /dev/null +++ b/src/subdomains/generic/gs/dto/debug-query.dto.ts @@ -0,0 +1,8 @@ +import { IsNotEmpty, IsString, MaxLength } from 'class-validator'; + +export class DebugQueryDto { + @IsNotEmpty() + @IsString() + @MaxLength(10000) + sql: string; +} diff --git a/src/subdomains/generic/gs/gs.controller.ts b/src/subdomains/generic/gs/gs.controller.ts index 6633bc6d64..3e3daf603a 100644 --- a/src/subdomains/generic/gs/gs.controller.ts +++ b/src/subdomains/generic/gs/gs.controller.ts @@ -8,6 +8,7 @@ import { UserActiveGuard } from 'src/shared/auth/user-active.guard'; import { UserRole } from 'src/shared/auth/user-role.enum'; import { DfxLogger } from 'src/shared/services/dfx-logger'; import { DbQueryBaseDto, DbQueryDto, DbReturnData } from './dto/db-query.dto'; +import { DebugQueryDto } from './dto/debug-query.dto'; import { SupportDataQuery, SupportReturnData } from './dto/support-data.dto'; import { GsService } from './gs.service'; @@ -45,4 +46,17 @@ export class GsController { async getSupportData(@Query() query: SupportDataQuery): Promise { return this.gsService.getSupportData(query); } + + @Post('debug') + @ApiBearerAuth() + @ApiExcludeEndpoint() + @UseGuards(AuthGuard(), RoleGuard(UserRole.DEBUG), UserActiveGuard()) + async executeDebugQuery(@GetJwt() jwt: JwtPayload, @Body() dto: DebugQueryDto): Promise { + try { + return await this.gsService.executeDebugQuery(dto.sql, jwt.address ?? `account:${jwt.account}`); + } catch (e) { + this.logger.verbose(`Debug query failed:`, e); + throw new BadRequestException(e.message); + } + } } diff --git a/src/subdomains/generic/gs/gs.service.ts b/src/subdomains/generic/gs/gs.service.ts index f6a973b462..45d131799e 100644 --- a/src/subdomains/generic/gs/gs.service.ts +++ b/src/subdomains/generic/gs/gs.service.ts @@ -1,4 +1,5 @@ import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; +import { Parser } from 'node-sql-parser'; import { UserRole } from 'src/shared/auth/user-role.enum'; import { DfxLogger } from 'src/shared/services/dfx-logger'; import { Util } from 'src/shared/utils/util'; @@ -54,6 +55,49 @@ export class GsService { }; private readonly RestrictedMarker = '[RESTRICTED]'; + private readonly sqlParser = new Parser(); + + // columns blocked for debug queries (personal data) + private readonly DebugBlockedColumns = [ + // contact + 'mail', + 'email', + 'recipientMail', + 'phone', + // personal names (specific patterns to avoid blocking assetName, dexName, etc.) + 'firstname', + 'surname', + 'verifiedName', + 'birthname', + 'organizationName', + 'allBeneficialOwnersName', + 'bankAccountName', + 'cardName', + 'ultimateName', + // address + 'street', + 'houseNumber', + 'zip', + // identity + 'birthday', + 'tin', + 'identDocumentId', + 'identDocumentType', + // financial + 'iban', + 'accountNumber', + // network/security + 'ip', + 'ipCountry', + 'apiKey', + 'apiKeyCT', + 'secret', + 'password', + 'signature', + ]; + + private readonly DebugMaxResults = 10000; + constructor( private readonly userDataService: UserDataService, private readonly userService: UserService, @@ -196,6 +240,75 @@ export class GsService { }; } + async executeDebugQuery(sql: string, userMail: string): Promise { + // 1. Parse SQL to AST for robust validation + let ast; + try { + ast = this.sqlParser.astify(sql, { database: 'TransactSQL' }); + } catch (e) { + throw new BadRequestException('Invalid SQL syntax'); + } + + // 2. Only single SELECT statements allowed (array means multiple statements) + const statements = Array.isArray(ast) ? ast : [ast]; + if (statements.length !== 1) { + throw new BadRequestException('Only single statements allowed'); + } + + const stmt = statements[0]; + if (stmt.type !== 'select') { + throw new BadRequestException('Only SELECT queries allowed'); + } + + // 3. No UNION/INTERSECT/EXCEPT queries (these have _next property) + if (stmt._next) { + throw new BadRequestException('UNION/INTERSECT/EXCEPT queries not allowed'); + } + + // 4. No SELECT INTO (creates tables - write operation!) + if (stmt.into?.type === 'into' || stmt.into?.expr) { + throw new BadRequestException('SELECT INTO not allowed'); + } + + // 5. No dangerous functions in FROM clause (external connections) + this.checkForDangerousFunctions(stmt); + + // 6. No FOR XML/JSON (data exfiltration) + const normalizedLower = sql.toLowerCase(); + if (normalizedLower.includes(' for xml') || normalizedLower.includes(' for json')) { + throw new BadRequestException('FOR XML/JSON not allowed'); + } + + // 7. Check for blocked columns BEFORE execution (prevents alias bypass) + const blockedColumn = this.findBlockedColumnInQuery(sql); + if (blockedColumn) { + throw new BadRequestException(`Access to column '${blockedColumn}' is not allowed`); + } + + // 8. Validate TOP value if present + const topMatch = normalizedLower.match(/\btop\s+(\d+)/i); + if (topMatch && parseInt(topMatch[1]) > this.DebugMaxResults) { + throw new BadRequestException(`TOP value exceeds maximum of ${this.DebugMaxResults}`); + } + + // 9. Log query for audit trail + this.logger.info(`Debug query by ${userMail}: ${sql.substring(0, 500)}${sql.length > 500 ? '...' : ''}`); + + // 10. Execute query with result limit + try { + const limitedSql = this.ensureResultLimit(sql); + const result = await this.dataSource.query(limitedSql); + + // 11. Additional masking for any columns that might have slipped through + this.maskDebugBlockedColumns(result); + + return result; + } catch (e) { + this.logger.warn(`Debug query by ${userMail} failed: ${e.message}`); + throw new BadRequestException('Query execution failed'); + } + } + //*** HELPER METHODS ***// private setJsonData(data: any[], selects: string[]): void { @@ -494,4 +607,90 @@ export class GsService { } } } + + private maskDebugBlockedColumns(data: Record[]): void { + if (!data?.length) return; + + for (const entry of data) { + for (const key of Object.keys(entry)) { + if (this.isDebugBlockedColumn(key)) { + entry[key] = this.RestrictedMarker; + } + } + } + } + + private isDebugBlockedColumn(columnName: string): boolean { + const lowerKey = columnName.toLowerCase(); + // Match exact column name or prefixed (e.g., "firstname" or "user_firstname") + return this.DebugBlockedColumns.some((blocked) => { + const lowerBlocked = blocked.toLowerCase(); + return lowerKey === lowerBlocked || lowerKey.endsWith('_' + lowerBlocked); + }); + } + + private findBlockedColumnInQuery(sql: string): string | null { + try { + // columnList returns: ['select::table::column', 'select::null::column', ...] + const columns = this.sqlParser.columnList(sql, { database: 'TransactSQL' }); + + for (const col of columns) { + // Format: 'operation::schema::column' - extract the column part + const parts = col.split('::'); + const columnName = parts[parts.length - 1]; + + // Skip wildcard + if (columnName === '*' || columnName === '(.*)') continue; + + if (this.isDebugBlockedColumn(columnName)) { + return columnName; + } + } + + return null; + } catch { + // If column extraction fails, let the query proceed (will be caught by result masking) + return null; + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private checkForDangerousFunctions(stmt: any): void { + const dangerousFunctions = ['openrowset', 'openquery', 'opendatasource', 'openxml']; + + const checkFromClause = (from: any[]): void => { + if (!from) return; + + for (const item of from) { + // Check if FROM contains a function call + if (item.type === 'expr' && item.expr?.type === 'function') { + const funcName = item.expr.name?.name?.[0]?.value?.toLowerCase(); + if (funcName && dangerousFunctions.includes(funcName)) { + throw new BadRequestException(`Function '${funcName.toUpperCase()}' not allowed`); + } + } + // Recursively check subqueries in FROM + if (item.expr?.ast) { + this.checkForDangerousFunctions(item.expr.ast); + } + } + }; + + checkFromClause(stmt.from); + } + + private ensureResultLimit(sql: string): string { + const normalized = sql.trim().toLowerCase(); + + // Check if query already has a LIMIT/TOP clause + if (normalized.includes(' top ') || /\blimit\s+\d+/i.test(sql)) { + return sql; + } + + // MSSQL requires ORDER BY for OFFSET/FETCH - add dummy order if missing + const hasOrderBy = /\border\s+by\b/i.test(sql); + const orderByClause = hasOrderBy ? '' : ' ORDER BY (SELECT NULL)'; + + return `${sql.trim().replace(/;*$/, '')}${orderByClause} OFFSET 0 ROWS FETCH NEXT ${this.DebugMaxResults} ROWS ONLY`; + } } From 9e2baadaaeed936b2bbb228132b396df6ec00aa6 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Wed, 31 Dec 2025 13:40:52 +0100 Subject: [PATCH 2/9] fix(gs): resolve eslint warnings in debug endpoint - Remove unused catch variable (use bare catch) - Remove unnecessary eslint-disable directive --- src/subdomains/generic/gs/gs.service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/subdomains/generic/gs/gs.service.ts b/src/subdomains/generic/gs/gs.service.ts index 45d131799e..cba464025e 100644 --- a/src/subdomains/generic/gs/gs.service.ts +++ b/src/subdomains/generic/gs/gs.service.ts @@ -245,7 +245,7 @@ export class GsService { let ast; try { ast = this.sqlParser.astify(sql, { database: 'TransactSQL' }); - } catch (e) { + } catch { throw new BadRequestException('Invalid SQL syntax'); } @@ -654,7 +654,6 @@ export class GsService { } } - // eslint-disable-next-line @typescript-eslint/no-explicit-any private checkForDangerousFunctions(stmt: any): void { const dangerousFunctions = ['openrowset', 'openquery', 'opendatasource', 'openxml']; From a2a3cc15ae55c22ad40712265a1e8ce907c0268b Mon Sep 17 00:00:00 2001 From: Yannick1712 <52333989+Yannick1712@users.noreply.github.com> Date: Wed, 31 Dec 2025 12:27:42 +0100 Subject: [PATCH 3/9] [NOTASK] add more blockedCols --- src/subdomains/generic/gs/gs.service.ts | 37 +++++++++++++++++++------ 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/src/subdomains/generic/gs/gs.service.ts b/src/subdomains/generic/gs/gs.service.ts index cba464025e..09337e3b58 100644 --- a/src/subdomains/generic/gs/gs.service.ts +++ b/src/subdomains/generic/gs/gs.service.ts @@ -52,6 +52,9 @@ export class GsService { // columns only visible to SUPER_ADMIN private readonly RestrictedColumns: Record = { asset: ['ikna'], + organization: ['name'], + bank_tx: ['name'], + kyc_step: ['result'], }; private readonly RestrictedMarker = '[RESTRICTED]'; @@ -59,25 +62,35 @@ export class GsService { // columns blocked for debug queries (personal data) private readonly DebugBlockedColumns = [ - // contact 'mail', - 'email', 'recipientMail', 'phone', - // personal names (specific patterns to avoid blocking assetName, dexName, etc.) 'firstname', 'surname', 'verifiedName', - 'birthname', 'organizationName', + 'organizationStreet', + 'organizationLocation', + 'organizationZip', + 'organizationCountryId', + 'organizationId', 'allBeneficialOwnersName', - 'bankAccountName', + 'allBeneficialOwnersDomicile', + 'accountOpenerAuthorization', + 'complexOrgStructure', + 'accountOpener', + 'legalEntity', + 'signatoryPower', 'cardName', 'ultimateName', // address 'street', 'houseNumber', + 'location', 'zip', + 'countryId', + 'verifiedCountryId', + 'nationalityId', // identity 'birthday', 'tin', @@ -91,9 +104,15 @@ export class GsService { 'ipCountry', 'apiKey', 'apiKeyCT', - 'secret', - 'password', 'signature', + 'kycHash', + 'kycFileId', + 'internalAmlNote', + 'blackSquadRecipientMail', + 'individualFees', + 'totpSecret', + 'paymentLinksConfig', + 'paymentLinksName', ]; private readonly DebugMaxResults = 10000; @@ -690,6 +709,8 @@ export class GsService { const hasOrderBy = /\border\s+by\b/i.test(sql); const orderByClause = hasOrderBy ? '' : ' ORDER BY (SELECT NULL)'; - return `${sql.trim().replace(/;*$/, '')}${orderByClause} OFFSET 0 ROWS FETCH NEXT ${this.DebugMaxResults} ROWS ONLY`; + return `${sql.trim().replace(/;*$/, '')}${orderByClause} OFFSET 0 ROWS FETCH NEXT ${ + this.DebugMaxResults + } ROWS ONLY`; } } From 07e241d6af1036e6e675e525d117281f45eed11d Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Wed, 31 Dec 2025 14:06:57 +0100 Subject: [PATCH 4/9] fix(gs): move restricted columns from ADMIN to DEBUG blocking - Remove organization.name, bank_tx.name, kyc_step.result from RestrictedColumns - Add 'name' and 'result' to DebugBlockedColumns - ADMIN can now see these columns on /gs/db - DEBUG role has these blocked on /gs/debug --- src/subdomains/generic/gs/gs.service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/subdomains/generic/gs/gs.service.ts b/src/subdomains/generic/gs/gs.service.ts index 09337e3b58..3c6e46277f 100644 --- a/src/subdomains/generic/gs/gs.service.ts +++ b/src/subdomains/generic/gs/gs.service.ts @@ -52,9 +52,6 @@ export class GsService { // columns only visible to SUPER_ADMIN private readonly RestrictedColumns: Record = { asset: ['ikna'], - organization: ['name'], - bank_tx: ['name'], - kyc_step: ['result'], }; private readonly RestrictedMarker = '[RESTRICTED]'; @@ -62,6 +59,9 @@ export class GsService { // columns blocked for debug queries (personal data) private readonly DebugBlockedColumns = [ + // restricted (for DEBUG only, ADMIN can see via /gs/db) + 'name', + 'result', 'mail', 'recipientMail', 'phone', From b28dab1ece7ec5bde710abd9ee4310f37f8f37db Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Fri, 2 Jan 2026 08:52:52 +0100 Subject: [PATCH 5/9] fix(gs): implement table-specific column blocking for debug endpoint (#2782) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(gs): implement table-specific column blocking for debug endpoint Replace generic DebugBlockedColumns list with table-specific blocking: - TableBlockedColumns: Record maps each table to its blocked columns - Pre-execution: Check columns against their specific tables - Post-execution for SELECT *: Mask columns from all query tables Examples: - SELECT name FROM asset → ALLOWED (asset has no blocked columns) - SELECT name FROM bank_tx → BLOCKED (bank_tx.name contains personal data) - SELECT * FROM bank_tx → name masked post-execution Tables with blocked columns: - user_data: mail, phone, firstname, surname, etc. - bank_tx: name, iban, addressLine1, etc. - bank_data: name, iban, label, comment - kyc_step: result (contains names, birthday) - organization: name, allBeneficialOwnersName, etc. * fix(gs): always run post-execution masking for defense in depth The previous implementation only masked post-execution for SELECT * queries. This was a security risk: if pre-execution column extraction failed (catch block), non-wildcard queries would not be masked. Now post-execution masking always runs, ensuring blocked columns are masked even if the SQL parser fails to detect them pre-execution. * fix(gs): add missing blocked columns from original list Add columns that were in the original DebugBlockedColumns but missing in the new table-specific structure: - user_data: countryId, verifiedCountryId, nationalityId - user: signature - fiat_output: accountNumber - checkout_tx: cardName (new table) - bank_account: accountNumber (new table) * fix(gs): add missing tables with sensitive columns Add tables that were missed when converting from global DebugBlockedColumns to table-specific TableBlockedColumns: - ref: ip (user IP for referral tracking) - ip_log: ip, country (user IP logging) - checkout_tx: ip (user IP during checkout, cardName already present) - buy: iban (user IBAN for buy routes) - deposit_route: iban (user IBAN for sell routes via Single Table Inheritance) These columns were blocked globally in the original implementation but were not added to all relevant tables in the table-specific version. * fix(gs): add additional sensitive columns found in codebase review Add missing blocked columns discovered during comprehensive entity scan: - buy_crypto: chargebackIban (user IBAN for refunds) - kyc_log: ipAddress (TfaLog), result (KYC data) - bank_tx_return: chargebackIban, recipientMail, chargebackRemittanceInfo - bank_tx_repeat: chargebackIban, chargebackRemittanceInfo - limit_request: recipientMail - ref_reward: recipientMail * fix(gs): add additional sensitive columns from codebase review Extend existing tables with missing blocked columns: - checkout_tx: cardBin, cardLast4, cardFingerPrint, cardIssuer, cardIssuerCountry, raw - buy_crypto: chargebackRemittanceInfo, siftResponse - buy_fiat: remittanceInfo, usedBank, info - crypto_input: senderAddresses - user_data: relatedUsers - limit_request: fundOriginText - bank_tx_return: info Add new tables with sensitive columns: - transaction_risk_assessment: reason, methods, summary, result (AML/KYC assessments) - support_issue: name, information (support tickets with user data) - support_message: message, fileUrl (message content and files) - sift_error_log: requestPayload (Sift API requests with PII) * fix(gs): add webhook.data to blocked columns * fix(gs): add notification.data to blocked columns * fix(gs): add kyc_step.data to blocked columns --- src/subdomains/generic/gs/gs.service.ts | 254 ++++++++++++++++-------- 1 file changed, 176 insertions(+), 78 deletions(-) diff --git a/src/subdomains/generic/gs/gs.service.ts b/src/subdomains/generic/gs/gs.service.ts index 3c6e46277f..b0381c77ac 100644 --- a/src/subdomains/generic/gs/gs.service.ts +++ b/src/subdomains/generic/gs/gs.service.ts @@ -57,63 +57,97 @@ export class GsService { private readonly sqlParser = new Parser(); - // columns blocked for debug queries (personal data) - private readonly DebugBlockedColumns = [ - // restricted (for DEBUG only, ADMIN can see via /gs/db) - 'name', - 'result', - 'mail', - 'recipientMail', - 'phone', - 'firstname', - 'surname', - 'verifiedName', - 'organizationName', - 'organizationStreet', - 'organizationLocation', - 'organizationZip', - 'organizationCountryId', - 'organizationId', - 'allBeneficialOwnersName', - 'allBeneficialOwnersDomicile', - 'accountOpenerAuthorization', - 'complexOrgStructure', - 'accountOpener', - 'legalEntity', - 'signatoryPower', - 'cardName', - 'ultimateName', - // address - 'street', - 'houseNumber', - 'location', - 'zip', - 'countryId', - 'verifiedCountryId', - 'nationalityId', - // identity - 'birthday', - 'tin', - 'identDocumentId', - 'identDocumentType', - // financial - 'iban', - 'accountNumber', - // network/security - 'ip', - 'ipCountry', - 'apiKey', - 'apiKeyCT', - 'signature', - 'kycHash', - 'kycFileId', - 'internalAmlNote', - 'blackSquadRecipientMail', - 'individualFees', - 'totpSecret', - 'paymentLinksConfig', - 'paymentLinksName', - ]; + // Table-specific blocked columns for debug queries (personal data) + private readonly TableBlockedColumns: Record = { + // user_data - main table with PII + user_data: [ + 'mail', 'phone', 'firstname', 'surname', 'verifiedName', + 'street', 'houseNumber', 'location', 'zip', + 'countryId', 'verifiedCountryId', 'nationalityId', // Foreign keys to country + 'birthday', 'tin', 'identDocumentId', 'identDocumentType', + 'organizationName', 'organizationStreet', 'organizationLocation', 'organizationZip', + 'organizationCountryId', 'organizationId', + 'allBeneficialOwnersName', 'allBeneficialOwnersDomicile', + 'accountOpenerAuthorization', 'complexOrgStructure', 'accountOpener', 'legalEntity', 'signatoryPower', + 'kycHash', 'kycFileId', 'apiKeyCT', 'totpSecret', + 'internalAmlNote', 'blackSquadRecipientMail', 'individualFees', + 'paymentLinksConfig', 'paymentLinksName', 'comment', 'relatedUsers', + ], + // user + user: ['ip', 'ipCountry', 'apiKeyCT', 'signature', 'label', 'comment'], + // bank_tx - bank transactions + bank_tx: [ + 'name', 'ultimateName', 'iban', 'accountIban', 'senderAccount', 'bic', + 'addressLine1', 'addressLine2', 'ultimateAddressLine1', 'ultimateAddressLine2', + 'bankAddressLine1', 'bankAddressLine2', + 'remittanceInfo', 'txInfo', 'txRaw', + ], + // bank_data + bank_data: ['name', 'iban', 'label', 'comment'], + // fiat_output + fiat_output: [ + 'name', 'iban', 'accountIban', 'accountNumber', 'bic', 'aba', + 'address', 'houseNumber', 'zip', 'city', + 'remittanceInfo', + ], + // checkout_tx - payment card data + checkout_tx: [ + 'cardName', 'ip', + 'cardBin', 'cardLast4', 'cardFingerPrint', 'cardIssuer', 'cardIssuerCountry', 'raw', + ], + // bank_account + bank_account: ['accountNumber'], + // virtual_iban + virtual_iban: ['iban', 'bban', 'label'], + // kyc_step - KYC steps (result/data contains names, birthday, document number) + kyc_step: ['result', 'comment', 'data'], + // kyc_file + kyc_file: ['name'], + // kyc_log (includes TfaLog ChildEntity with ipAddress) + kyc_log: ['comment', 'ipAddress', 'result'], + // organization + organization: [ + 'name', 'street', 'houseNumber', 'location', 'zip', + 'allBeneficialOwnersName', 'allBeneficialOwnersDomicile', + ], + // transactions + buy_crypto: ['recipientMail', 'comment', 'chargebackIban', 'chargebackRemittanceInfo', 'siftResponse'], + buy_fiat: ['recipientMail', 'comment', 'remittanceInfo', 'usedBank', 'info'], + transaction: ['recipientMail'], + crypto_input: ['recipientMail', 'senderAddresses'], + // payment_link + payment_link: ['comment', 'label'], + // wallet (integration) + wallet: ['apiKey'], + // ref - referral tracking + ref: ['ip'], + // ip_log - IP logging + ip_log: ['ip', 'country'], + // buy - buy crypto routes + buy: ['iban'], + // deposit_route - sell routes (Single Table Inheritance for Sell entity) + deposit_route: ['iban'], + // bank_tx_return - chargeback returns + bank_tx_return: ['chargebackIban', 'recipientMail', 'chargebackRemittanceInfo', 'info'], + // bank_tx_repeat - repeat transactions + bank_tx_repeat: ['chargebackIban', 'chargebackRemittanceInfo'], + // limit_request - limit increase requests + limit_request: ['recipientMail', 'fundOriginText'], + // ref_reward - referral rewards + ref_reward: ['recipientMail'], + // transaction_risk_assessment - AML/KYC assessments + transaction_risk_assessment: ['reason', 'methods', 'summary', 'result'], + // support_issue - support tickets with user data + support_issue: ['name', 'information'], + // support_message - message content and file URLs + support_message: ['message', 'fileUrl'], + // sift_error_log - Sift API request payloads containing PII + sift_error_log: ['requestPayload'], + // webhook - serialized user/transaction data + webhook: ['data'], + // notification - notification payloads with user data + notification: ['data'], + }; private readonly DebugMaxResults = 10000; @@ -299,7 +333,7 @@ export class GsService { } // 7. Check for blocked columns BEFORE execution (prevents alias bypass) - const blockedColumn = this.findBlockedColumnInQuery(sql); + const blockedColumn = this.findBlockedColumnInQuery(sql, stmt); if (blockedColumn) { throw new BadRequestException(`Access to column '${blockedColumn}' is not allowed`); } @@ -310,16 +344,19 @@ export class GsService { throw new BadRequestException(`TOP value exceeds maximum of ${this.DebugMaxResults}`); } - // 9. Log query for audit trail + // 9. Get tables from query for post-execution masking + const tables = this.getTablesFromQuery(sql); + + // 10. Log query for audit trail this.logger.info(`Debug query by ${userMail}: ${sql.substring(0, 500)}${sql.length > 500 ? '...' : ''}`); - // 10. Execute query with result limit + // 11. Execute query with result limit try { const limitedSql = this.ensureResultLimit(sql); const result = await this.dataSource.query(limitedSql); - // 11. Additional masking for any columns that might have slipped through - this.maskDebugBlockedColumns(result); + // 12. Post-execution masking (defense in depth - also catches pre-execution failures) + this.maskDebugBlockedColumns(result, tables); return result; } catch (e) { @@ -627,42 +664,103 @@ export class GsService { } } - private maskDebugBlockedColumns(data: Record[]): void { - if (!data?.length) return; + private maskDebugBlockedColumns(data: Record[], tables: string[]): void { + if (!data?.length || !tables?.length) return; + + // Collect all blocked columns from all tables in the query + const blockedColumns = new Set(); + for (const table of tables) { + const tableCols = this.TableBlockedColumns[table]; + if (tableCols) { + for (const col of tableCols) { + blockedColumns.add(col.toLowerCase()); + } + } + } + + if (blockedColumns.size === 0) return; for (const entry of data) { for (const key of Object.keys(entry)) { - if (this.isDebugBlockedColumn(key)) { + if (this.shouldMaskDebugColumn(key, blockedColumns)) { entry[key] = this.RestrictedMarker; } } } } - private isDebugBlockedColumn(columnName: string): boolean { - const lowerKey = columnName.toLowerCase(); - // Match exact column name or prefixed (e.g., "firstname" or "user_firstname") - return this.DebugBlockedColumns.some((blocked) => { - const lowerBlocked = blocked.toLowerCase(); - return lowerKey === lowerBlocked || lowerKey.endsWith('_' + lowerBlocked); - }); + private shouldMaskDebugColumn(columnName: string, blockedColumns: Set): boolean { + const lower = columnName.toLowerCase(); + + // Check exact match or with table prefix (e.g., "name" or "bank_tx_name") + for (const blocked of blockedColumns) { + if (lower === blocked || lower.endsWith('_' + blocked)) { + return true; + } + } + return false; + } + + private getTablesFromQuery(sql: string): string[] { + const tableList = this.sqlParser.tableList(sql, { database: 'TransactSQL' }); + // Format: 'select::null::table_name' → extract table_name + return tableList.map((t) => t.split('::')[2]).filter(Boolean); + } + + private getAliasToTableMap(ast: any): Map { + const map = new Map(); + if (!ast.from) return map; + + for (const item of ast.from) { + if (item.table) { + map.set(item.as || item.table, item.table); + } + } + return map; + } + + private isColumnBlockedInTable(columnName: string, table: string | null, allTables: string[]): boolean { + const lower = columnName.toLowerCase(); + + if (table) { + // Explicit table known → check if this column is blocked in this table + const blockedCols = this.TableBlockedColumns[table]; + return blockedCols?.some((b) => b.toLowerCase() === lower) ?? false; + } else { + // No explicit table → if ANY of the query tables blocks this column, block it + return allTables.some((t) => { + const blockedCols = this.TableBlockedColumns[t]; + return blockedCols?.some((b) => b.toLowerCase() === lower) ?? false; + }); + } } - private findBlockedColumnInQuery(sql: string): string | null { + private findBlockedColumnInQuery(sql: string, ast: any): string | null { try { // columnList returns: ['select::table::column', 'select::null::column', ...] const columns = this.sqlParser.columnList(sql, { database: 'TransactSQL' }); + const tables = this.getTablesFromQuery(sql); + const aliasMap = this.getAliasToTableMap(ast); for (const col of columns) { - // Format: 'operation::schema::column' - extract the column part const parts = col.split('::'); - const columnName = parts[parts.length - 1]; + const tableOrAlias = parts[1]; // can be 'null' + const columnName = parts[2]; - // Skip wildcard + // Skip wildcard - handled post-execution if (columnName === '*' || columnName === '(.*)') continue; - if (this.isDebugBlockedColumn(columnName)) { - return columnName; + // Resolve table from alias + const resolvedTable = + tableOrAlias === 'null' + ? tables.length === 1 + ? tables[0] + : null // Single table without alias → use that table + : aliasMap.get(tableOrAlias) || tableOrAlias; + + // Check if column is blocked in this table + if (this.isColumnBlockedInTable(columnName, resolvedTable, tables)) { + return `${resolvedTable || 'unknown'}.${columnName}`; } } From abec1b0efd8c73cbf029edbfaff34ef4e28b991a Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Fri, 2 Jan 2026 13:12:39 +0100 Subject: [PATCH 6/9] feat(gs): add App Insights log query endpoint with template-based security (#2778) Add POST /gs/debug/logs endpoint for querying Azure Application Insights logs using predefined, safe KQL templates. Security features: - Template-based queries only (no free-form KQL input) - Strict parameter validation via class-validator (GUID, alphanumeric) - All KQL-relevant special characters blocked in user input - Defense-in-depth string escaping - Result limits per template (200-500 rows) - Full audit logging of queries Available templates: - traces-by-operation: Traces for specific operation ID - traces-by-message: Traces filtered by message pattern - exceptions-recent: Recent exceptions - request-failures: Failed HTTP requests - dependencies-slow: Slow external dependencies (by duration threshold) - custom-events: Custom events by name Infrastructure: - AppInsightsQueryService: OAuth2 client with token caching - Proper error handling and logging - Mock responses for LOC mode Requires UserRole.DEBUG and APPINSIGHTS_APP_ID env variable. --- src/config/config.ts | 3 + .../app-insights-query.service.ts | 81 ++++ src/integration/integration.module.ts | 4 +- src/shared/services/http.service.ts | 2 + .../generic/gs/dto/log-query.dto.ts | 47 +++ src/subdomains/generic/gs/gs.controller.ts | 14 + src/subdomains/generic/gs/gs.module.ts | 2 + src/subdomains/generic/gs/gs.service.ts | 370 +++++++++--------- 8 files changed, 346 insertions(+), 177 deletions(-) create mode 100644 src/integration/infrastructure/app-insights-query.service.ts create mode 100644 src/subdomains/generic/gs/dto/log-query.dto.ts diff --git a/src/config/config.ts b/src/config/config.ts index d49bbf3562..df1bf182d1 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -1005,6 +1005,9 @@ export class Configuration { ?.replace('BlobEndpoint=', ''), connectionString: process.env.AZURE_STORAGE_CONNECTION_STRING, }, + appInsights: { + appId: process.env.APPINSIGHTS_APP_ID, + }, }; alby = { diff --git a/src/integration/infrastructure/app-insights-query.service.ts b/src/integration/infrastructure/app-insights-query.service.ts new file mode 100644 index 0000000000..f30cf01afc --- /dev/null +++ b/src/integration/infrastructure/app-insights-query.service.ts @@ -0,0 +1,81 @@ +import { Injectable } from '@nestjs/common'; +import { Config } from 'src/config/config'; +import { DfxLogger } from 'src/shared/services/dfx-logger'; +import { HttpService } from 'src/shared/services/http.service'; + +interface AppInsightsQueryResponse { + tables: { + name: string; + columns: { name: string; type: string }[]; + rows: unknown[][]; + }[]; +} + +@Injectable() +export class AppInsightsQueryService { + private readonly logger = new DfxLogger(AppInsightsQueryService); + + private readonly baseUrl = 'https://api.applicationinsights.io/v1'; + private readonly TOKEN_REFRESH_BUFFER_MS = 60000; + + private accessToken: string | null = null; + private tokenExpiresAt = 0; + + constructor(private readonly http: HttpService) {} + + async query(kql: string, timespan?: string): Promise { + const appId = Config.azure.appInsights?.appId; + if (!appId) { + throw new Error('App Insights App ID not configured'); + } + + const body: { query: string; timespan?: string } = { query: kql }; + if (timespan) body.timespan = timespan; + + return this.request(`apps/${appId}/query`, body); + } + + private async request(url: string, body: object, nthTry = 3): Promise { + try { + if (!this.accessToken || Date.now() >= this.tokenExpiresAt - this.TOKEN_REFRESH_BUFFER_MS) { + await this.refreshAccessToken(); + } + + return await this.http.request({ + url: `${this.baseUrl}/${url}`, + method: 'POST', + data: body, + headers: { + Authorization: `Bearer ${this.accessToken}`, + 'Content-Type': 'application/json', + }, + }); + } catch (e) { + if (nthTry > 1 && e.response?.status === 401) { + await this.refreshAccessToken(); + return this.request(url, body, nthTry - 1); + } + throw e; + } + } + + private async refreshAccessToken(): Promise { + try { + const { access_token, expires_in } = await this.http.post<{ access_token: string; expires_in: number }>( + `https://login.microsoftonline.com/${Config.azure.tenantId}/oauth2/token`, + new URLSearchParams({ + grant_type: 'client_credentials', + client_id: Config.azure.clientId, + client_secret: Config.azure.clientSecret, + resource: 'https://api.applicationinsights.io', + }), + ); + + this.accessToken = access_token; + this.tokenExpiresAt = Date.now() + expires_in * 1000; + } catch (e) { + this.logger.error('Failed to refresh App Insights access token', e); + throw new Error('Failed to authenticate with App Insights'); + } + } +} diff --git a/src/integration/integration.module.ts b/src/integration/integration.module.ts index 83ddd1fe91..549bdb7957 100644 --- a/src/integration/integration.module.ts +++ b/src/integration/integration.module.ts @@ -5,6 +5,7 @@ import { BlockchainModule } from './blockchain/blockchain.module'; import { CheckoutModule } from './checkout/checkout.module'; import { ExchangeModule } from './exchange/exchange.module'; import { IknaModule } from './ikna/ikna.module'; +import { AppInsightsQueryService } from './infrastructure/app-insights-query.service'; import { AzureService } from './infrastructure/azure-service'; import { LetterModule } from './letter/letter.module'; import { SiftModule } from './sift/sift.module'; @@ -21,7 +22,7 @@ import { SiftModule } from './sift/sift.module'; SiftModule, ], controllers: [], - providers: [AzureService], + providers: [AzureService, AppInsightsQueryService], exports: [ BankIntegrationModule, BlockchainModule, @@ -30,6 +31,7 @@ import { SiftModule } from './sift/sift.module'; IknaModule, CheckoutModule, AzureService, + AppInsightsQueryService, SiftModule, ], }) diff --git a/src/shared/services/http.service.ts b/src/shared/services/http.service.ts index 7591d94613..766a02b1a1 100644 --- a/src/shared/services/http.service.ts +++ b/src/shared/services/http.service.ts @@ -38,6 +38,8 @@ const MOCK_RESPONSES: { pattern: RegExp; response: any }[] = [ bic_candidates: [{ bic: 'MOCKBIC1XXX' }], }, }, + { pattern: /login\.microsoftonline\.com/, response: { access_token: 'mock-token', expires_in: 3600 } }, + { pattern: /api\.applicationinsights\.io/, response: { tables: [{ name: 'PrimaryResult', columns: [], rows: [] }] } }, ]; @Injectable() diff --git a/src/subdomains/generic/gs/dto/log-query.dto.ts b/src/subdomains/generic/gs/dto/log-query.dto.ts new file mode 100644 index 0000000000..aad543573a --- /dev/null +++ b/src/subdomains/generic/gs/dto/log-query.dto.ts @@ -0,0 +1,47 @@ +import { IsEnum, IsInt, IsOptional, IsString, Matches, Max, Min } from 'class-validator'; + +export enum LogQueryTemplate { + TRACES_BY_OPERATION = 'traces-by-operation', + TRACES_BY_MESSAGE = 'traces-by-message', + EXCEPTIONS_RECENT = 'exceptions-recent', + REQUEST_FAILURES = 'request-failures', + DEPENDENCIES_SLOW = 'dependencies-slow', + CUSTOM_EVENTS = 'custom-events', +} + +export class LogQueryDto { + @IsEnum(LogQueryTemplate) + template: LogQueryTemplate; + + @IsOptional() + @IsString() + @Matches(/^[a-f0-9-]{36}$/i, { message: 'operationId must be a valid GUID' }) + operationId?: string; + + @IsOptional() + @IsString() + @Matches(/^[a-zA-Z0-9_\-.: ()]{1,100}$/, { message: 'messageFilter must be alphanumeric with basic punctuation (max 100 chars)' }) + messageFilter?: string; + + @IsOptional() + @IsInt() + @Min(1) + @Max(168) // max 7 days + hours?: number; + + @IsOptional() + @IsInt() + @Min(100) + @Max(5000) + durationMs?: number; + + @IsOptional() + @IsString() + @Matches(/^[a-zA-Z0-9_]{1,50}$/, { message: 'eventName must be alphanumeric' }) + eventName?: string; +} + +export class LogQueryResult { + columns: { name: string; type: string }[]; + rows: unknown[][]; +} diff --git a/src/subdomains/generic/gs/gs.controller.ts b/src/subdomains/generic/gs/gs.controller.ts index 3e3daf603a..039f6053a4 100644 --- a/src/subdomains/generic/gs/gs.controller.ts +++ b/src/subdomains/generic/gs/gs.controller.ts @@ -9,6 +9,7 @@ import { UserRole } from 'src/shared/auth/user-role.enum'; import { DfxLogger } from 'src/shared/services/dfx-logger'; import { DbQueryBaseDto, DbQueryDto, DbReturnData } from './dto/db-query.dto'; import { DebugQueryDto } from './dto/debug-query.dto'; +import { LogQueryDto, LogQueryResult } from './dto/log-query.dto'; import { SupportDataQuery, SupportReturnData } from './dto/support-data.dto'; import { GsService } from './gs.service'; @@ -59,4 +60,17 @@ export class GsController { throw new BadRequestException(e.message); } } + + @Post('debug/logs') + @ApiBearerAuth() + @ApiExcludeEndpoint() + @UseGuards(AuthGuard(), RoleGuard(UserRole.DEBUG), UserActiveGuard()) + async executeLogQuery(@GetJwt() jwt: JwtPayload, @Body() dto: LogQueryDto): Promise { + try { + return await this.gsService.executeLogQuery(dto, jwt.address ?? `account:${jwt.account}`); + } catch (e) { + this.logger.verbose(`Log query failed:`, e); + throw new BadRequestException(e.message); + } + } } diff --git a/src/subdomains/generic/gs/gs.module.ts b/src/subdomains/generic/gs/gs.module.ts index a1734fec9b..dfddb2e2b9 100644 --- a/src/subdomains/generic/gs/gs.module.ts +++ b/src/subdomains/generic/gs/gs.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; import { BlockchainModule } from 'src/integration/blockchain/blockchain.module'; +import { IntegrationModule } from 'src/integration/integration.module'; import { LetterModule } from 'src/integration/letter/letter.module'; import { SharedModule } from 'src/shared/shared.module'; import { BuyCryptoModule } from 'src/subdomains/core/buy-crypto/buy-crypto.module'; @@ -24,6 +25,7 @@ import { GsService } from './gs.service'; imports: [ SharedModule, BlockchainModule, + IntegrationModule, AddressPoolModule, ReferralModule, BuyCryptoModule, diff --git a/src/subdomains/generic/gs/gs.service.ts b/src/subdomains/generic/gs/gs.service.ts index b0381c77ac..0d635d93b3 100644 --- a/src/subdomains/generic/gs/gs.service.ts +++ b/src/subdomains/generic/gs/gs.service.ts @@ -1,5 +1,6 @@ import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { Parser } from 'node-sql-parser'; +import { AppInsightsQueryService } from 'src/integration/infrastructure/app-insights-query.service'; import { UserRole } from 'src/shared/auth/user-role.enum'; import { DfxLogger } from 'src/shared/services/dfx-logger'; import { Util } from 'src/shared/utils/util'; @@ -28,6 +29,7 @@ import { UserData } from '../user/models/user-data/user-data.entity'; import { UserDataService } from '../user/models/user-data/user-data.service'; import { UserService } from '../user/models/user/user.service'; import { DbQueryBaseDto, DbQueryDto, DbReturnData } from './dto/db-query.dto'; +import { LogQueryDto, LogQueryResult, LogQueryTemplate } from './dto/log-query.dto'; import { SupportDataQuery, SupportReturnData } from './dto/support-data.dto'; export enum SupportTable { @@ -57,101 +59,128 @@ export class GsService { private readonly sqlParser = new Parser(); - // Table-specific blocked columns for debug queries (personal data) - private readonly TableBlockedColumns: Record = { - // user_data - main table with PII - user_data: [ - 'mail', 'phone', 'firstname', 'surname', 'verifiedName', - 'street', 'houseNumber', 'location', 'zip', - 'countryId', 'verifiedCountryId', 'nationalityId', // Foreign keys to country - 'birthday', 'tin', 'identDocumentId', 'identDocumentType', - 'organizationName', 'organizationStreet', 'organizationLocation', 'organizationZip', - 'organizationCountryId', 'organizationId', - 'allBeneficialOwnersName', 'allBeneficialOwnersDomicile', - 'accountOpenerAuthorization', 'complexOrgStructure', 'accountOpener', 'legalEntity', 'signatoryPower', - 'kycHash', 'kycFileId', 'apiKeyCT', 'totpSecret', - 'internalAmlNote', 'blackSquadRecipientMail', 'individualFees', - 'paymentLinksConfig', 'paymentLinksName', 'comment', 'relatedUsers', - ], - // user - user: ['ip', 'ipCountry', 'apiKeyCT', 'signature', 'label', 'comment'], - // bank_tx - bank transactions - bank_tx: [ - 'name', 'ultimateName', 'iban', 'accountIban', 'senderAccount', 'bic', - 'addressLine1', 'addressLine2', 'ultimateAddressLine1', 'ultimateAddressLine2', - 'bankAddressLine1', 'bankAddressLine2', - 'remittanceInfo', 'txInfo', 'txRaw', - ], - // bank_data - bank_data: ['name', 'iban', 'label', 'comment'], - // fiat_output - fiat_output: [ - 'name', 'iban', 'accountIban', 'accountNumber', 'bic', 'aba', - 'address', 'houseNumber', 'zip', 'city', - 'remittanceInfo', - ], - // checkout_tx - payment card data - checkout_tx: [ - 'cardName', 'ip', - 'cardBin', 'cardLast4', 'cardFingerPrint', 'cardIssuer', 'cardIssuerCountry', 'raw', - ], - // bank_account - bank_account: ['accountNumber'], - // virtual_iban - virtual_iban: ['iban', 'bban', 'label'], - // kyc_step - KYC steps (result/data contains names, birthday, document number) - kyc_step: ['result', 'comment', 'data'], - // kyc_file - kyc_file: ['name'], - // kyc_log (includes TfaLog ChildEntity with ipAddress) - kyc_log: ['comment', 'ipAddress', 'result'], - // organization - organization: [ - 'name', 'street', 'houseNumber', 'location', 'zip', - 'allBeneficialOwnersName', 'allBeneficialOwnersDomicile', - ], - // transactions - buy_crypto: ['recipientMail', 'comment', 'chargebackIban', 'chargebackRemittanceInfo', 'siftResponse'], - buy_fiat: ['recipientMail', 'comment', 'remittanceInfo', 'usedBank', 'info'], - transaction: ['recipientMail'], - crypto_input: ['recipientMail', 'senderAddresses'], - // payment_link - payment_link: ['comment', 'label'], - // wallet (integration) - wallet: ['apiKey'], - // ref - referral tracking - ref: ['ip'], - // ip_log - IP logging - ip_log: ['ip', 'country'], - // buy - buy crypto routes - buy: ['iban'], - // deposit_route - sell routes (Single Table Inheritance for Sell entity) - deposit_route: ['iban'], - // bank_tx_return - chargeback returns - bank_tx_return: ['chargebackIban', 'recipientMail', 'chargebackRemittanceInfo', 'info'], - // bank_tx_repeat - repeat transactions - bank_tx_repeat: ['chargebackIban', 'chargebackRemittanceInfo'], - // limit_request - limit increase requests - limit_request: ['recipientMail', 'fundOriginText'], - // ref_reward - referral rewards - ref_reward: ['recipientMail'], - // transaction_risk_assessment - AML/KYC assessments - transaction_risk_assessment: ['reason', 'methods', 'summary', 'result'], - // support_issue - support tickets with user data - support_issue: ['name', 'information'], - // support_message - message content and file URLs - support_message: ['message', 'fileUrl'], - // sift_error_log - Sift API request payloads containing PII - sift_error_log: ['requestPayload'], - // webhook - serialized user/transaction data - webhook: ['data'], - // notification - notification payloads with user data - notification: ['data'], - }; + // columns blocked for debug queries (personal data) + private readonly DebugBlockedColumns = [ + // restricted (for DEBUG only, ADMIN can see via /gs/db) + 'name', + 'result', + 'mail', + 'recipientMail', + 'phone', + 'firstname', + 'surname', + 'verifiedName', + 'organizationName', + 'organizationStreet', + 'organizationLocation', + 'organizationZip', + 'organizationCountryId', + 'organizationId', + 'allBeneficialOwnersName', + 'allBeneficialOwnersDomicile', + 'accountOpenerAuthorization', + 'complexOrgStructure', + 'accountOpener', + 'legalEntity', + 'signatoryPower', + 'cardName', + 'ultimateName', + // address + 'street', + 'houseNumber', + 'location', + 'zip', + 'countryId', + 'verifiedCountryId', + 'nationalityId', + // identity + 'birthday', + 'tin', + 'identDocumentId', + 'identDocumentType', + // financial + 'iban', + 'accountNumber', + // network/security + 'ip', + 'ipCountry', + 'apiKey', + 'apiKeyCT', + 'signature', + 'kycHash', + 'kycFileId', + 'internalAmlNote', + 'blackSquadRecipientMail', + 'individualFees', + 'totpSecret', + 'paymentLinksConfig', + 'paymentLinksName', + ]; private readonly DebugMaxResults = 10000; + // Log query templates (safe, predefined KQL queries) + private readonly LogQueryTemplates: Record< + LogQueryTemplate, + { kql: string; requiredParams: (keyof LogQueryDto)[]; defaultLimit: number } + > = { + [LogQueryTemplate.TRACES_BY_OPERATION]: { + kql: `traces +| where operation_Id == "{operationId}" +| where timestamp > ago({hours}h) +| project timestamp, severityLevel, message, customDimensions +| order by timestamp desc`, + requiredParams: ['operationId'], + defaultLimit: 500, + }, + [LogQueryTemplate.TRACES_BY_MESSAGE]: { + kql: `traces +| where timestamp > ago({hours}h) +| where message contains "{messageFilter}" +| project timestamp, severityLevel, message, operation_Id +| order by timestamp desc`, + requiredParams: ['messageFilter'], + defaultLimit: 200, + }, + [LogQueryTemplate.EXCEPTIONS_RECENT]: { + kql: `exceptions +| where timestamp > ago({hours}h) +| project timestamp, problemId, outerMessage, innermostMessage, operation_Id +| order by timestamp desc`, + requiredParams: [], + defaultLimit: 500, + }, + [LogQueryTemplate.REQUEST_FAILURES]: { + kql: `requests +| where timestamp > ago({hours}h) +| where success == false +| project timestamp, resultCode, duration, operation_Name, operation_Id +| order by timestamp desc`, + requiredParams: [], + defaultLimit: 500, + }, + [LogQueryTemplate.DEPENDENCIES_SLOW]: { + kql: `dependencies +| where timestamp > ago({hours}h) +| where duration > {durationMs} +| project timestamp, target, type, duration, success, operation_Id +| order by duration desc`, + requiredParams: ['durationMs'], + defaultLimit: 200, + }, + [LogQueryTemplate.CUSTOM_EVENTS]: { + kql: `customEvents +| where timestamp > ago({hours}h) +| where name == "{eventName}" +| project timestamp, name, customDimensions, operation_Id +| order by timestamp desc`, + requiredParams: ['eventName'], + defaultLimit: 500, + }, + }; + constructor( + private readonly appInsightsQueryService: AppInsightsQueryService, private readonly userDataService: UserDataService, private readonly userService: UserService, private readonly buyService: BuyService, @@ -333,7 +362,7 @@ export class GsService { } // 7. Check for blocked columns BEFORE execution (prevents alias bypass) - const blockedColumn = this.findBlockedColumnInQuery(sql, stmt); + const blockedColumn = this.findBlockedColumnInQuery(sql); if (blockedColumn) { throw new BadRequestException(`Access to column '${blockedColumn}' is not allowed`); } @@ -344,19 +373,16 @@ export class GsService { throw new BadRequestException(`TOP value exceeds maximum of ${this.DebugMaxResults}`); } - // 9. Get tables from query for post-execution masking - const tables = this.getTablesFromQuery(sql); - - // 10. Log query for audit trail + // 9. Log query for audit trail this.logger.info(`Debug query by ${userMail}: ${sql.substring(0, 500)}${sql.length > 500 ? '...' : ''}`); - // 11. Execute query with result limit + // 10. Execute query with result limit try { const limitedSql = this.ensureResultLimit(sql); const result = await this.dataSource.query(limitedSql); - // 12. Post-execution masking (defense in depth - also catches pre-execution failures) - this.maskDebugBlockedColumns(result, tables); + // 11. Additional masking for any columns that might have slipped through + this.maskDebugBlockedColumns(result); return result; } catch (e) { @@ -365,6 +391,58 @@ export class GsService { } } + async executeLogQuery(dto: LogQueryDto, userMail: string): Promise { + const template = this.LogQueryTemplates[dto.template]; + if (!template) { + throw new BadRequestException('Unknown template'); + } + + // Validate required params + for (const param of template.requiredParams) { + if (!dto[param]) { + throw new BadRequestException(`Parameter '${param}' is required for template '${dto.template}'`); + } + } + + // Build KQL with safe parameter substitution + let kql = template.kql; + kql = kql.replace('{operationId}', dto.operationId ?? ''); + kql = kql.replace('{messageFilter}', this.escapeKqlString(dto.messageFilter ?? '')); + kql = kql.replace(/{hours}/g, String(dto.hours ?? 1)); + kql = kql.replace('{durationMs}', String(dto.durationMs ?? 1000)); + kql = kql.replace('{eventName}', this.escapeKqlString(dto.eventName ?? '')); + + // Add limit + kql += `\n| take ${template.defaultLimit}`; + + // Log for audit + this.logger.info(`Log query by ${userMail}: template=${dto.template}, params=${JSON.stringify(dto)}`); + + // Execute + const timespan = `PT${dto.hours ?? 1}H`; + + try { + const response = await this.appInsightsQueryService.query(kql, timespan); + + if (!response.tables?.length) { + return { columns: [], rows: [] }; + } + + return { + columns: response.tables[0].columns, + rows: response.tables[0].rows, + }; + } catch (e) { + this.logger.warn(`Log query by ${userMail} failed: ${e.message}`); + throw new BadRequestException('Query execution failed'); + } + } + + private escapeKqlString(value: string): string { + // Escape quotes and backslashes for KQL string literals + return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); + } + //*** HELPER METHODS ***// private setJsonData(data: any[], selects: string[]): void { @@ -664,103 +742,42 @@ export class GsService { } } - private maskDebugBlockedColumns(data: Record[], tables: string[]): void { - if (!data?.length || !tables?.length) return; - - // Collect all blocked columns from all tables in the query - const blockedColumns = new Set(); - for (const table of tables) { - const tableCols = this.TableBlockedColumns[table]; - if (tableCols) { - for (const col of tableCols) { - blockedColumns.add(col.toLowerCase()); - } - } - } - - if (blockedColumns.size === 0) return; + private maskDebugBlockedColumns(data: Record[]): void { + if (!data?.length) return; for (const entry of data) { for (const key of Object.keys(entry)) { - if (this.shouldMaskDebugColumn(key, blockedColumns)) { + if (this.isDebugBlockedColumn(key)) { entry[key] = this.RestrictedMarker; } } } } - private shouldMaskDebugColumn(columnName: string, blockedColumns: Set): boolean { - const lower = columnName.toLowerCase(); - - // Check exact match or with table prefix (e.g., "name" or "bank_tx_name") - for (const blocked of blockedColumns) { - if (lower === blocked || lower.endsWith('_' + blocked)) { - return true; - } - } - return false; - } - - private getTablesFromQuery(sql: string): string[] { - const tableList = this.sqlParser.tableList(sql, { database: 'TransactSQL' }); - // Format: 'select::null::table_name' → extract table_name - return tableList.map((t) => t.split('::')[2]).filter(Boolean); - } - - private getAliasToTableMap(ast: any): Map { - const map = new Map(); - if (!ast.from) return map; - - for (const item of ast.from) { - if (item.table) { - map.set(item.as || item.table, item.table); - } - } - return map; - } - - private isColumnBlockedInTable(columnName: string, table: string | null, allTables: string[]): boolean { - const lower = columnName.toLowerCase(); - - if (table) { - // Explicit table known → check if this column is blocked in this table - const blockedCols = this.TableBlockedColumns[table]; - return blockedCols?.some((b) => b.toLowerCase() === lower) ?? false; - } else { - // No explicit table → if ANY of the query tables blocks this column, block it - return allTables.some((t) => { - const blockedCols = this.TableBlockedColumns[t]; - return blockedCols?.some((b) => b.toLowerCase() === lower) ?? false; - }); - } + private isDebugBlockedColumn(columnName: string): boolean { + const lowerKey = columnName.toLowerCase(); + // Match exact column name or prefixed (e.g., "firstname" or "user_firstname") + return this.DebugBlockedColumns.some((blocked) => { + const lowerBlocked = blocked.toLowerCase(); + return lowerKey === lowerBlocked || lowerKey.endsWith('_' + lowerBlocked); + }); } - private findBlockedColumnInQuery(sql: string, ast: any): string | null { + private findBlockedColumnInQuery(sql: string): string | null { try { // columnList returns: ['select::table::column', 'select::null::column', ...] const columns = this.sqlParser.columnList(sql, { database: 'TransactSQL' }); - const tables = this.getTablesFromQuery(sql); - const aliasMap = this.getAliasToTableMap(ast); for (const col of columns) { + // Format: 'operation::schema::column' - extract the column part const parts = col.split('::'); - const tableOrAlias = parts[1]; // can be 'null' - const columnName = parts[2]; + const columnName = parts[parts.length - 1]; - // Skip wildcard - handled post-execution + // Skip wildcard if (columnName === '*' || columnName === '(.*)') continue; - // Resolve table from alias - const resolvedTable = - tableOrAlias === 'null' - ? tables.length === 1 - ? tables[0] - : null // Single table without alias → use that table - : aliasMap.get(tableOrAlias) || tableOrAlias; - - // Check if column is blocked in this table - if (this.isColumnBlockedInTable(columnName, resolvedTable, tables)) { - return `${resolvedTable || 'unknown'}.${columnName}`; + if (this.isDebugBlockedColumn(columnName)) { + return columnName; } } @@ -811,4 +828,5 @@ export class GsService { this.DebugMaxResults } ROWS ONLY`; } + } From 24c33118bc4f2904bdb1c5dc8a6f4089705539a5 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Fri, 2 Jan 2026 13:26:59 +0100 Subject: [PATCH 7/9] fix(gs): add security hardening for debug SQL endpoint Security improvements: 1. Block system tables and schemas: - Added BlockedSchemas list: sys, information_schema, master, msdb, tempdb - checkForBlockedSchemas() validates FROM clause and subqueries - Prevents access to sys.sql_logins, INFORMATION_SCHEMA.TABLES, etc. 2. Fix TOP validation to use AST instead of regex: - Previous regex /\btop\s+(\d+)/ missed TOP(n) with parentheses - Now uses stmt.top?.value from AST for accurate detection - Both TOP 100 and TOP(100) are correctly validated 3. Extend dangerous function check to all clauses: - Previous check only validated FROM clause - Now recursively checks SELECT columns and WHERE clauses - checkForDangerousFunctionsRecursive() traverses entire AST - Blocks OPENROWSET, OPENQUERY, OPENDATASOURCE, OPENXML everywhere --- src/subdomains/generic/gs/gs.service.ts | 156 ++++++++++++++++++++---- 1 file changed, 133 insertions(+), 23 deletions(-) diff --git a/src/subdomains/generic/gs/gs.service.ts b/src/subdomains/generic/gs/gs.service.ts index 0d635d93b3..d025d2d266 100644 --- a/src/subdomains/generic/gs/gs.service.ts +++ b/src/subdomains/generic/gs/gs.service.ts @@ -119,6 +119,12 @@ export class GsService { private readonly DebugMaxResults = 10000; + // blocked system schemas (prevent access to system tables) + private readonly BlockedSchemas = ['sys', 'information_schema', 'master', 'msdb', 'tempdb']; + + // dangerous functions that could be used for data exfiltration or external connections + private readonly DangerousFunctions = ['openrowset', 'openquery', 'opendatasource', 'openxml']; + // Log query templates (safe, predefined KQL queries) private readonly LogQueryTemplates: Record< LogQueryTemplate, @@ -352,36 +358,38 @@ export class GsService { throw new BadRequestException('SELECT INTO not allowed'); } - // 5. No dangerous functions in FROM clause (external connections) - this.checkForDangerousFunctions(stmt); + // 5. No system tables/schemas (prevent access to sys.*, INFORMATION_SCHEMA.*, etc.) + this.checkForBlockedSchemas(stmt); + + // 6. No dangerous functions anywhere in the query (external connections) + this.checkForDangerousFunctionsRecursive(stmt); - // 6. No FOR XML/JSON (data exfiltration) + // 7. No FOR XML/JSON (data exfiltration) const normalizedLower = sql.toLowerCase(); if (normalizedLower.includes(' for xml') || normalizedLower.includes(' for json')) { throw new BadRequestException('FOR XML/JSON not allowed'); } - // 7. Check for blocked columns BEFORE execution (prevents alias bypass) + // 8. Check for blocked columns BEFORE execution (prevents alias bypass) const blockedColumn = this.findBlockedColumnInQuery(sql); if (blockedColumn) { throw new BadRequestException(`Access to column '${blockedColumn}' is not allowed`); } - // 8. Validate TOP value if present - const topMatch = normalizedLower.match(/\btop\s+(\d+)/i); - if (topMatch && parseInt(topMatch[1]) > this.DebugMaxResults) { + // 9. Validate TOP value if present (use AST for accurate detection including TOP(n) syntax) + if (stmt.top?.value > this.DebugMaxResults) { throw new BadRequestException(`TOP value exceeds maximum of ${this.DebugMaxResults}`); } - // 9. Log query for audit trail + // 10. Log query for audit trail this.logger.info(`Debug query by ${userMail}: ${sql.substring(0, 500)}${sql.length > 500 ? '...' : ''}`); - // 10. Execute query with result limit + // 11. Execute query with result limit try { const limitedSql = this.ensureResultLimit(sql); const result = await this.dataSource.query(limitedSql); - // 11. Additional masking for any columns that might have slipped through + // 12. Additional masking for any columns that might have slipped through this.maskDebugBlockedColumns(result); return result; @@ -788,28 +796,130 @@ export class GsService { } } - private checkForDangerousFunctions(stmt: any): void { - const dangerousFunctions = ['openrowset', 'openquery', 'opendatasource', 'openxml']; - - const checkFromClause = (from: any[]): void => { + private checkForBlockedSchemas(stmt: any): void { + const checkTables = (from: any[]): void => { if (!from) return; for (const item of from) { - // Check if FROM contains a function call - if (item.type === 'expr' && item.expr?.type === 'function') { - const funcName = item.expr.name?.name?.[0]?.value?.toLowerCase(); - if (funcName && dangerousFunctions.includes(funcName)) { - throw new BadRequestException(`Function '${funcName.toUpperCase()}' not allowed`); - } + // Check table schema (e.g., sys.sql_logins, INFORMATION_SCHEMA.TABLES) + const schema = item.db?.toLowerCase() || item.schema?.toLowerCase(); + const table = item.table?.toLowerCase(); + + if (schema && this.BlockedSchemas.includes(schema)) { + throw new BadRequestException(`Access to schema '${schema}' is not allowed`); } - // Recursively check subqueries in FROM + + // Also check if table name starts with blocked schema (e.g., "sys.objects" without explicit schema) + if (table && this.BlockedSchemas.some((s) => table.startsWith(s + '.'))) { + throw new BadRequestException(`Access to system tables is not allowed`); + } + + // Recursively check subqueries if (item.expr?.ast) { - this.checkForDangerousFunctions(item.expr.ast); + this.checkForBlockedSchemas(item.expr.ast); } } }; - checkFromClause(stmt.from); + checkTables(stmt.from); + + // Also check WHERE clause subqueries + this.checkSubqueriesForBlockedSchemas(stmt.where); + } + + private checkSubqueriesForBlockedSchemas(node: any): void { + if (!node) return; + + if (node.ast) { + this.checkForBlockedSchemas(node.ast); + } + + if (node.left) this.checkSubqueriesForBlockedSchemas(node.left); + if (node.right) this.checkSubqueriesForBlockedSchemas(node.right); + if (node.expr) this.checkSubqueriesForBlockedSchemas(node.expr); + if (node.args) { + const args = Array.isArray(node.args) ? node.args : [node.args]; + for (const arg of args) { + this.checkSubqueriesForBlockedSchemas(arg); + } + } + } + + private checkForDangerousFunctionsRecursive(stmt: any): void { + // Check FROM clause for dangerous functions + this.checkFromForDangerousFunctions(stmt.from); + + // Check SELECT columns for dangerous functions + this.checkExpressionsForDangerousFunctions(stmt.columns); + + // Check WHERE clause for dangerous functions + this.checkNodeForDangerousFunctions(stmt.where); + } + + private checkFromForDangerousFunctions(from: any[]): void { + if (!from) return; + + for (const item of from) { + // Check if FROM contains a function call + if (item.type === 'expr' && item.expr?.type === 'function') { + const funcName = this.extractFunctionName(item.expr); + if (funcName && this.DangerousFunctions.includes(funcName)) { + throw new BadRequestException(`Function '${funcName.toUpperCase()}' not allowed`); + } + } + + // Recursively check subqueries in FROM + if (item.expr?.ast) { + this.checkForDangerousFunctionsRecursive(item.expr.ast); + } + } + } + + private checkExpressionsForDangerousFunctions(columns: any[]): void { + if (!columns) return; + + for (const col of columns) { + this.checkNodeForDangerousFunctions(col.expr); + } + } + + private checkNodeForDangerousFunctions(node: any): void { + if (!node) return; + + // Check if this node is a function call + if (node.type === 'function') { + const funcName = this.extractFunctionName(node); + if (funcName && this.DangerousFunctions.includes(funcName)) { + throw new BadRequestException(`Function '${funcName.toUpperCase()}' not allowed`); + } + } + + // Check subqueries + if (node.ast) { + this.checkForDangerousFunctionsRecursive(node.ast); + } + + // Recursively check child nodes + if (node.left) this.checkNodeForDangerousFunctions(node.left); + if (node.right) this.checkNodeForDangerousFunctions(node.right); + if (node.expr) this.checkNodeForDangerousFunctions(node.expr); + if (node.args) { + const args = Array.isArray(node.args) ? node.args : node.args?.value || []; + for (const arg of Array.isArray(args) ? args : [args]) { + this.checkNodeForDangerousFunctions(arg); + } + } + } + + private extractFunctionName(funcNode: any): string | null { + // Handle different AST structures for function names + if (funcNode.name?.name?.[0]?.value) { + return funcNode.name.name[0].value.toLowerCase(); + } + if (typeof funcNode.name === 'string') { + return funcNode.name.toLowerCase(); + } + return null; } private ensureResultLimit(sql: string): string { From 9389c74d70358313c8b48ee48166413f12eed327 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Fri, 2 Jan 2026 13:53:19 +0100 Subject: [PATCH 8/9] refactor(gs): improve code professionalism in debug endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove commented debug code - Fix return type any[] → Record[] - Remove redundant try-catch in controller (service handles errors) - Rename misleading parameter userMail → userIdentifier - Standardize comment style --- src/subdomains/generic/gs/gs.controller.ts | 16 +++------------- src/subdomains/generic/gs/gs.service.ts | 21 +++++++-------------- 2 files changed, 10 insertions(+), 27 deletions(-) diff --git a/src/subdomains/generic/gs/gs.controller.ts b/src/subdomains/generic/gs/gs.controller.ts index 039f6053a4..15f11227fb 100644 --- a/src/subdomains/generic/gs/gs.controller.ts +++ b/src/subdomains/generic/gs/gs.controller.ts @@ -52,13 +52,8 @@ export class GsController { @ApiBearerAuth() @ApiExcludeEndpoint() @UseGuards(AuthGuard(), RoleGuard(UserRole.DEBUG), UserActiveGuard()) - async executeDebugQuery(@GetJwt() jwt: JwtPayload, @Body() dto: DebugQueryDto): Promise { - try { - return await this.gsService.executeDebugQuery(dto.sql, jwt.address ?? `account:${jwt.account}`); - } catch (e) { - this.logger.verbose(`Debug query failed:`, e); - throw new BadRequestException(e.message); - } + async executeDebugQuery(@GetJwt() jwt: JwtPayload, @Body() dto: DebugQueryDto): Promise[]> { + return this.gsService.executeDebugQuery(dto.sql, jwt.address ?? `account:${jwt.account}`); } @Post('debug/logs') @@ -66,11 +61,6 @@ export class GsController { @ApiExcludeEndpoint() @UseGuards(AuthGuard(), RoleGuard(UserRole.DEBUG), UserActiveGuard()) async executeLogQuery(@GetJwt() jwt: JwtPayload, @Body() dto: LogQueryDto): Promise { - try { - return await this.gsService.executeLogQuery(dto, jwt.address ?? `account:${jwt.account}`); - } catch (e) { - this.logger.verbose(`Log query failed:`, e); - throw new BadRequestException(e.message); - } + return this.gsService.executeLogQuery(dto, jwt.address ?? `account:${jwt.account}`); } } diff --git a/src/subdomains/generic/gs/gs.service.ts b/src/subdomains/generic/gs/gs.service.ts index d025d2d266..d01a1994c7 100644 --- a/src/subdomains/generic/gs/gs.service.ts +++ b/src/subdomains/generic/gs/gs.service.ts @@ -328,7 +328,7 @@ export class GsService { }; } - async executeDebugQuery(sql: string, userMail: string): Promise { + async executeDebugQuery(sql: string, userIdentifier: string): Promise[]> { // 1. Parse SQL to AST for robust validation let ast; try { @@ -382,7 +382,7 @@ export class GsService { } // 10. Log query for audit trail - this.logger.info(`Debug query by ${userMail}: ${sql.substring(0, 500)}${sql.length > 500 ? '...' : ''}`); + this.logger.info(`Debug query by ${userIdentifier}: ${sql.substring(0, 500)}${sql.length > 500 ? '...' : ''}`); // 11. Execute query with result limit try { @@ -394,12 +394,12 @@ export class GsService { return result; } catch (e) { - this.logger.warn(`Debug query by ${userMail} failed: ${e.message}`); + this.logger.warn(`Debug query by ${userIdentifier} failed: ${e.message}`); throw new BadRequestException('Query execution failed'); } } - async executeLogQuery(dto: LogQueryDto, userMail: string): Promise { + async executeLogQuery(dto: LogQueryDto, userIdentifier: string): Promise { const template = this.LogQueryTemplates[dto.template]; if (!template) { throw new BadRequestException('Unknown template'); @@ -424,7 +424,7 @@ export class GsService { kql += `\n| take ${template.defaultLimit}`; // Log for audit - this.logger.info(`Log query by ${userMail}: template=${dto.template}, params=${JSON.stringify(dto)}`); + this.logger.info(`Log query by ${userIdentifier}: template=${dto.template}, params=${JSON.stringify(dto)}`); // Execute const timespan = `PT${dto.hours ?? 1}H`; @@ -441,7 +441,7 @@ export class GsService { rows: response.tables[0].rows, }; } catch (e) { - this.logger.warn(`Log query by ${userMail} failed: ${e.message}`); + this.logger.warn(`Log query by ${userIdentifier} failed: ${e.message}`); throw new BadRequestException('Query execution failed'); } } @@ -451,7 +451,7 @@ export class GsService { return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); } - //*** HELPER METHODS ***// + // --- Helper Methods --- private setJsonData(data: any[], selects: string[]): void { const jsonSelects = selects.filter((s) => s.includes('-') && !s.includes('documents')); @@ -492,13 +492,6 @@ export class GsService { }; }, {}); - // if (table === 'support_issue' && selects.some((s) => s.includes('messages[max].author'))) - // this.logger.info( - // `GS array select log, entities: ${entities.map( - // (e) => `${e['messages_id']}-${e['messages_author']}`, - // )}, selectedData: ${selectedData['messages[max].author']}`, - // ); - return selectedData; }); } From a2be9a2c205078176538dde88202c3c88fed7c36 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Sat, 3 Jan 2026 09:04:12 +0100 Subject: [PATCH 9/9] fix(gs): restore table-specific column blocking for debug endpoint (#2797) PR #2778 accidentally reverted PR #2782's TableBlockedColumns changes due to a git reset that preserved stale local file contents. This commit restores: - TableBlockedColumns: Record with 30+ table-specific blocked column lists (42 columns across 23 tables) - getTablesFromQuery(): Extract table names from SQL AST - getAliasToTableMap(): Map table aliases to real names - isColumnBlockedInTable(): Table-aware column blocking check - findBlockedColumnInQuery(): Pre-execution validation with table context - maskDebugBlockedColumns(): Post-execution masking with table context Security impact of the regression: - Columns like comment, label, data, message, fileUrl, remittanceInfo, txRaw, chargebackIban, siftResponse, requestPayload were unblocked - Table-specific blocking allows SELECT name FROM asset (harmless) while blocking SELECT name FROM bank_tx (PII) All new features from later commits are preserved: - BlockedSchemas, DangerousFunctions - checkForBlockedSchemas(), checkForDangerousFunctionsRecursive() - LogQueryTemplates, executeLogQuery(), AppInsightsQueryService --- src/subdomains/generic/gs/gs.service.ts | 247 ++++++++++++++++-------- 1 file changed, 171 insertions(+), 76 deletions(-) diff --git a/src/subdomains/generic/gs/gs.service.ts b/src/subdomains/generic/gs/gs.service.ts index d01a1994c7..581cb95885 100644 --- a/src/subdomains/generic/gs/gs.service.ts +++ b/src/subdomains/generic/gs/gs.service.ts @@ -59,63 +59,97 @@ export class GsService { private readonly sqlParser = new Parser(); - // columns blocked for debug queries (personal data) - private readonly DebugBlockedColumns = [ - // restricted (for DEBUG only, ADMIN can see via /gs/db) - 'name', - 'result', - 'mail', - 'recipientMail', - 'phone', - 'firstname', - 'surname', - 'verifiedName', - 'organizationName', - 'organizationStreet', - 'organizationLocation', - 'organizationZip', - 'organizationCountryId', - 'organizationId', - 'allBeneficialOwnersName', - 'allBeneficialOwnersDomicile', - 'accountOpenerAuthorization', - 'complexOrgStructure', - 'accountOpener', - 'legalEntity', - 'signatoryPower', - 'cardName', - 'ultimateName', - // address - 'street', - 'houseNumber', - 'location', - 'zip', - 'countryId', - 'verifiedCountryId', - 'nationalityId', - // identity - 'birthday', - 'tin', - 'identDocumentId', - 'identDocumentType', - // financial - 'iban', - 'accountNumber', - // network/security - 'ip', - 'ipCountry', - 'apiKey', - 'apiKeyCT', - 'signature', - 'kycHash', - 'kycFileId', - 'internalAmlNote', - 'blackSquadRecipientMail', - 'individualFees', - 'totpSecret', - 'paymentLinksConfig', - 'paymentLinksName', - ]; + // Table-specific blocked columns for debug queries (personal data) + private readonly TableBlockedColumns: Record = { + // user_data - main table with PII + user_data: [ + 'mail', 'phone', 'firstname', 'surname', 'verifiedName', + 'street', 'houseNumber', 'location', 'zip', + 'countryId', 'verifiedCountryId', 'nationalityId', // Foreign keys to country + 'birthday', 'tin', 'identDocumentId', 'identDocumentType', + 'organizationName', 'organizationStreet', 'organizationLocation', 'organizationZip', + 'organizationCountryId', 'organizationId', + 'allBeneficialOwnersName', 'allBeneficialOwnersDomicile', + 'accountOpenerAuthorization', 'complexOrgStructure', 'accountOpener', 'legalEntity', 'signatoryPower', + 'kycHash', 'kycFileId', 'apiKeyCT', 'totpSecret', + 'internalAmlNote', 'blackSquadRecipientMail', 'individualFees', + 'paymentLinksConfig', 'paymentLinksName', 'comment', 'relatedUsers', + ], + // user + user: ['ip', 'ipCountry', 'apiKeyCT', 'signature', 'label', 'comment'], + // bank_tx - bank transactions + bank_tx: [ + 'name', 'ultimateName', 'iban', 'accountIban', 'senderAccount', 'bic', + 'addressLine1', 'addressLine2', 'ultimateAddressLine1', 'ultimateAddressLine2', + 'bankAddressLine1', 'bankAddressLine2', + 'remittanceInfo', 'txInfo', 'txRaw', + ], + // bank_data + bank_data: ['name', 'iban', 'label', 'comment'], + // fiat_output + fiat_output: [ + 'name', 'iban', 'accountIban', 'accountNumber', 'bic', 'aba', + 'address', 'houseNumber', 'zip', 'city', + 'remittanceInfo', + ], + // checkout_tx - payment card data + checkout_tx: [ + 'cardName', 'ip', + 'cardBin', 'cardLast4', 'cardFingerPrint', 'cardIssuer', 'cardIssuerCountry', 'raw', + ], + // bank_account + bank_account: ['accountNumber'], + // virtual_iban + virtual_iban: ['iban', 'bban', 'label'], + // kyc_step - KYC steps (result/data contains names, birthday, document number) + kyc_step: ['result', 'comment', 'data'], + // kyc_file + kyc_file: ['name'], + // kyc_log (includes TfaLog ChildEntity with ipAddress) + kyc_log: ['comment', 'ipAddress', 'result'], + // organization + organization: [ + 'name', 'street', 'houseNumber', 'location', 'zip', + 'allBeneficialOwnersName', 'allBeneficialOwnersDomicile', + ], + // transactions + buy_crypto: ['recipientMail', 'comment', 'chargebackIban', 'chargebackRemittanceInfo', 'siftResponse'], + buy_fiat: ['recipientMail', 'comment', 'remittanceInfo', 'usedBank', 'info'], + transaction: ['recipientMail'], + crypto_input: ['recipientMail', 'senderAddresses'], + // payment_link + payment_link: ['comment', 'label'], + // wallet (integration) + wallet: ['apiKey'], + // ref - referral tracking + ref: ['ip'], + // ip_log - IP logging + ip_log: ['ip', 'country'], + // buy - buy crypto routes + buy: ['iban'], + // deposit_route - sell routes (Single Table Inheritance for Sell entity) + deposit_route: ['iban'], + // bank_tx_return - chargeback returns + bank_tx_return: ['chargebackIban', 'recipientMail', 'chargebackRemittanceInfo', 'info'], + // bank_tx_repeat - repeat transactions + bank_tx_repeat: ['chargebackIban', 'chargebackRemittanceInfo'], + // limit_request - limit increase requests + limit_request: ['recipientMail', 'fundOriginText'], + // ref_reward - referral rewards + ref_reward: ['recipientMail'], + // transaction_risk_assessment - AML/KYC assessments + transaction_risk_assessment: ['reason', 'methods', 'summary', 'result'], + // support_issue - support tickets with user data + support_issue: ['name', 'information'], + // support_message - message content and file URLs + support_message: ['message', 'fileUrl'], + // sift_error_log - Sift API request payloads containing PII + sift_error_log: ['requestPayload'], + // webhook - serialized user/transaction data + webhook: ['data'], + // notification - notification payloads with user data + notification: ['data'], + }; private readonly DebugMaxResults = 10000; @@ -371,7 +405,8 @@ export class GsService { } // 8. Check for blocked columns BEFORE execution (prevents alias bypass) - const blockedColumn = this.findBlockedColumnInQuery(sql); + const tables = this.getTablesFromQuery(sql); + const blockedColumn = this.findBlockedColumnInQuery(sql, stmt, tables); if (blockedColumn) { throw new BadRequestException(`Access to column '${blockedColumn}' is not allowed`); } @@ -389,8 +424,8 @@ export class GsService { const limitedSql = this.ensureResultLimit(sql); const result = await this.dataSource.query(limitedSql); - // 12. Additional masking for any columns that might have slipped through - this.maskDebugBlockedColumns(result); + // 12. Post-execution masking (defense in depth - also catches pre-execution failures) + this.maskDebugBlockedColumns(result, tables); return result; } catch (e) { @@ -743,42 +778,102 @@ export class GsService { } } - private maskDebugBlockedColumns(data: Record[]): void { - if (!data?.length) return; + private maskDebugBlockedColumns(data: Record[], tables: string[]): void { + if (!data?.length || !tables?.length) return; + + // Collect all blocked columns from all tables in the query + const blockedColumns = new Set(); + for (const table of tables) { + const tableCols = this.TableBlockedColumns[table]; + if (tableCols) { + for (const col of tableCols) { + blockedColumns.add(col.toLowerCase()); + } + } + } + + if (blockedColumns.size === 0) return; for (const entry of data) { for (const key of Object.keys(entry)) { - if (this.isDebugBlockedColumn(key)) { + if (this.shouldMaskDebugColumn(key, blockedColumns)) { entry[key] = this.RestrictedMarker; } } } } - private isDebugBlockedColumn(columnName: string): boolean { - const lowerKey = columnName.toLowerCase(); - // Match exact column name or prefixed (e.g., "firstname" or "user_firstname") - return this.DebugBlockedColumns.some((blocked) => { - const lowerBlocked = blocked.toLowerCase(); - return lowerKey === lowerBlocked || lowerKey.endsWith('_' + lowerBlocked); - }); + private shouldMaskDebugColumn(columnName: string, blockedColumns: Set): boolean { + const lower = columnName.toLowerCase(); + + // Check exact match or with table prefix (e.g., "name" or "bank_tx_name") + for (const blocked of blockedColumns) { + if (lower === blocked || lower.endsWith('_' + blocked)) { + return true; + } + } + return false; + } + + private getTablesFromQuery(sql: string): string[] { + const tableList = this.sqlParser.tableList(sql, { database: 'TransactSQL' }); + // Format: 'select::null::table_name' → extract table_name + return tableList.map((t) => t.split('::')[2]).filter(Boolean); + } + + private getAliasToTableMap(ast: any): Map { + const map = new Map(); + if (!ast.from) return map; + + for (const item of ast.from) { + if (item.table) { + map.set(item.as || item.table, item.table); + } + } + return map; + } + + private isColumnBlockedInTable(columnName: string, table: string | null, allTables: string[]): boolean { + const lower = columnName.toLowerCase(); + + if (table) { + // Explicit table known → check if this column is blocked in this table + const blockedCols = this.TableBlockedColumns[table]; + return blockedCols?.some((b) => b.toLowerCase() === lower) ?? false; + } else { + // No explicit table → if ANY of the query tables blocks this column, block it + return allTables.some((t) => { + const blockedCols = this.TableBlockedColumns[t]; + return blockedCols?.some((b) => b.toLowerCase() === lower) ?? false; + }); + } } - private findBlockedColumnInQuery(sql: string): string | null { + private findBlockedColumnInQuery(sql: string, ast: any, tables: string[]): string | null { try { // columnList returns: ['select::table::column', 'select::null::column', ...] const columns = this.sqlParser.columnList(sql, { database: 'TransactSQL' }); + const aliasMap = this.getAliasToTableMap(ast); for (const col of columns) { - // Format: 'operation::schema::column' - extract the column part const parts = col.split('::'); - const columnName = parts[parts.length - 1]; + const tableOrAlias = parts[1]; // can be 'null' + const columnName = parts[2]; - // Skip wildcard + // Skip wildcard - handled post-execution if (columnName === '*' || columnName === '(.*)') continue; - if (this.isDebugBlockedColumn(columnName)) { - return columnName; + // Resolve table from alias + const resolvedTable = + tableOrAlias === 'null' + ? tables.length === 1 + ? tables[0] + : null // Single table without alias → use that table + : aliasMap.get(tableOrAlias) || tableOrAlias; + + // Check if column is blocked in this table + if (this.isColumnBlockedInTable(columnName, resolvedTable, tables)) { + return `${resolvedTable || 'unknown'}.${columnName}`; } }