From 7988853b5726bcf51b8452c13e7a4b118e024ffa Mon Sep 17 00:00:00 2001 From: Charlotte Croce Date: Mon, 7 Apr 2025 12:22:06 -0400 Subject: [PATCH] first commit --- .gitignore | 8 + README.md | 48 + fylgja.example.yml | 37 + package-lock.json | 3221 +++++++++++++++++ package.json | 28 + requirements.txt | 1 + slack.example.yml | 81 + src/app.js | 138 + src/blocks/sigma_conversion_block.js | 124 + src/blocks/sigma_details_block.js | 298 ++ src/blocks/sigma_search_results_block.js | 206 ++ src/blocks/sigma_stats_block.js | 269 ++ src/blocks/sigma_view_yaml_block.js | 83 + src/config/appConfig.js | 83 + src/handlers/alerts/alerts_handler.js | 0 src/handlers/case/case_handler.js | 0 src/handlers/config/config_handler.js | 88 + src/handlers/sigma/sigma_action_handlers.js | 552 +++ src/handlers/sigma/sigma_create_handler.js | 62 + src/handlers/sigma/sigma_details_handler.js | 56 + src/handlers/sigma/sigma_search_handler.js | 171 + src/handlers/sigma/sigma_stats_handler.js | 68 + src/handlers/stats/stats_handler.js | 3 + src/index.js | 63 + src/services/elastic/cases.js | 0 src/services/elastic/clients.js | 0 src/services/elastic/elastic_api_service.js | 154 + src/services/elastic/rules.js | 0 src/services/elastic/spaces.js | 0 src/services/sigma/sigma_backend_converter.js | 153 + src/services/sigma/sigma_converter_service.js | 422 +++ src/services/sigma/sigma_details_service.js | 150 + .../sigma/sigma_repository_service.js | 188 + src/services/sigma/sigma_search_service.js | 214 ++ src/services/sigma/sigma_stats_service.js | 53 + src/sigma_db/sigma_db_connection.js | 93 + src/sigma_db/sigma_db_initialize.js | 560 +++ src/sigma_db/sigma_db_queries.js | 585 +++ src/utils/error_handler.js | 42 + src/utils/file_utils.js | 10 + src/utils/formatters.js | 0 src/utils/logger.js | 103 + src/utils/validators.js | 0 43 files changed, 8415 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 fylgja.example.yml create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 requirements.txt create mode 100644 slack.example.yml create mode 100644 src/app.js create mode 100644 src/blocks/sigma_conversion_block.js create mode 100644 src/blocks/sigma_details_block.js create mode 100644 src/blocks/sigma_search_results_block.js create mode 100644 src/blocks/sigma_stats_block.js create mode 100644 src/blocks/sigma_view_yaml_block.js create mode 100644 src/config/appConfig.js create mode 100644 src/handlers/alerts/alerts_handler.js create mode 100644 src/handlers/case/case_handler.js create mode 100644 src/handlers/config/config_handler.js create mode 100644 src/handlers/sigma/sigma_action_handlers.js create mode 100644 src/handlers/sigma/sigma_create_handler.js create mode 100644 src/handlers/sigma/sigma_details_handler.js create mode 100644 src/handlers/sigma/sigma_search_handler.js create mode 100644 src/handlers/sigma/sigma_stats_handler.js create mode 100644 src/handlers/stats/stats_handler.js create mode 100644 src/index.js create mode 100644 src/services/elastic/cases.js create mode 100644 src/services/elastic/clients.js create mode 100644 src/services/elastic/elastic_api_service.js create mode 100644 src/services/elastic/rules.js create mode 100644 src/services/elastic/spaces.js create mode 100644 src/services/sigma/sigma_backend_converter.js create mode 100644 src/services/sigma/sigma_converter_service.js create mode 100644 src/services/sigma/sigma_details_service.js create mode 100644 src/services/sigma/sigma_repository_service.js create mode 100644 src/services/sigma/sigma_search_service.js create mode 100644 src/services/sigma/sigma_stats_service.js create mode 100644 src/sigma_db/sigma_db_connection.js create mode 100644 src/sigma_db/sigma_db_initialize.js create mode 100644 src/sigma_db/sigma_db_queries.js create mode 100644 src/utils/error_handler.js create mode 100644 src/utils/file_utils.js create mode 100644 src/utils/formatters.js create mode 100644 src/utils/logger.js create mode 100644 src/utils/validators.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..19ad939 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.venv +.env +node_modules/ +fylgja.log +fylgja.yml +slack.yml +sigma.db +sigma-repo/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..ee0e7cd --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ + +# fylgja +Manage your Elastic Stack threat detection ruleset through a Slack frontend + +## Features + - [Sigma](https://github.com/SigmaHQ/sigma) integration: + - Imports the Sigma rule repository to an SQLite database + - Search rules by keyword + - Convert rules into SIEM format + - Upload generated rules to Elastic + - All without leaving the Slack channel! + +## Setup +### Clone Repo +``` +git clone https://codeberg.org/charlottecroce/fylgja.git +cd fylgja/ +``` +### Install requirements +``` +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +``` + +### Install sigma-cli elasticsearch plugin +``` +sigma plugin install elasticsearch +``` + +### Create the database +``` +npm run update-db +``` + +### YAML Configuration +- Copy the example config: `cp fylgja.example.yml fylgja.yml` +- Edit `fylgja.yml` and replace the placeholder values with your real API keys + +### Slack Configuration +- Copy the example config: `cp slack.example.yml slack.yml` +- Edit `slack.yml` and replace the placeholder values with your real server domain name + +> this should probably be all included in a setup script or something + +> [!Important] +> While detection rules are stored in Elasticsearch, in my case, they are managed through the Kibana API. This has not been tested on other frontend APIs. + diff --git a/fylgja.example.yml b/fylgja.example.yml new file mode 100644 index 0000000..fc66c2c --- /dev/null +++ b/fylgja.example.yml @@ -0,0 +1,37 @@ +# Fylgja Configuration File +# This file contains all configurable settings for the Fylgja Slack bot + +# Slack settings +slack: + bot_token: "xoxb-TOKEN_HERE" + signing_secret: "SIGNING_SECRET_HERE" + +# Server settings +server: + port: 3000 + +# Paths configuration +paths: + sigma_repo_dir: "./sigma-repo" + db_path: "./sigma.db" + +# Sigma settings +sigma: + sigma-cli: + path: "./.venv/bin/sigma" + backend: "lucene" + target: "ecs_windows" + format: "siem_rule_ndjson" + repo: + url: "https://github.com/SigmaHQ/sigma.git" + branch: "main" + +# Elastic settings +elastic: + api-endpoint: "http://localhost:5601/api/detection_engine/rules" + elastic-authentication-credentials: "elastic:changeme" + +# Logging settings +logging: + level: "debug" + file: "./logs/fylgja.log" \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..4cb0601 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3221 @@ +{ + "name": "fylgja", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "fylgja", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@slack/bolt": "^4.2.1", + "dotenv": "^16.4.7", + "express": "^5.1.0", + "glob": "^8.1.0", + "js-yaml": "^4.1.0", + "sqlite3": "^5.1.7" + }, + "devDependencies": { + "concurrently": "^9.1.2" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "license": "MIT", + "optional": true + }, + "node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "license": "MIT", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@slack/bolt": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@slack/bolt/-/bolt-4.2.1.tgz", + "integrity": "sha512-O+c7i5iZKlxt6ltJAu2BclEoyWuAVkcpir1F3HWCHTez8Pjz0GxwdBzNHR5HDXvOdBT7En1BU0T2L6Ldv++GSg==", + "license": "MIT", + "dependencies": { + "@slack/logger": "^4.0.0", + "@slack/oauth": "^3.0.2", + "@slack/socket-mode": "^2.0.3", + "@slack/types": "^2.13.0", + "@slack/web-api": "^7.8.0", + "axios": "^1.7.8", + "express": "^5.0.0", + "path-to-regexp": "^8.1.0", + "raw-body": "^3", + "tsscmp": "^1.0.6" + }, + "engines": { + "node": ">=18", + "npm": ">=8.6.0" + }, + "peerDependencies": { + "@types/express": "^5.0.0" + } + }, + "node_modules/@slack/logger": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@slack/logger/-/logger-4.0.0.tgz", + "integrity": "sha512-Wz7QYfPAlG/DR+DfABddUZeNgoeY7d1J39OCR2jR+v7VBsB8ezulDK5szTnDDPDwLH5IWhLvXIHlCFZV7MSKgA==", + "license": "MIT", + "dependencies": { + "@types/node": ">=18.0.0" + }, + "engines": { + "node": ">= 18", + "npm": ">= 8.6.0" + } + }, + "node_modules/@slack/oauth": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@slack/oauth/-/oauth-3.0.3.tgz", + "integrity": "sha512-N3pLJPacZ57bqmD1HzHDmHe/CNsL9pESZXRw7pfv6QXJVRgufPIW84aRpAez2Xb0616RpGBYZW5dZH0Nbskwyg==", + "license": "MIT", + "dependencies": { + "@slack/logger": "^4", + "@slack/web-api": "^7.9.1", + "@types/jsonwebtoken": "^9", + "@types/node": ">=18", + "jsonwebtoken": "^9", + "lodash.isstring": "^4" + }, + "engines": { + "node": ">=18", + "npm": ">=8.6.0" + } + }, + "node_modules/@slack/socket-mode": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@slack/socket-mode/-/socket-mode-2.0.4.tgz", + "integrity": "sha512-PB2fO4TSv47TXJ6WlKY7BeVNdcHcpPOxZsztGyG7isWXp69MVj+xAzQ3KSZ8aVTgV59f8xFJPXSHipn1x2Z5IQ==", + "license": "MIT", + "dependencies": { + "@slack/logger": "^4", + "@slack/web-api": "^7.9.1", + "@types/node": ">=18", + "@types/ws": "^8", + "eventemitter3": "^5", + "ws": "^8" + }, + "engines": { + "node": ">= 18", + "npm": ">= 8.6.0" + } + }, + "node_modules/@slack/types": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/@slack/types/-/types-2.14.0.tgz", + "integrity": "sha512-n0EGm7ENQRxlXbgKSrQZL69grzg1gHLAVd+GlRVQJ1NSORo0FrApR7wql/gaKdu2n4TO83Sq/AmeUOqD60aXUA==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0", + "npm": ">= 6.12.0" + } + }, + "node_modules/@slack/web-api": { + "version": "7.9.1", + "resolved": "https://registry.npmjs.org/@slack/web-api/-/web-api-7.9.1.tgz", + "integrity": "sha512-qMcb1oWw3Y/KlUIVJhkI8+NcQXq1lNymwf+ewk93ggZsGd6iuz9ObQsOEbvlqlx1J+wd8DmIm3DORGKs0fcKdg==", + "license": "MIT", + "dependencies": { + "@slack/logger": "^4.0.0", + "@slack/types": "^2.9.0", + "@types/node": ">=18.0.0", + "@types/retry": "0.12.0", + "axios": "^1.8.3", + "eventemitter3": "^5.0.1", + "form-data": "^4.0.0", + "is-electron": "2.2.2", + "is-stream": "^2", + "p-queue": "^6", + "p-retry": "^4", + "retry": "^0.13.1" + }, + "engines": { + "node": ">= 18", + "npm": ">= 8.6.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz", + "integrity": "sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz", + "integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.14.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz", + "integrity": "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC", + "optional": true + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "license": "MIT", + "optional": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "license": "ISC", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT", + "optional": true + }, + "node_modules/concurrently": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.2.tgz", + "integrity": "sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC", + "optional": true + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT", + "optional": true + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "license": "MIT", + "optional": true + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC", + "optional": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC", + "optional": true + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "license": "BSD-2-Clause", + "optional": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "license": "ISC", + "optional": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "license": "MIT", + "optional": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-electron": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-electron/-/is-electron-2.2.2.tgz", + "integrity": "sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==", + "license": "MIT" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "license": "MIT", + "optional": true + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC", + "optional": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT", + "optional": true + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "license": "ISC", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/make-fetch-happen/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.74.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz", + "integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "license": "MIT", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/node-gyp/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/node-gyp/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue/node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "license": "ISC", + "optional": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "license": "MIT", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/promise-retry/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC", + "optional": true + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shell-quote": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz", + "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC", + "optional": true + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", + "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/sqlite3": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", + "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1", + "tar": "^6.1.11" + }, + "optionalDependencies": { + "node-gyp": "8.x" + }, + "peerDependencies": { + "node-gyp": "8.x" + }, + "peerDependenciesMeta": { + "node-gyp": { + "optional": true + } + } + }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "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" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", + "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "license": "MIT", + "engines": { + "node": ">=0.6.x" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "license": "ISC", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..1cdbd06 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "fylgja", + "version": "1.0.0", + "main": "src/index.js", + "scripts": { + "start": "node src/index.js", + "ngrok": "ngrok http 3000 --log=stdout --url=tolerant-bull-ideal.ngrok-free.app", + "dev": "concurrently \"npm run start\" \"npm run ngrok\"", + "update-db": "node src/sigma_db/sigma_db_initialize.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "Fylgja - Threat detection rule Slack interface", + "dependencies": { + "@slack/bolt": "^4.2.1", + "axios": "^1.6.7", + "dotenv": "^16.4.7", + "express": "^5.1.0", + "glob": "^8.1.0", + "js-yaml": "^4.1.0", + "sqlite3": "^5.1.7" + }, + "devDependencies": { + "concurrently": "^9.1.2" + } +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e72c5ab --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +sigma-cli \ No newline at end of file diff --git a/slack.example.yml b/slack.example.yml new file mode 100644 index 0000000..79ed59b --- /dev/null +++ b/slack.example.yml @@ -0,0 +1,81 @@ +display_information: + name: fylgja + description: threat detection engine bot + background_color: "#344d59" +features: + bot_user: + display_name: fylgja + always_online: false + slash_commands: + - command: /rule + url: http://SERVER_DOMAIN_NAME/slack/events + description: Convert, explain, search, view, or test Sigma rules + should_escape: false + - command: /sigma-create + url: http://SERVER_DOMAIN_NAME/slack/events + description: Convert a Sigma rule to configured output format + usage_hint: "[id]" + should_escape: false + - command: /sigma-details + url: http://SERVER_DOMAIN_NAME/slack/events + description: Get an explanation of a Sigma rule + usage_hint: "[id]" + should_escape: false + - command: /sigma-search + url: http://SERVER_DOMAIN_NAME/slack/events + description: Search for Sigma rules + usage_hint: "[keyword]" + should_escape: false + - command: /sigma-view + url: http://SERVER_DOMAIN_NAME/slack/events + description: View rule definition + usage_hint: "[id] [space]" + should_escape: false + - command: /sigma-test + url: http://SERVER_DOMAIN_NAME/slack/events + description: Test a Sigma rule with event log + usage_hint: "[event log]" + should_escape: false + - command: /sigma-config + url: http://SERVER_DOMAIN_NAME/slack/events + description: Update Sigma rule conversion configuration + usage_hint: siem [value] OR lang [value] OR output [value] OR update + should_escape: false + - command: /sigma-stats + url: http://SERVER_DOMAIN_NAME/slack/events + description: Show stats about Sigma rules + should_escape: false + - command: /alerts + url: http://SERVER_DOMAIN_NAME/slack/events + description: List alerts with IDs + usage_hint: "[space]" + should_escape: false + - command: /case + url: http://SERVER_DOMAIN_NAME/slack/events + description: Create an Elasticsearch case + usage_hint: "[id]" + should_escape: false + - command: /stats + url: http://SERVER_DOMAIN_NAME/slack/events + description: Show statistics + should_escape: false +oauth_config: + scopes: + bot: + - app_mentions:read + - chat:write + - im:history + - im:read + - commands +settings: + event_subscriptions: + request_url: http://SERVER_DOMAIN_NAME/slack/events + bot_events: + - app_mention + - message.im + interactivity: + is_enabled: true + request_url: http://SERVER_DOMAIN_NAME/slack/events + org_deploy_enabled: false + socket_mode_enabled: false + token_rotation_enabled: false diff --git a/src/app.js b/src/app.js new file mode 100644 index 0000000..c5e99be --- /dev/null +++ b/src/app.js @@ -0,0 +1,138 @@ +/** + * app.js + * + * Main application file for Fylgja Slack bot + * Initializes the Slack Bolt app with custom ExpressReceiver Registers command handlers + * + */ +const { App, ExpressReceiver } = require('@slack/bolt'); +const fs = require('fs'); +const logger = require('./utils/logger'); +const { SIGMA_CLI_PATH, SIGMA_CLI_CONFIG, SLACK_CONFIG } = require('./config/appConfig'); + +const { getFileName } = require('./utils/file_utils'); +const FILE_NAME = getFileName(__filename); + +// Import individual command handlers +const sigmaDetailsHandler = require('./handlers/sigma/sigma_details_handler'); +const sigmaSearchHandler = require('./handlers/sigma/sigma_search_handler'); +const sigmaCreateHandler = require('./handlers/sigma/sigma_create_handler'); +const sigmaActionHandlers = require('./handlers/sigma/sigma_action_handlers'); +//const configCommand = require('./commands/config/index.js'); +//const alertsCommand = require('./commands/alerts/index.js'); +//const caseCommand = require('./commands/case/index.js'); +//const statsCommand = require('./commands/stats/index.js'); + +// Verify sigma-cli is installed +if (!fs.existsSync(SIGMA_CLI_PATH)) { + logger.error(`Error: Sigma CLI not found at specified path: ${SIGMA_CLI_PATH}`); + process.exit(1); +} + +// Log the loaded configuration +logger.info(`Loaded sigma-cli configuration: ${JSON.stringify(SIGMA_CLI_CONFIG)}`); + +/** + * Create a custom ExpressReceiver for more control over the HTTP server + */ +const expressReceiver = new ExpressReceiver({ + signingSecret: SLACK_CONFIG.signingSecret, + processBeforeResponse: true +}); + +/** + * Initialize the Slack app with the custom receiver + */ +const app = new App({ + token: SLACK_CONFIG.botToken, + receiver: expressReceiver +}); + +// Register individual command handlers for all sigma commands +logger.info('Registering command handlers'); + +// Register sigma command handlers directly +app.command('/sigma-create', async ({ command, ack, respond }) => { + try { + await ack(); + logger.info(`Received sigma-create command: ${command.text}`); + await sigmaCreateHandler.handleCommand(command, respond); + } catch (error) { + logger.error(`Error handling sigma-create command: ${error.message}`); + logger.debug(`Error stack: ${error.stack}`); + await respond({ + text: `An error occurred: ${error.message}`, + response_type: 'ephemeral' + }); + } +}); + +app.command('/sigma-details', async ({ command, ack, respond }) => { + try { + await ack(); + logger.info(`Received sigma-details command: ${command.text}`); + await sigmaDetailsHandler.handleCommand(command, respond); + } catch (error) { + logger.error(`Error handling sigma-details command: ${error.message}`); + logger.debug(`Error stack: ${error.stack}`); + await respond({ + text: `An error occurred: ${error.message}`, + response_type: 'ephemeral' + }); + } +}); + +app.command('/sigma-search', async ({ command, ack, respond }) => { + try { + await ack(); + logger.info(`Received sigma-search command: ${command.text}`); + await sigmaSearchHandler.handleCommand(command, respond); + } catch (error) { + logger.error(`Error handling sigma-search command: ${error.message}`); + logger.debug(`Error stack: ${error.stack}`); + await respond({ + text: `An error occurred: ${error.message}`, + response_type: 'ephemeral' + }); + } +}); + +app.command('/sigma-stats', async ({ command, ack, respond }) => { + try { + await ack(); + logger.info(`Received sigma-stats command`); + const sigmaStatsHandler = require('./handlers/sigma/sigma_stats_handler'); + await sigmaStatsHandler.handleCommand(command, respond); + } catch (error) { + logger.error(`Error handling sigma-stats command: ${error.message}`); + logger.debug(`Error stack: ${error.stack}`); + await respond({ + text: `An error occurred: ${error.message}`, + response_type: 'ephemeral' + }); + } +}); + +// Register all button action handlers from centralized module +sigmaActionHandlers.registerActionHandlers(app); + +/** + * Listen for any message in DMs + * This allows users to interact with the bot directly + */ +app.message(async ({ message, say }) => { + // Only respond to DMs (message.channel starts with 'D') + if (message.channel.startsWith('D')) { + logger.info(`DM received from user ${message.user}`); + logger.debug(`DM content: ${message.text}`); + + // For now, we're just logging DMs but not responding + // Uncomment below to enable responses to DMs + // await say(`I received your message: "${message.text}"`); + } +}); + +/** + * Export the configured app for use in the main server file + */ +module.exports = app; \ No newline at end of file diff --git a/src/blocks/sigma_conversion_block.js b/src/blocks/sigma_conversion_block.js new file mode 100644 index 0000000..58a76c0 --- /dev/null +++ b/src/blocks/sigma_conversion_block.js @@ -0,0 +1,124 @@ +/** + * sigma_conversion_block.js + * + * Provides block templates for displaying Sigma rule conversion results in Slack + */ +const logger = require('../utils/logger'); + +const { getFileName } = require('../utils/file_utils'); +const FILE_NAME = getFileName(__filename); + +/** + * Generate blocks for displaying a Sigma rule conversion result + * + * @param {Object} conversionResult - The result of the conversion operation + * @returns {Array} Array of blocks for Slack message + */ +function getConversionResultBlocks(conversionResult) { + logger.debug(`${FILE_NAME}: Generating blocks for conversion result`); + + if (!conversionResult || !conversionResult.success) { + logger.warn(`${FILE_NAME}: Invalid conversion result provided for block generation`); + return [{ + type: 'section', + text: { + type: 'mrkdwn', + text: 'Error: Failed to generate conversion result blocks' + } + }]; + } + + const rule = conversionResult.rule || { + id: 'unknown', + title: 'Unknown Rule', + description: 'No rule metadata available' + }; + + const details = conversionResult.conversionDetails || { + backend: 'lucene', + target: 'ecs_windows', + format: 'siem_rule_ndjson' + }; + + // Truncate output if it's too long for Slack + let output = conversionResult.output || ''; + const maxOutputLength = 2900; // Slack has a limit of ~3000 chars in a code block + const isTruncated = output.length > maxOutputLength; + + if (isTruncated) { + output = output.substring(0, maxOutputLength) + '... [truncated]'; + } + + // Create the blocks + const blocks = [ + { + type: 'header', + text: { + type: 'plain_text', + text: `Converted Rule: ${rule.title}`, + emoji: true + } + }, + { + type: 'section', + text: { + type: 'mrkdwn', + text: `*Rule ID:* ${rule.id}\n*Description:* ${rule.description}` + } + }, + { + type: 'section', + text: { + type: 'mrkdwn', + text: `*Conversion Settings:*\nBackend: \`${details.backend}\` | Target: \`${details.target}\` | Format: \`${details.format}\`` + } + }, + { + type: 'divider' + }, + { + type: 'section', + text: { + type: 'mrkdwn', + text: `*Converted Output:*${isTruncated ? ' (truncated for display)' : ''}\n\`\`\`\n${output}\n\`\`\`` + } + } + ]; + + // Action buttons + blocks.push({ + type: 'actions', + elements: [ + { + type: 'button', + text: { + type: 'plain_text', + text: 'send_sigma_rule_to_siem', + emoji: true + }, + value: `send_sigma_rule_to_siem_${rule.id}`, + action_id: 'send_sigma_rule_to_siem' + }, + ] + }); + + // Warning if output was truncated + if (isTruncated) { + blocks.push({ + type: 'context', + elements: [ + { + type: 'mrkdwn', + text: ':warning: The output was truncated for display in Slack. Use the copy button to get the full content.' + } + ] + }); + } + + logger.debug(`${FILE_NAME}: Generated ${blocks.length} blocks for conversion result`); + return blocks; +} + +module.exports = { + getConversionResultBlocks +}; \ No newline at end of file diff --git a/src/blocks/sigma_details_block.js b/src/blocks/sigma_details_block.js new file mode 100644 index 0000000..a528b04 --- /dev/null +++ b/src/blocks/sigma_details_block.js @@ -0,0 +1,298 @@ +/** + * sigma_details_block.js + * + * Creates Slack Block Kit blocks for displaying Sigma rule explanations + * + */ +const logger = require('../utils/logger'); + +const { getFileName } = require('../utils/file_utils'); +const FILE_NAME = getFileName(__filename); + +/** + * Create Slack block kit blocks for rule explanation + * + * @param {Object} details - The rule details object containing all rule metadata + * @returns {Array} Formatted Slack blocks ready for display + */ +function getRuleExplanationBlocks(details) { + logger.debug(`${FILE_NAME}: Creating rule explanation blocks for rule: ${details?.id || 'unknown'}`); + + if (!details) { + logger.error('Failed to create explanation blocks: No details object provided'); + return [ + { + type: 'section', + text: { + type: 'mrkdwn', + text: 'Error: No explanation data provided' + } + } + ]; + } + + // Map severity levels to emojis for visual representation + const severityConfig = { + 'critical': { emoji: '🔴', text: 'Critical' }, + 'high': { emoji: '🟠', text: 'High' }, + 'medium': { emoji: '🟡', text: 'Medium' }, + 'low': { emoji: '🟢', text: 'Low' }, + 'informational': { emoji: '🔵', text: 'Info' } + }; + + // Normalize severity to lowercase for matching + const normalizedSeverity = (details.severity || '').toLowerCase(); + const severityInfo = severityConfig[normalizedSeverity] || { emoji: '⚪', text: details.severity || 'Unknown' }; + + // Create a formatted severity indicator + const severityDisplay = `${severityInfo.emoji} *${severityInfo.text}*`; + + logger.debug(`Rule severity: ${normalizedSeverity} (${severityDisplay})`); + + /** + * Format tags with MITRE links where applicable + * + * @param {Array} tags - Array of tag strings to format + * @returns {Array} Array of formatted tags with links where appropriate + */ + const formatTags = (tags = []) => { + if (!tags || tags.length === 0 || (tags.length === 2 && tags.includes('error'))) { + logger.debug('No valid tags to format'); + return []; + } + + logger.debug(`Formatting ${tags.length} tags`); + + return tags.map(tag => { + let formattedTag = tag.trim(); + let url = ''; + let displayText = formattedTag; + + // Handle MITRE ATT&CK Technique IDs + if (/^T\d{4}(\.\d{3})?$/.test(formattedTag)) { + // Technique ID (e.g., T1234 or T1234.001) + url = `https://attack.mitre.org/techniques/${formattedTag}/`; + logger.debug(`Formatted MITRE technique: ${formattedTag}`); + } + // Handle MITRE ATT&CK Tactic IDs + else if (/^TA\d{4}$/.test(formattedTag)) { + // Tactic ID (e.g., TA0001) + url = `https://attack.mitre.org/tactics/${formattedTag}/`; + logger.debug(`Formatted MITRE tactic: ${formattedTag}`); + } + // Handle CWE IDs + else if (/^S\d{4}$/.test(formattedTag)) { + // CWE ID + url = `https://cwe.mitre.org/data/definitions/${formattedTag.substring(1)}.html`; + logger.debug(`Formatted CWE: ${formattedTag}`); + } + // Handle attack.* tactics + else if (formattedTag.startsWith('attack.')) { + const tacticName = formattedTag.substring(7); // Remove 'attack.' prefix + + // Handle specific techniques with T#### format + if (/^t\d{4}(\.\d{3})?$/.test(tacticName)) { + const techniqueId = tacticName.toUpperCase(); + url = `https://attack.mitre.org/techniques/${techniqueId}/`; + displayText = techniqueId; + logger.debug(`Formatted MITRE technique from attack. format: ${techniqueId}`); + } + // Handle regular tactics + else { + // Map common tactics to their MITRE ATT&CK IDs + const tacticMappings = { + 'reconnaissance': 'TA0043', + 'resourcedevelopment': 'TA0042', + 'initialaccess': 'TA0001', + 'execution': 'TA0002', + 'persistence': 'TA0003', + 'privilegeescalation': 'TA0004', + 'defenseevasion': 'TA0005', + 'credentialaccess': 'TA0006', + 'discovery': 'TA0007', + 'lateralmovement': 'TA0008', + 'collection': 'TA0009', + 'command-and-control': 'TA0011', + 'exfiltration': 'TA0010', + 'impact': 'TA0040' + }; + + // Remove hyphens and convert to lowercase for matching + const normalizedTactic = tacticName.toLowerCase().replace(/-/g, ''); + + if (tacticMappings[normalizedTactic]) { + url = `https://attack.mitre.org/tactics/${tacticMappings[normalizedTactic]}/`; + logger.debug(`Mapped tactic ${tacticName} to ${tacticMappings[normalizedTactic]}`); + } else { + // If we don't have a specific mapping, try a search + url = `https://attack.mitre.org/search/?q=${encodeURIComponent(tacticName)}`; + logger.debug(`Created search URL for unmapped tactic: ${tacticName}`); + } + + // Format the display text with proper capitalization + displayText = tacticName.replace(/-/g, ' ') + .split(' ') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); + } + } + // Handle CVE IDs + else if (/^CVE-\d{4}-\d{4,}$/i.test(formattedTag)) { + url = `https://nvd.nist.gov/vuln/detail/${formattedTag.toUpperCase()}`; + displayText = formattedTag.toUpperCase(); + logger.debug(`Formatted CVE: ${displayText}`); + } + + if (url) { + return `<${url}|${displayText}>`; + } else { + return displayText; + } + }); + }; + + // Define header based on title - check if it contains error messages + const isErrorMessage = details.title.toLowerCase().includes('error') || + details.title.toLowerCase().includes('missing'); + + if (isErrorMessage) { + logger.warn(`Rule appears to have errors: ${details.title}`); + } + + // Start with header block + const blocks = [ + { + type: 'header', + text: { + type: 'plain_text', + text: details.title || 'Rule Explanation', + emoji: true + } + }, + { + type: 'context', + elements: [ + { + type: 'mrkdwn', + text: `ID: ${details.id || 'Unknown'}` + } + ] + }, + { + type: 'section', + fields: [ + { + type: 'mrkdwn', + text: `*Severity:* ${severityDisplay}` + }, + { + type: 'mrkdwn', + text: `*Author:* ${details.author || 'Unknown'}` + } + ] + } + ]; + + // Add divider for visual separation + blocks.push({ type: 'divider' }); + + // Add description section + blocks.push({ + type: 'section', + text: { + type: 'mrkdwn', + text: `*Description:*\n${details.description || 'No description available'}` + } + }); + + // Detection explanation section - only add if not an error case or has useful detection info + if (!isErrorMessage || details.detectionExplanation !== 'Content missing') { + blocks.push({ + type: 'section', + text: { + type: 'mrkdwn', + text: `*What This Rule Detects:*\n${details.detectionExplanation || 'No detection information available'}` + } + }); + } + + // False positives section - only add if not an error case with N/A values + if (details.falsePositives && !details.falsePositives.includes('N/A - Content missing')) { + const fpItems = Array.isArray(details.falsePositives) + ? details.falsePositives.map(item => `• ${item}`).join('\n') + : `• ${details.falsePositives}`; + + blocks.push({ + type: 'section', + text: { + type: 'mrkdwn', + text: `*Possible False Positives:*\n${fpItems || 'None specified'}` + } + }); + } + + // Add tags if they exist and are formatted + const formattedTags = formatTags(details.tags); + if (formattedTags.length > 0) { + logger.debug(`Added ${formattedTags.length} formatted tags to the block`); + blocks.push({ + type: 'context', + elements: [ + { + type: 'mrkdwn', + text: `*Tags:* ${formattedTags.join(' | ')}` + } + ] + }); + } + + // If this is an error message, add a troubleshooting section + if (isErrorMessage) { + blocks.push({ + type: 'section', + text: { + type: 'mrkdwn', + text: ':warning: *Troubleshooting:*\nThis rule appears to have issues in the database. You may want to check the rule import process or run a database maintenance task to fix this issue.' + } + }); + } + + // Add action buttons for better interactivity + blocks.push({ + type: 'actions', + elements: [ + { + type: 'button', + text: { + type: 'plain_text', + text: 'View YAML', + emoji: true + }, + action_id: 'view_yaml', + value: `view_yaml_${details.id}` + }, + { + type: 'button', + text: { + type: 'plain_text', + text: 'Convert to SIEM Rule', + emoji: true + }, + action_id: 'convert_rule_to_siem', + value: `convert_rule_to_siem_${details.id}` + }, + ] + }); + + // Add a divider at the end + blocks.push({ + type: 'divider' + }); + + logger.debug(`Created ${blocks.length} blocks for rule explanation`); + return blocks; +} + +module.exports = { + getRuleExplanationBlocks +}; diff --git a/src/blocks/sigma_search_results_block.js b/src/blocks/sigma_search_results_block.js new file mode 100644 index 0000000..4fa3ef7 --- /dev/null +++ b/src/blocks/sigma_search_results_block.js @@ -0,0 +1,206 @@ +/** + * sigma_search_results_block.js + * + * Generates Slack Block Kit blocks for displaying Sigma rule search results + * Includes pagination controls for navigating large result sets + * + * @author Fylgja Development Team + */ +const logger = require('../utils/logger'); + +const { getFileName } = require('../utils/file_utils'); +const FILE_NAME = getFileName(__filename); + +/** + * Generate blocks for Slack UI to display search results with pagination + * + * @param {string} keyword - The search keyword used for the query + * @param {Array} results - Array of rule results from the search + * @param {Object} pagination - Pagination information object + * @returns {Array} - Slack blocks for displaying results + */ +const getSearchResultBlocks = (keyword, results, pagination = {}) => { + + logger.debug(`${FILE_NAME}: Creating search result blocks for keyword: "${keyword}"`); + + // Add debug for input validation + logger.debug(`${FILE_NAME}: Results type: ${typeof results}, isArray: ${Array.isArray(results)}, length: ${Array.isArray(results) ? results.length : 'N/A'}`); + logger.debug(`${FILE_NAME}: Pagination: ${JSON.stringify(pagination)}`); + + // Ensure results is always an array + const safeResults = Array.isArray(results) ? results : []; + + // Default pagination values if not provided + const pagingInfo = { + currentPage: pagination.currentPage || 1, + pageSize: pagination.pageSize || 10, + totalPages: pagination.totalPages || 0, + totalResults: pagination.totalResults || 0, + hasMore: pagination.hasMore || false + }; + + logger.debug(`${FILE_NAME}: Processing ${safeResults.length} search results (page ${pagingInfo.currentPage} of ${pagingInfo.totalPages}, total: ${pagingInfo.totalResults})`); + + // Initialize with header block that includes pagination info + const blocks = [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": `*Search Results for "${keyword}"*\n${ + pagingInfo.totalResults > 0 + ? `Showing ${safeResults.length} of ${pagingInfo.totalResults} matching rules (page ${pagingInfo.currentPage} of ${pagingInfo.totalPages})` + : `Found ${safeResults.length} matching rules:` + }` + } + } + ]; + + // Debug log as we build blocks + logger.debug(`${FILE_NAME}: Added header block`); + + // Add blocks for each result if we have any + if (safeResults.length === 0) { + logger.debug(`${FILE_NAME}: No search results to display`); + blocks.push({ + "type": "section", + "text": { + "type": "mrkdwn", + "text": pagingInfo.totalResults > 0 + ? "No rules on this page. Try a different page." + : "No matching rules found." + } + }); + } else { + logger.debug(`${FILE_NAME}: Creating blocks for ${safeResults.length} search results`); + safeResults.forEach((rule, index) => { + // Ensure rule is an object with expected properties + const safeRule = rule || {}; + const ruleId = safeRule.id || 'unknown'; + logger.debug(`${FILE_NAME}: Adding result #${index + 1}: ${ruleId} - ${safeRule.title || 'Untitled'}`); + + // Combine rule information and action button into a single line + blocks.push({ + "type": "section", + "text": { + "type": "mrkdwn", + "text": `*${safeRule.title || 'Untitled Rule'}*\nID: \`${ruleId}\`` + }, + "accessory": { + "type": "button", + "text": { + "type": "plain_text", + "text": "Details", + "emoji": true + }, + "value": ruleId, + "action_id": "view_rule_details" + } + }); + + // Add a divider between results (except after the last one) + if (index < safeResults.length - 1) { + blocks.push({ + "type": "divider" + }); + } + }); + } + + // Debug log for pagination controls + logger.debug(`${FILE_NAME}: Checking if pagination controls needed (totalPages: ${pagingInfo.totalPages})`); + + // Add pagination navigation if there are multiple pages + if (pagingInfo.totalPages > 1) { + // Add a divider before pagination controls + blocks.push({ + "type": "divider" + }); + + // Create pagination navigation buttons + const paginationButtons = []; + + // Previous page button (if not on first page) + if (pagingInfo.currentPage > 1) { + paginationButtons.push({ + "type": "button", + "text": { + "type": "plain_text", + "text": "Previous", + "emoji": true + }, + "value": JSON.stringify({ + keyword, + page: pagingInfo.currentPage - 1, + pageSize: pagingInfo.pageSize + }), + "action_id": "search_prev_page" + }); + logger.debug(`${FILE_NAME}: Added Previous page button for page ${pagingInfo.currentPage - 1}`); + } + + // Next page button (if there are more pages) + if (pagingInfo.hasMore) { + paginationButtons.push({ + "type": "button", + "text": { + "type": "plain_text", + "text": "Next", + "emoji": true + }, + "value": JSON.stringify({ + keyword, + page: pagingInfo.currentPage + 1, + pageSize: pagingInfo.pageSize + }), + "action_id": "search_next_page" + }); + logger.debug(`${FILE_NAME}: Added Next page button for page ${pagingInfo.currentPage + 1}`); + } + + // Add the pagination buttons block if we have buttons to show + if (paginationButtons.length > 0) { + blocks.push({ + "type": "actions", + "elements": paginationButtons + }); + logger.debug(`${FILE_NAME}: Added ${paginationButtons.length} pagination buttons`); + } + + // Add page indicator text + blocks.push({ + "type": "context", + "elements": [ + { + "type": "plain_text", + "text": `Page ${pagingInfo.currentPage} of ${pagingInfo.totalPages}`, + "emoji": true + } + ] + }); + logger.debug(`${FILE_NAME}: Added page indicator text`); + } + + logger.debug(`${FILE_NAME}: Created ${blocks.length} blocks for search results`); + + // Final validation of blocks array + if (!Array.isArray(blocks) || blocks.length === 0) { + logger.error(`${FILE_NAME}: Generated blocks is not a valid array or is empty`); + // Return a minimal valid blocks array + return [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": `Search Results for "${keyword}": Unable to generate proper blocks. Please try again.` + } + } + ]; + } + + return blocks; +}; + +module.exports = { + getSearchResultBlocks +}; \ No newline at end of file diff --git a/src/blocks/sigma_stats_block.js b/src/blocks/sigma_stats_block.js new file mode 100644 index 0000000..2c3c086 --- /dev/null +++ b/src/blocks/sigma_stats_block.js @@ -0,0 +1,269 @@ +/** + * sigma_stats_block.js + * + * Creates Slack Block Kit blocks for displaying Sigma rule database statistics + */ +const logger = require('../utils/logger'); + +const { getFileName } = require('../utils/file_utils'); +const FILE_NAME = getFileName(__filename); + +/** + * Create Slack block kit blocks for statistics display + * + * @param {Object} stats - The statistics object with all statistical data + * @returns {Array} Formatted Slack blocks ready for display + */ +function getStatsBlocks(stats) { + logger.debug(`${FILE_NAME}: Creating statistics display blocks`); + + if (!stats) { + logger.error(`${FILE_NAME}: Failed to create statistics blocks: No stats object provided`); + return [ + { + type: 'section', + text: { + type: 'mrkdwn', + text: 'Error: No statistics data provided' + } + } + ]; + } + + // Format the date for display + const formatDate = (dateString) => { + if (!dateString) return 'Unknown'; + + try { + const date = new Date(dateString); + return date.toLocaleString(); + } catch (error) { + return dateString; + } + }; + + // Start with header block + const blocks = [ + { + type: 'header', + text: { + type: 'plain_text', + text: 'Sigma Rule Database Statistics', + emoji: true + } + }, + { + type: 'context', + elements: [ + { + type: 'mrkdwn', + text: `Last updated: ${formatDate(stats.lastUpdate)}` + } + ] + } + ]; + + // Add divider for visual separation + blocks.push({ type: 'divider' }); + + // Overall statistics section + blocks.push({ + type: 'section', + text: { + type: 'mrkdwn', + text: '*Overall Statistics*' + } + }); + + blocks.push({ + type: 'section', + fields: [ + { + type: 'mrkdwn', + text: `*Total Rules:* ${stats.totalRules.toLocaleString()}` + }, + { + type: 'mrkdwn', + text: `*Database Health:* ${stats.databaseHealth.contentPercentage}% Complete` + } + ] + }); + + // Add divider for visual separation + blocks.push({ type: 'divider' }); + + // Operating system breakdown + blocks.push({ + type: 'section', + text: { + type: 'mrkdwn', + text: '*Rules by Operating System*' + } + }); + + blocks.push({ + type: 'section', + fields: [ + { + type: 'mrkdwn', + text: `*Windows:* ${stats.operatingSystems.windows.toLocaleString()} rules` + }, + { + type: 'mrkdwn', + text: `*Linux:* ${stats.operatingSystems.linux.toLocaleString()} rules` + }, + { + type: 'mrkdwn', + text: `*macOS:* ${stats.operatingSystems.macos.toLocaleString()} rules` + }, + { + type: 'mrkdwn', + text: `*Other/Unknown:* ${stats.operatingSystems.other.toLocaleString()} rules` + } + ] + }); + + // Add divider for visual separation + blocks.push({ type: 'divider' }); + + // Severity breakdown + blocks.push({ + type: 'section', + text: { + type: 'mrkdwn', + text: '*Rules by Severity Level*' + } + }); + + // Create a colorful representation of severity levels + const severityEmoji = { + 'critical': '🔴', + 'high': '🟠', + 'medium': '🟡', + 'low': '🟢', + 'informational': '🔵' + }; + + let severityFields = []; + stats.severityLevels.forEach(level => { + const emoji = severityEmoji[level.level?.toLowerCase()] || '⚪'; + severityFields.push({ + type: 'mrkdwn', + text: `*${emoji} ${level.level ? (level.level.charAt(0).toUpperCase() + level.level.slice(1)) : 'Unknown'}:* ${level.count.toLocaleString()} rules` + }); + }); + + // Ensure we have an even number of fields for layout + if (severityFields.length % 2 !== 0) { + severityFields.push({ + type: 'mrkdwn', + text: ' ' // Empty space to balance fields + }); + } + + blocks.push({ + type: 'section', + fields: severityFields + }); + + // Add divider for visual separation + blocks.push({ type: 'divider' }); + + // Top MITRE ATT&CK tactics + if (stats.mitreTactics && stats.mitreTactics.length > 0) { + blocks.push({ + type: 'section', + text: { + type: 'mrkdwn', + text: '*Top MITRE ATT&CK Tactics*' + } + }); + + const mitreFields = stats.mitreTactics.map(tactic => { + // Format tactic name for better readability + const formattedTactic = tactic.tactic + .replace(/-/g, ' ') + .split(' ') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); + + return { + type: 'mrkdwn', + text: `*${formattedTactic}:* ${tactic.count.toLocaleString()} rules` + }; + }); + + // Split into multiple sections if needed for layout + for (let i = 0; i < mitreFields.length; i += 2) { + const sectionFields = mitreFields.slice(i, Math.min(i + 2, mitreFields.length)); + + // If we have an odd number at the end, add an empty field + if (sectionFields.length === 1) { + sectionFields.push({ + type: 'mrkdwn', + text: ' ' // Empty space to balance fields + }); + } + + blocks.push({ + type: 'section', + fields: sectionFields + }); + } + + blocks.push({ type: 'divider' }); + } + + // Top authors + if (stats.topAuthors && stats.topAuthors.length > 0) { + blocks.push({ + type: 'section', + text: { + type: 'mrkdwn', + text: '*Top Rule Authors*' + } + }); + + const authorFields = stats.topAuthors.map(author => ({ + type: 'mrkdwn', + text: `*${author.name || 'Unknown'}:* ${author.count.toLocaleString()} rules` + })); + + // Split into multiple sections if needed for layout + for (let i = 0; i < authorFields.length; i += 2) { + const sectionFields = authorFields.slice(i, Math.min(i + 2, authorFields.length)); + + // If we have an odd number at the end, add an empty field + if (sectionFields.length === 1) { + sectionFields.push({ + type: 'mrkdwn', + text: ' ' // Empty space to balance fields + }); + } + + blocks.push({ + type: 'section', + fields: sectionFields + }); + } + } + + // Add a footer + blocks.push({ type: 'divider' }); + blocks.push({ + type: 'context', + elements: [ + { + type: 'mrkdwn', + text: 'Use `/sigma-search [keyword]` to search for specific rules and `/sigma-details [id]` to get detailed information about a rule.' + } + ] + }); + + logger.debug(`${FILE_NAME}: Created ${blocks.length} blocks for statistics display`); + return blocks; +} + +module.exports = { + getStatsBlocks +}; \ No newline at end of file diff --git a/src/blocks/sigma_view_yaml_block.js b/src/blocks/sigma_view_yaml_block.js new file mode 100644 index 0000000..79351cb --- /dev/null +++ b/src/blocks/sigma_view_yaml_block.js @@ -0,0 +1,83 @@ +/** + * sigma_view_yaml_block.js + * + * Creates Slack Block Kit blocks for displaying Sigma rule YAML content + * + */ +const logger = require('../utils/logger'); + +const { getFileName } = require('../utils/file_utils'); +const FILE_NAME = getFileName(__filename); + +/** + * Create Slack block kit blocks for displaying YAML content + * + * @param {string} ruleId - The ID of the rule + * @param {string} yamlContent - The YAML content to display + * @returns {Array} Formatted Slack blocks ready for display + */ +function getYamlViewBlocks(ruleId, yamlContent) { + logger.debug(`${FILE_NAME}: Creating YAML view blocks for rule: ${ruleId || 'unknown'}`); + + if (!yamlContent) { + logger.warn(`${FILE_NAME}: Empty YAML content for rule: ${ruleId}`); + return [ + { + type: 'header', + text: { + type: 'plain_text', + text: `YAML for Rule: ${ruleId}`, + emoji: true + } + }, + { + type: 'section', + text: { + type: 'mrkdwn', + text: '_No YAML content available_' + } + } + ]; + } + + const blocks = [ + { + type: 'header', + text: { + type: 'plain_text', + text: `YAML for Rule: ${ruleId}`, + emoji: true + } + }, + { + type: 'section', + text: { + type: 'mrkdwn', + text: '```\n' + yamlContent + '```' + } + } + ]; + + blocks.push({ + type: 'actions', + elements: [ + { + type: 'button', + text: { + type: 'plain_text', + text: 'Convert to SIEM Rule', + emoji: true + }, + action_id: 'convert_rule_to_siem', + value: `convert_rule_to_siem_${ruleId}` + }, + ] + }); + + logger.debug(`${FILE_NAME}: Created ${blocks.length} blocks for YAML view`); + return blocks; +} + +module.exports = { + getYamlViewBlocks +}; \ No newline at end of file diff --git a/src/config/appConfig.js b/src/config/appConfig.js new file mode 100644 index 0000000..1ece4b9 --- /dev/null +++ b/src/config/appConfig.js @@ -0,0 +1,83 @@ +const path = require('path'); +const fs = require('fs'); +const yaml = require('js-yaml'); + +// Load YAML configuration file +let yamlConfig = {}; +try { + const configPath = path.join(__dirname, '..', '..', 'fylgja.yml'); + const fileContents = fs.readFileSync(configPath, 'utf8'); + yamlConfig = yaml.load(fileContents); + console.log('Successfully loaded fylgja.yml configuration'); +} catch (error) { + console.error(`Error loading fylgja.yml: ${error.message}`); + console.log('Using default configuration values'); + // Default values will be used if file cannot be loaded +} + +// Base directory for resolving relative paths from config +const baseDir = path.join(__dirname, '..', '..'); + +// Configuration paths +module.exports = { + + SLACK_CONFIG: { + botToken: yamlConfig?.slack?.bot_token || process.env.SLACK_BOT_TOKEN, + signingSecret: yamlConfig?.slack?.signing_secret || process.env.SLACK_SIGNING_SECRET + }, + + // Server configuration from YAML (with fallback to env vars) + SERVER_CONFIG: { + port: parseInt(yamlConfig?.server?.port || process.env.PORT || 3000) + }, + + // Path configurations + SIGMA_REPO_DIR: yamlConfig?.paths?.sigma_repo_dir + ? path.resolve(baseDir, yamlConfig.paths.sigma_repo_dir) + : path.join(baseDir, 'sigma-repo'), + + DB_PATH: yamlConfig?.paths?.db_path + ? path.resolve(baseDir, yamlConfig.paths.db_path) + : path.resolve(baseDir, 'sigma.db'), + + // Load SIGMA_CLI_PATH from YAML, env, or use default path + SIGMA_CLI_PATH: yamlConfig?.sigma?.['sigma-cli']?.path + ? path.resolve(baseDir, yamlConfig.sigma['sigma-cli'].path) + : path.join(process.env.VIRTUAL_ENV || './.venv', 'bin', 'sigma'), + + // Sigma CLI configuration from YAML + SIGMA_CLI_CONFIG: { + backend: yamlConfig?.sigma?.['sigma-cli']?.backend || "lucene", + target: yamlConfig?.sigma?.['sigma-cli']?.target || "ecs_windows", + format: yamlConfig?.sigma?.['sigma-cli']?.format || "siem_rule_ndjson" + }, + + // Sigma Repository configuration from YAML + SIGMA_REPO_CONFIG: { + url: yamlConfig?.sigma?.repo?.url || "https://github.com/SigmaHQ/sigma.git", + branch: yamlConfig?.sigma?.repo?.branch || "main" + }, + + // Elasticsearch configuration from YAML + ELASTICSEARCH_CONFIG: { + apiEndpoint: yamlConfig?.elastic?.['api-endpoint'] || + "http://localhost:5601/api/detection_engine/rules", + credentials: yamlConfig?.elastic?.['elastic-authentication-credentials'] || + "elastic:changeme" + }, + + // Logging configuration from YAML + LOGGING_CONFIG: { + level: yamlConfig?.logging?.level || "info", + file: yamlConfig?.logging?.file || "./logs/fylgja.log" + }, + + // Default configuration (fallback) + DEFAULT_CONFIG: { + siem: 'elasticsearch', + lang: 'lucene', + output: 'ndjson', + repoUrl: yamlConfig?.sigma?.repo?.url || "https://github.com/SigmaHQ/sigma.git", + repoBranch: yamlConfig?.sigma?.repo?.branch || "main" + } +}; \ No newline at end of file diff --git a/src/handlers/alerts/alerts_handler.js b/src/handlers/alerts/alerts_handler.js new file mode 100644 index 0000000..e69de29 diff --git a/src/handlers/case/case_handler.js b/src/handlers/case/case_handler.js new file mode 100644 index 0000000..e69de29 diff --git a/src/handlers/config/config_handler.js b/src/handlers/config/config_handler.js new file mode 100644 index 0000000..ff58672 --- /dev/null +++ b/src/handlers/config/config_handler.js @@ -0,0 +1,88 @@ +// +// config_handler.js +// handle the /sigma-config command +// +const util = require('util'); +const { exec } = require('child_process'); +const { SIGMA_CLI_PATH } = require('../../config/constants'); +const { loadConfig, updateConfig } = require('../../config/config-manager'); +const { updateSigmaDatabase } = require('../../services/sigma/sigma_repository_service'); +const logger = require('../../utils/logger'); + +// Promisify exec for async/await usage +const execPromise = util.promisify(exec); + +module.exports = (app) => { + app.command('/sigma-config', async ({ command, ack, respond }) => { + await ack(); + logger.info(`Sigma config command received: ${command.text}`); + + const args = command.text.split(' '); + + if (args.length === 0 || args[0] === '') { + // Display current configuration + const config = loadConfig(); + logger.info('Displaying current configuration'); + await respond(`Current configuration:\nSIEM: ${config.siem}\nLanguage: ${config.lang}\nOutput: ${config.output}`); + return; + } + + const configType = args[0]; + + if (configType === 'update') { + logger.info('Starting database update from command'); + try { + await respond('Updating Sigma database... This may take a moment.'); + await updateSigmaDatabase(); + logger.info('Database update completed from command'); + await respond('Sigma database updated successfully'); + } catch (error) { + logger.error(`Database update failed: ${error.message}`); + await respond(`Error updating Sigma database: ${error.message}`); + } + return; + } + + if (args.length < 2) { + logger.warn(`Invalid config command format: ${command.text}`); + await respond(`Invalid command format. Usage: /sigma-config ${configType} [value]`); + return; + } + + const configValue = args[1]; + const config = loadConfig(); + + if (configType === 'siem') { + // Verify the SIEM backend is installed + logger.info(`Attempting to change SIEM to: ${configValue}`); + try { + await execPromise(`${SIGMA_CLI_PATH} list targets | grep ${configValue}`); + updateConfig('siem', configValue); + logger.info(`SIEM configuration updated to: ${configValue}`); + await respond(`SIEM configuration updated to: ${configValue}`); + } catch (error) { + logger.error(`SIEM backend '${configValue}' not found or not installed`); + await respond(`Error: SIEM backend '${configValue}' not found or not installed. Please install it with: sigma plugin install ${configValue}`); + } + } else if (configType === 'lang') { + logger.info(`Changing language to: ${configValue}`); + updateConfig('lang', configValue); + await respond(`Language configuration updated to: ${configValue}`); + } else if (configType === 'output') { + // Check if output format is supported by the current backend + logger.info(`Attempting to change output format to: ${configValue}`); + try { + await execPromise(`${SIGMA_CLI_PATH} list formats ${config.siem} | grep ${configValue}`); + updateConfig('output', configValue); + logger.info(`Output configuration updated to: ${configValue}`); + await respond(`Output configuration updated to: ${configValue}`); + } catch (error) { + logger.error(`Output format '${configValue}' not supported by SIEM backend '${config.siem}'`); + await respond(`Error: Output format '${configValue}' not supported by SIEM backend '${config.siem}'. Run 'sigma list formats ${config.siem}' to see available formats.`); + } + } else { + logger.warn(`Unknown configuration type: ${configType}`); + await respond(`Unknown configuration type: ${configType}. Available types: siem, lang, output, update`); + } + }); +}; \ No newline at end of file diff --git a/src/handlers/sigma/sigma_action_handlers.js b/src/handlers/sigma/sigma_action_handlers.js new file mode 100644 index 0000000..6bd1a13 --- /dev/null +++ b/src/handlers/sigma/sigma_action_handlers.js @@ -0,0 +1,552 @@ +/** + * sigma_action_handlers.js + * + * Centralized action handlers for Sigma-related Slack interactions + */ +const logger = require('../../utils/logger'); +const { handleError } = require('../../utils/error_handler'); +const { explainSigmaRule, getSigmaRuleYaml } = require('../../services/sigma/sigma_details_service'); +const { convertRuleToBackend } = require('../../services/sigma/sigma_backend_converter'); +const { searchSigmaRules } = require('../../services/sigma/sigma_search_service'); +const { getYamlViewBlocks } = require('../../blocks/sigma_view_yaml_block'); +const { getSearchResultBlocks } = require('../../blocks/sigma_search_results_block'); +const { getConversionResultBlocks } = require('../../blocks/sigma_conversion_block'); +const { getRuleExplanationBlocks } = require('../../blocks/sigma_details_block'); +const { sendRuleToSiem } = require('../../services/elastic/elastic_api_service'); + +const { SIGMA_CLI_CONFIG, ELASTICSEARCH_CONFIG } = require('../../config/appConfig'); + +const FILE_NAME = 'sigma_action_handlers.js'; + +/** + * Process and display details for a Sigma rule + * + * @param {string} ruleId - The ID of the rule to get details for + * @param {Function} respond - Function to send response back to Slack + * @param {boolean} replaceOriginal - Whether to replace the original message + * @param {string} responseType - Response type (ephemeral or in_channel) + * @returns {Promise} + */ +const processRuleDetails = async (ruleId, respond, replaceOriginal = false, responseType = 'in_channel') => { + try { + if (!ruleId) { + logger.warn(`${FILE_NAME}: Missing rule ID in processRuleDetails`); + await respond({ + text: 'Error: Missing rule ID for details', + replace_original: replaceOriginal, + response_type: responseType + }); + return; + } + + logger.info(`${FILE_NAME}: Processing details for sigma rule: ${ruleId}`); + + // Get Sigma rule details + logger.info(`${FILE_NAME}: Calling explainSigmaRule with ID: '${ruleId}'`); + const result = await explainSigmaRule(ruleId); + + if (!result.success) { + logger.error(`${FILE_NAME}: Rule details retrieval failed: ${result.message}`); + await respond({ + text: `Error: ${result.message}`, + replace_original: replaceOriginal, + response_type: responseType + }); + return; + } + + if (!result.explanation) { + logger.error(`${FILE_NAME}: Rule details succeeded but no explanation object was returned`); + await respond({ + text: 'Error: Generated details were empty', + replace_original: replaceOriginal, + response_type: responseType + }); + return; + } + + logger.info(`${FILE_NAME}: Rule ${ruleId} details retrieved successfully`); + + // Generate blocks + let blocks; + try { + blocks = getRuleExplanationBlocks(result.explanation); + } catch (blockError) { + await handleError(blockError, `${FILE_NAME}: Block generation`, respond, { + replaceOriginal: replaceOriginal, + responseType: responseType, + customMessage: `Rule ${result.explanation.id}: ${result.explanation.title}\n${result.explanation.description}` + }); + return; + } + + // Respond with the details + await respond({ + blocks: blocks, + replace_original: replaceOriginal, + response_type: responseType + }); + } catch (error) { + await handleError(error, `${FILE_NAME}: Process rule details`, respond, { + replaceOriginal: replaceOriginal, + responseType: responseType + }); + } +}; + +/** + * Process and convert a Sigma rule to the target backend format + * + * @param {string} ruleId - The ID of the rule to convert + * @param {Object} config - Configuration for the conversion (backend, target, format) + * @param {Function} respond - Function to send response back to Slack + * @param {boolean} replaceOriginal - Whether to replace the original message + * @param {string} responseType - Response type (ephemeral or in_channel) + * @returns {Promise} + */ +const processRuleConversion = async (ruleId, config, respond, replaceOriginal = false, responseType = 'in_channel') => { + try { + if (!ruleId) { + logger.warn(`${FILE_NAME}: Missing rule ID in processRuleConversion`); + await respond({ + text: 'Error: Missing rule ID for conversion', + replace_original: replaceOriginal, + response_type: responseType + }); + return; + } + + logger.info(`${FILE_NAME}: Processing conversion for sigma rule: ${ruleId}`); + + // Set default configuration from YAML config if not provided + const conversionConfig = config || { + backend: SIGMA_CLI_CONFIG.backend, + target: SIGMA_CLI_CONFIG.target, + format: SIGMA_CLI_CONFIG.format + }; + + await respond({ + text: `Converting rule ${ruleId} using ${conversionConfig.backend}/${conversionConfig.target} to ${conversionConfig.format}...`, + replace_original: replaceOriginal, + response_type: 'ephemeral' + }); + + // Get the rule and convert it + const conversionResult = await convertRuleToBackend(ruleId, conversionConfig); + + if (!conversionResult.success) { + logger.error(`${FILE_NAME}: Rule conversion failed: ${conversionResult.message}`); + await respond({ + text: `Error: ${conversionResult.message}`, + replace_original: replaceOriginal, + response_type: responseType + }); + return; + } + + // Generate blocks for displaying the result + let blocks; + try { + blocks = getConversionResultBlocks(conversionResult); + } catch (blockError) { + await handleError(blockError, `${FILE_NAME}: Block generation`, respond, { + replaceOriginal: replaceOriginal, + responseType: responseType, + customMessage: `Rule ${ruleId} converted successfully. Use the following output with your SIEM:\n\`\`\`\n${conversionResult.output}\n\`\`\`` + }); + return; + } + + // Respond with the conversion result + await respond({ + blocks: blocks, + replace_original: replaceOriginal, + response_type: responseType + }); + } catch (error) { + await handleError(error, `${FILE_NAME}: Process rule conversion`, respond, { + replaceOriginal: replaceOriginal, + responseType: responseType + }); + } +}; + +/** + * Handle pagination actions (Previous, Next) + * + * @param {Object} body - The action payload body + * @param {Function} ack - Function to acknowledge the action + * @param {Function} respond - Function to send response + */ +const handlePaginationAction = async (body, ack, respond) => { + try { + await ack(); + logger.debug(`${FILE_NAME}: Pagination action received: ${JSON.stringify(body.actions)}`); + + if (!body || !body.actions || !body.actions[0] || !body.actions[0].value) { + logger.error(`${FILE_NAME}: Invalid pagination action payload: missing parameters`); + await respond({ + text: 'Error: Could not process pagination request', + replace_original: false + }); + return; + } + + // Parse the action value which contains our pagination parameters + const action = body.actions[0]; + let valueData; + + try { + valueData = JSON.parse(action.value); + } catch (parseError) { + await handleError(parseError, `${FILE_NAME}: Pagination value parsing`, respond, { + replaceOriginal: false, + customMessage: 'Error: Invalid pagination parameters' + }); + return; + } + + const { keyword, page, pageSize } = valueData; + + if (!keyword) { + logger.warn(`${FILE_NAME}: Missing keyword in pagination action`); + await respond({ + text: 'Error: Missing search keyword in pagination request', + replace_original: false + }); + return; + } + + logger.info(`${FILE_NAME}: Processing pagination request for "${keyword}" (page ${page}, size ${pageSize})`); + + // Perform the search with the new pagination parameters + const searchResult = await searchSigmaRules(keyword, page, pageSize); + + if (!searchResult.success) { + logger.error(`${FILE_NAME}: Search failed during pagination: ${searchResult.message}`); + await respond({ + text: `Error: ${searchResult.message}`, + replace_original: false + }); + return; + } + + // Generate the updated blocks for the search results + let blocks; + try { + blocks = getSearchResultBlocks( + keyword, + searchResult.results, + searchResult.pagination + ); + } catch (blockError) { + await handleError(blockError, `${FILE_NAME}: Pagination block generation`, respond, { + replaceOriginal: false, + customMessage: `Error generating results view: ${blockError.message}` + }); + return; + } + + // Return the response that will update the original message + await respond({ + blocks: blocks, + replace_original: true + }); + } catch (error) { + await handleError(error, `${FILE_NAME}: Pagination action handler`, respond, { + replaceOriginal: false + }); + } +}; + +/** + * Register all Sigma-related action handlers + * + * @param {Object} app - The Slack app instance + */ +const registerActionHandlers = (app) => { + logger.info(`${FILE_NAME}: Registering consolidated sigma action handlers`); + + // Handle "Send to SIEM" button clicks + app.action('send_sigma_rule_to_siem', async ({ body, ack, respond }) => { + try { + await ack(); + logger.debug(`${FILE_NAME}: send_sigma_rule_to_siem action received: ${JSON.stringify(body.actions)}`); + + if (!body || !body.actions || !body.actions[0] || !body.actions[0].value) { + logger.error(`${FILE_NAME}: Invalid action payload: missing rule ID`); + await respond({ + text: 'Error: Could not determine which rule to send', + replace_original: false, + response_type: 'ephemeral' + }); + return; + } + + // Extract rule ID from action value + // Value format is "send_sigma_rule_to_siem_[ruleID]" + const actionValue = body.actions[0].value; + const ruleId = actionValue.replace('send_sigma_rule_to_siem_', ''); + + if (!ruleId) { + logger.error(`${FILE_NAME}: Missing rule ID in action value: ${actionValue}`); + await respond({ + text: 'Error: Missing rule ID in button data', + replace_original: false, + response_type: 'ephemeral' + }); + return; + } + + logger.info(`${FILE_NAME}: Sending rule ${ruleId} to SIEM`); + + // Inform user that processing is happening + await respond({ + text: `Sending rule ${ruleId} to Elasticsearch SIEM...`, + replace_original: false, + response_type: 'ephemeral' + }); + + // Get the converted rule in Elasticsearch format using config from YAML + const config = { + backend: SIGMA_CLI_CONFIG.backend, + target: SIGMA_CLI_CONFIG.target, + format: SIGMA_CLI_CONFIG.format + }; + + logger.info(`${FILE_NAME}: Converting rule ${ruleId} for SIEM export`); + const conversionResult = await convertRuleToBackend(ruleId, config); + + if (!conversionResult.success) { + logger.error(`${FILE_NAME}: Rule conversion failed: ${conversionResult.message}`); + await respond({ + text: `Error: Failed to convert rule for SIEM: ${conversionResult.message}`, + replace_original: false, + response_type: 'ephemeral' + }); + return; + } + + // Parse the converted rule JSON + let rulePayload; + try { + rulePayload = JSON.parse(conversionResult.output); + + // Add required fields if not present + rulePayload.rule_id = rulePayload.rule_id || ruleId; + rulePayload.from = rulePayload.from || "now-360s"; + rulePayload.to = rulePayload.to || "now"; + rulePayload.interval = rulePayload.interval || "5m"; + + // Make sure required fields are present + if (!rulePayload.name) { + rulePayload.name = conversionResult.rule?.title || `Sigma Rule ${ruleId}`; + } + + if (!rulePayload.description) { + rulePayload.description = conversionResult.rule?.description || + `Converted from Sigma rule: ${ruleId}`; + } + + if (!rulePayload.risk_score) { + // Map Sigma level to risk score + const levelMap = { + 'critical': 90, + 'high': 73, + 'medium': 50, + 'low': 25, + 'informational': 10 + }; + + rulePayload.risk_score = levelMap[conversionResult.rule?.level] || 50; + } + + if (!rulePayload.severity) { + rulePayload.severity = conversionResult.rule?.level || 'medium'; + } + + if (!rulePayload.enabled) { + rulePayload.enabled = true; + } + + } catch (parseError) { + logger.error(`${FILE_NAME}: Failed to parse converted rule JSON: ${parseError.message}`); + await respond({ + text: `Error: The converted rule is not valid JSON: ${parseError.message}`, + replace_original: false, + response_type: 'ephemeral' + }); + return; + } + + // Send the rule to Elasticsearch using api service + try { + const result = await sendRuleToSiem(rulePayload); + + if (result.success) { + logger.info(`${FILE_NAME}: Successfully sent rule ${ruleId} to SIEM`); + await respond({ + text: `✅ Success! Rule "${rulePayload.name}" has been added to your Elasticsearch SIEM.`, + replace_original: false, + response_type: 'in_channel' + }); + } else { + logger.error(`${FILE_NAME}: Error sending rule to SIEM: ${result.message}`); + await respond({ + text: `Error: Failed to add rule to SIEM: ${result.message}`, + replace_original: false, + response_type: 'ephemeral' + }); + } + } catch (error) { + await handleError(error, `${FILE_NAME}: send_sigma_rule_to_siem action`, respond, { + replaceOriginal: false + }); + } + } catch (error) { + await handleError(error, `${FILE_NAME}: send_sigma_rule_to_siem action`, respond, { + replaceOriginal: false + }); + } + }); + + // Handle View YAML button clicks + app.action('view_yaml', async ({ body, ack, respond }) => { + logger.info(`${FILE_NAME}: VIEW_YAML ACTION TRIGGERED`); + try { + await ack(); + logger.debug(`${FILE_NAME}: View YAML action received: ${JSON.stringify(body.actions)}`); + + if (!body || !body.actions || !body.actions[0] || !body.actions[0].value) { + logger.error(`${FILE_NAME}: Invalid action payload: missing rule ID`); + await respond({ + text: 'Error: Could not determine which rule to get YAML for', + replace_original: false + }); + return; + } + + // Extract rule ID from button value + // Handle both formats: direct ID from search results or view_yaml_{ruleId} from details view + let ruleId = body.actions[0].value; + if (ruleId.startsWith('view_yaml_')) { + ruleId = ruleId.replace('view_yaml_', ''); + } + + logger.info(`${FILE_NAME}: View YAML button clicked for rule: ${ruleId}`); + + // Get Sigma rule YAML + const result = await getSigmaRuleYaml(ruleId); + logger.debug(`${FILE_NAME}: YAML retrieval result: ${JSON.stringify(result, null, 2)}`); + + if (!result.success) { + logger.error(`${FILE_NAME}: Rule YAML retrieval failed: ${result.message}`); + await respond({ + text: `Error: ${result.message}`, + replace_original: false + }); + return; + } + + logger.info(`${FILE_NAME}: Rule ${ruleId} YAML retrieved successfully via button click`); + + // Use the module to generate blocks + const blocks = getYamlViewBlocks(ruleId, result.yaml || ''); + + // Respond with the YAML content + await respond({ + blocks: blocks, + replace_original: false + }); + } catch (error) { + await handleError(error, `${FILE_NAME}: View YAML action`, respond, { + replaceOriginal: false + }); + } + }); + + // Handle convert_rule_to_siem button clicks + app.action('convert_rule_to_siem', async ({ body, ack, respond }) => { + try { + await ack(); + logger.debug(`${FILE_NAME}: convert_rule_to_siem action received: ${JSON.stringify(body.actions)}`); + + if (!body || !body.actions || !body.actions[0] || !body.actions[0].value) { + logger.error(`${FILE_NAME}: Invalid action payload: missing rule ID`); + await respond({ + text: 'Error: Could not determine which rule to convert', + replace_original: false + }); + return; + } + + // Extract rule ID from button value + const ruleId = body.actions[0].value.replace('convert_rule_to_siem_', ''); + logger.info(`${FILE_NAME}: convert_rule_to_siem button clicked for rule: ${ruleId}`); + + const config = { + backend: 'lucene', + target: 'ecs_windows', + format: 'siem_rule_ndjson' + }; + + await processRuleConversion(ruleId, config, respond, false, 'in_channel'); + } catch (error) { + await handleError(error, `${FILE_NAME}: convert_rule_to_siem action`, respond, { + replaceOriginal: false + }); + } + }); + + + // Handle "View Rule Details" button clicks from search results + app.action('view_rule_details', async ({ body, ack, respond }) => { + logger.info(`${FILE_NAME}: VIEW_RULE_DETAILS ACTION TRIGGERED`); + try { + await ack(); + logger.debug(`${FILE_NAME}: View Rule Details action received: ${JSON.stringify(body.actions)}`); + + if (!body || !body.actions || !body.actions[0] || !body.actions[0].value) { + logger.error(`${FILE_NAME}: Invalid action payload: missing rule ID`); + await respond({ + text: 'Error: Could not determine which rule to explain', + replace_original: false + }); + return; + } + + const ruleId = body.actions[0].value; + logger.info(`${FILE_NAME}: Rule details button clicked for rule ID: ${ruleId}`); + + // Inform user we're processing + await respond({ + text: `Processing details for rule ${ruleId}...`, + replace_original: false, + response_type: 'ephemeral' + }); + + await processRuleDetails(ruleId, respond, false, 'in_channel'); + } catch (error) { + await handleError(error, `${FILE_NAME}: View rule details action`, respond, { + replaceOriginal: false + }); + } + }); + + // Handle pagination button clicks + app.action('search_prev_page', async ({ body, ack, respond }) => { + await handlePaginationAction(body, ack, respond); + }); + + app.action('search_next_page', async ({ body, ack, respond }) => { + await handlePaginationAction(body, ack, respond); + }); + + logger.info(`${FILE_NAME}: All sigma action handlers registered successfully`); +}; + +module.exports = { + registerActionHandlers, + processRuleDetails, + processRuleConversion +}; \ No newline at end of file diff --git a/src/handlers/sigma/sigma_create_handler.js b/src/handlers/sigma/sigma_create_handler.js new file mode 100644 index 0000000..48384a1 --- /dev/null +++ b/src/handlers/sigma/sigma_create_handler.js @@ -0,0 +1,62 @@ +/** + * sigma_create_handler.js + * + * Handles Sigma rule conversion requests from Slack commands + * Action handlers moved to sigma_action_handlers.js + */ +const logger = require('../../utils/logger'); +const { handleError } = require('../../utils/error_handler'); +const { processRuleConversion } = require('./sigma_action_handlers'); +const { SIGMA_CLI_CONFIG } = require('../../config/appConfig'); + +const FILE_NAME = 'sigma_create_handler.js'; + +/** + * Handle the sigma-create command for converting Sigma rules + * + * @param {Object} command - The Slack command object + * @param {Function} respond - Function to send response back to Slack + */ +const handleCommand = async (command, respond) => { + try { + logger.debug(`${FILE_NAME}: Processing sigma-create command: ${JSON.stringify(command.text)}`); + + if (!command || !command.text) { + logger.warn(`${FILE_NAME}: Empty command received for sigma-create`); + await respond('Invalid command. Usage: /sigma-create [id]'); + return; + } + + // Extract rule ID and parameters + const params = command.text.trim().split(/\s+/); + const ruleId = params[0]; + + if (!ruleId) { + logger.warn(`${FILE_NAME}: Missing rule ID in sigma-create command`); + await respond('Invalid command: missing rule ID. Usage: /sigma-create [id]'); + return; + } + + await respond({ + text: 'Processing your request... This may take a moment.', + response_type: 'ephemeral' + }); + + // Use configuration from YAML through constants + const config = { + backend: SIGMA_CLI_CONFIG.backend, + target: SIGMA_CLI_CONFIG.target, + format: SIGMA_CLI_CONFIG.format + }; + + await processRuleConversion(ruleId, config, respond, false, 'in_channel'); + } catch (error) { + await handleError(error, `${FILE_NAME}: Create command handler`, respond, { + responseType: 'ephemeral' + }); + } +}; + +module.exports = { + handleCommand +}; \ No newline at end of file diff --git a/src/handlers/sigma/sigma_details_handler.js b/src/handlers/sigma/sigma_details_handler.js new file mode 100644 index 0000000..ce5bbb7 --- /dev/null +++ b/src/handlers/sigma/sigma_details_handler.js @@ -0,0 +1,56 @@ +/** + * sigma_details_handler.js + * + * Handles Sigma rule details requests from Slack commands + * Processes requests for rule explanations + */ +const logger = require('../../utils/logger'); +const { handleError } = require('../../utils/error_handler'); +const { explainSigmaRule } = require('../../services/sigma/sigma_details_service'); +const { processRuleDetails } = require('./sigma_action_handlers'); + +const FILE_NAME = 'sigma_details_handler.js'; + +/** + * Handle the sigma-details command for Sigma rules + * + * @param {Object} command - The Slack command object + * @param {Function} respond - Function to send response back to Slack + */ +const handleCommand = async (command, respond) => { + try { + logger.debug(`${FILE_NAME}: Processing sigma-details command: ${JSON.stringify(command.text)}`); + + if (!command || !command.text) { + logger.warn(`${FILE_NAME}: Empty command received for sigma-details`); + await respond('Invalid command. Usage: /sigma-details [id]'); + return; + } + + // Extract rule ID + const ruleId = command.text.trim(); + + if (!ruleId) { + logger.warn(`${FILE_NAME}: Missing rule ID in sigma-details command`); + await respond('Invalid command: missing rule ID. Usage: /sigma-details [id]'); + return; + } + + // Inform user we're processing + await respond({ + text: 'Processing your request... This may take a moment.', + response_type: 'ephemeral' + }); + + // Use the shared processRuleDetails function from action handlers + await processRuleDetails(ruleId, respond, false, 'in_channel'); + } catch (error) { + await handleError(error, `${FILE_NAME}: Details command handler`, respond, { + responseType: 'ephemeral' + }); + } +}; + +module.exports = { + handleCommand +}; \ No newline at end of file diff --git a/src/handlers/sigma/sigma_search_handler.js b/src/handlers/sigma/sigma_search_handler.js new file mode 100644 index 0000000..574a8bf --- /dev/null +++ b/src/handlers/sigma/sigma_search_handler.js @@ -0,0 +1,171 @@ +/** + * sigma_search_handler.js + * + * Handles Sigma rule search requests from Slack commands + */ +const { searchSigmaRules } = require('../../services/sigma/sigma_search_service'); +const logger = require('../../utils/logger'); +const { handleError } = require('../../utils/error_handler'); +const { getSearchResultBlocks } = require('../../blocks/sigma_search_results_block'); + +const { getFileName } = require('../../utils/file_utils'); +const FILE_NAME = getFileName(__filename); + +const MAX_RESULTS_PER_PAGE = 10; +const MAX_RESULTS_THRESHOLD = 99; + +/** + * Handle the sigma-search command for Sigma rules + * Searches for rules based on keywords and displays results with pagination + * + * @param {Object} command - The Slack command object + * @param {Function} respond - Function to send response back to Slack + */ +const handleCommand = async (command, respond) => { + try { + logger.debug(`${FILE_NAME}: Processing sigma-search command: ${JSON.stringify(command.text)}`); + + if (!command || !command.text) { + logger.warn(`${FILE_NAME}: Empty command received for sigma-search`); + await respond('Invalid command. Usage: /sigma-search [keyword]'); + return; + } + + // Extract search keyword and check for pagination parameters + let keyword = command.text.trim(); + let page = 1; + let pageSize = MAX_RESULTS_PER_PAGE; + + // Check for pagination format: keyword page=X + const pagingMatch = keyword.match(/(.+)\s+page=(\d+)$/i); + if (pagingMatch) { + keyword = pagingMatch[1].trim(); + page = parseInt(pagingMatch[2], 10) || 1; + logger.debug(`${FILE_NAME}: Detected pagination request: "${keyword}" page ${page}`); + } + + // Check for page size format: keyword limit=X + const limitMatch = keyword.match(/(.+)\s+limit=(\d+)$/i); + if (limitMatch) { + keyword = limitMatch[1].trim(); + pageSize = parseInt(limitMatch[2], 10) || MAX_RESULTS_PER_PAGE; + // Ensure the page size is within reasonable limits + pageSize = Math.min(Math.max(pageSize, 1), 100); + logger.debug(`${FILE_NAME}: Detected page size request: "${keyword}" limit ${pageSize}`); + } + + if (!keyword) { + logger.warn(`${FILE_NAME}: Missing keyword in sigma-search command`); + await respond('Invalid command: missing keyword. Usage: /sigma-search [keyword]'); + return; + } + + logger.info(`${FILE_NAME}: Searching for rules with keyword: ${keyword} (page ${page}, size ${pageSize})`); + logger.debug(`${FILE_NAME}: Search keyword length: ${keyword.length}`); + + await respond({ + text: 'Searching for rules... This may take a moment.', + response_type: 'ephemeral' + }); + + // Search for rules using the service function with pagination + const searchResult = await searchSigmaRules(keyword, page, pageSize); + + logger.debug(`${FILE_NAME}: Search result status: ${searchResult.success}`); + logger.debug(`${FILE_NAME}: Found ${searchResult.results?.length || 0} results out of ${searchResult.pagination?.totalResults || 0} total matches`); + + logger.debug(`${FILE_NAME}: About to generate blocks for search results`); + + if (!searchResult.success) { + logger.error(`${FILE_NAME}: Search failed: ${searchResult.message}`); + await respond({ + text: `Search failed: ${searchResult.message}`, + response_type: 'ephemeral' + }); + return; + } + + // Get total count for validation + const totalCount = searchResult.pagination?.totalResults || 0; + + // Check if search returned too many results + if (totalCount > MAX_RESULTS_THRESHOLD) { + logger.warn(`${FILE_NAME}: Search for "${keyword}" returned too many results (${totalCount}), displaying first page with warning`); + + // Continue processing but add a notification + searchResult.tooManyResults = true; + } + + if (!searchResult.results || searchResult.results.length === 0) { + if (totalCount > 0) { + logger.warn(`${FILE_NAME}: No rules found on page ${page} for "${keyword}", but ${totalCount} total matches exist`); + await respond({ + text: `No rules found on page ${page} for "${keyword}". Try a different page or refine your search.`, + response_type: 'ephemeral' + }); + } else { + logger.warn(`${FILE_NAME}: No rules found matching "${keyword}"`); + await respond({ + text: `No rules found matching "${keyword}"`, + response_type: 'ephemeral' + }); + } + return; + } + + // Generate blocks with pagination support + let blocks; + try { + logger.debug(`${FILE_NAME}: Calling getSearchResultBlocks with ${searchResult.results.length} results`); + + // If we have too many results, add a warning block at the beginning + if (searchResult.tooManyResults) { + blocks = getSearchResultBlocks(keyword, searchResult.results, searchResult.pagination); + + // Insert warning at the beginning of blocks (after the header) + blocks.splice(1, 0, { + "type": "section", + "text": { + "type": "mrkdwn", + "text": `:warning: Your search for "${keyword}" returned ${totalCount} results, which is a lot. Displaying the first page. Consider using a more specific keyword for narrower results.` + } + }); + } else { + blocks = getSearchResultBlocks(keyword, searchResult.results, searchResult.pagination); + } + + logger.debug(`${FILE_NAME}: Successfully generated ${blocks?.length || 0} blocks`); + } catch (blockError) { + // Use error handler for block generation errors + await handleError(blockError, `${FILE_NAME}: Block generation`, respond, { + responseType: 'in_channel', + customMessage: `Found ${searchResult.results.length} of ${totalCount} rules matching "${keyword}" (page ${page} of ${searchResult.pagination?.totalPages || 1}). Use /sigma-details [id] to view details.` + }); + return; + } + + // Add debug log before sending response + logger.debug(`${FILE_NAME}: About to send response with ${blocks?.length || 0} blocks`); + + // Determine if this should be visible to everyone or just the user + const isEphemeral = totalCount > 20; + + // Respond with the search results + await respond({ + blocks: blocks, + response_type: isEphemeral ? 'ephemeral' : 'in_channel' + }); + + // Add debug log after sending response + logger.debug(`${FILE_NAME}: Response sent successfully`); + } catch (error) { + // Use error handler for unexpected errors + await handleError(error, `${FILE_NAME}: Search command handler`, respond, { + responseType: 'ephemeral' + }); + } +}; + +module.exports = { + handleCommand +}; \ No newline at end of file diff --git a/src/handlers/sigma/sigma_stats_handler.js b/src/handlers/sigma/sigma_stats_handler.js new file mode 100644 index 0000000..a9c571c --- /dev/null +++ b/src/handlers/sigma/sigma_stats_handler.js @@ -0,0 +1,68 @@ +/** + * sigma_stats_handler.js + * + * Handles Sigma rule statistics requests from Slack commands + * Processes requests for database statistics + */ +const logger = require('../../utils/logger'); +const { handleError } = require('../../utils/error_handler'); +const { getSigmaStats } = require('../../services/sigma/sigma_stats_service'); +const { getStatsBlocks } = require('../../blocks/sigma_stats_block'); + +const { getFileName } = require('../../utils/file_utils'); +const FILE_NAME = getFileName(__filename); + +/** + * Handle the sigma-stats command for Sigma rules + * + * @param {Object} command - The Slack command object + * @param {Function} respond - Function to send response back to Slack + */ +const handleCommand = async (command, respond) => { + try { + logger.info(`${FILE_NAME}: Processing sigma-stats command`); + + await respond({ + text: 'Gathering Sigma rule statistics... This may take a moment.', + response_type: 'ephemeral' + }); + + // Get statistics from service + const statsResult = await getSigmaStats(); + + if (!statsResult.success) { + logger.error(`${FILE_NAME}: Failed to retrieve statistics: ${statsResult.message}`); + await respond({ + text: `Error retrieving statistics: ${statsResult.message}`, + response_type: 'ephemeral' + }); + return; + } + + // Generate blocks for displaying statistics + let blocks; + try { + blocks = getStatsBlocks(statsResult.stats); + } catch (blockError) { + await handleError(blockError, `${FILE_NAME}: Block generation`, respond, { + responseType: 'ephemeral', + customMessage: 'Error generating statistics view' + }); + return; + } + + // Return the response + await respond({ + blocks: blocks, + response_type: 'in_channel' + }); + } catch (error) { + await handleError(error, `${FILE_NAME}: Stats command handler`, respond, { + responseType: 'ephemeral' + }); + } +}; + +module.exports = { + handleCommand +}; \ No newline at end of file diff --git a/src/handlers/stats/stats_handler.js b/src/handlers/stats/stats_handler.js new file mode 100644 index 0000000..fb140cc --- /dev/null +++ b/src/handlers/stats/stats_handler.js @@ -0,0 +1,3 @@ +// +// stats_handler.js +// \ No newline at end of file diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..3e40bc4 --- /dev/null +++ b/src/index.js @@ -0,0 +1,63 @@ +/** + * index.js + * + * Entry point for the Fylgja application. + * This module initializes the Slack bot server using configuration from fylgja.yml. + * It handles startup errors and provides logging. + */ +const app = require('./app'); +const logger = require('./utils/logger'); +const { version } = require('../package.json'); +const { SERVER_CONFIG } = require('./config/appConfig'); + +// Start the app +const PORT = SERVER_CONFIG.port; +const ENV = process.env.NODE_ENV || 'development'; +const { getFileName } = require('./utils/file_utils'); +const FILE_NAME = getFileName(__filename); + +/** + * Gracefully handles server shutdown + * Logs shutdown information and exits the process + * + * @param {string} reason - The reason for the shutdown + */ +function handleShutdown(reason) { + logger.info(`${FILE_NAME}: Shutting down server (${reason})`); + process.exit(0); +} + +/** + * Immediately-invoked async function to start the server + * Allows for proper async/await error handling + */ +(async () => { + try { + logger.info(`${FILE_NAME}: Starting Fylgja Slack bot v${version} on port ${PORT} (${ENV} environment)`); + + // Set up signal handlers for graceful shutdown + process.on('SIGTERM', () => handleShutdown('SIGTERM')); + process.on('SIGINT', () => handleShutdown('SIGINT')); + + // Handle uncaught exceptions + process.on('uncaughtException', (error) => { + logger.error(`${FILE_NAME}: Uncaught exception: ${error.message}`); + logger.debug(`${FILE_NAME}: Error stack: ${error.stack}`); + process.exit(1); + }); + + // Handle unhandled promise rejections + process.on('unhandledRejection', (reason, promise) => { + logger.error(`${FILE_NAME}: Unhandled rejection at: ${promise}, reason: ${reason}`); + // Continue running despite unhandled rejection + }); + + await app.start(PORT); + logger.info(`${FILE_NAME}: ✅ Fylgja Slack bot is running on port ${PORT}`); + logger.info(`${FILE_NAME}: Server URL: http://localhost:${PORT}`); + } catch (error) { + logger.error(`${FILE_NAME}: 💥 Fatal error starting app: ${error.message}`); + logger.debug(`${FILE_NAME}: Error stack: ${error.stack}`); + process.exit(1); + } +})(); \ No newline at end of file diff --git a/src/services/elastic/cases.js b/src/services/elastic/cases.js new file mode 100644 index 0000000..e69de29 diff --git a/src/services/elastic/clients.js b/src/services/elastic/clients.js new file mode 100644 index 0000000..e69de29 diff --git a/src/services/elastic/elastic_api_service.js b/src/services/elastic/elastic_api_service.js new file mode 100644 index 0000000..939c859 --- /dev/null +++ b/src/services/elastic/elastic_api_service.js @@ -0,0 +1,154 @@ +/** + * elastic_api_service.js + * + * Service for interacting with Elasticsearch API endpoints + */ +const axios = require('axios'); +const logger = require('../../utils/logger'); +const { ELASTICSEARCH_CONFIG } = require('../../config/appConfig'); + +const FILE_NAME = 'elastic_api_service.js'; + +/** + * Get Elasticsearch configuration with credentials + * + * @returns {Object} Configuration object with URL and credentials + */ +const getElasticConfig = () => { + return { + url: ELASTICSEARCH_CONFIG.apiEndpoint.split('/api/')[0] || process.env.ELASTIC_URL, + username: ELASTICSEARCH_CONFIG.credentials.split(':')[0] || process.env.ELASTIC_USERNAME, + password: ELASTICSEARCH_CONFIG.credentials.split(':')[1] || process.env.ELASTIC_PASSWORD, + apiEndpoint: ELASTICSEARCH_CONFIG.apiEndpoint + }; +}; + +/** + * Send a rule to Elasticsearch SIEM + * + * @param {Object} rulePayload - The rule payload to send to Elasticsearch + * @returns {Promise} - Object containing success status and response/error information + */ +const sendRuleToSiem = async (rulePayload) => { + logger.info(`${FILE_NAME}: Sending rule to Elasticsearch SIEM`); + + try { + const elasticConfig = getElasticConfig(); + const apiUrl = elasticConfig.apiEndpoint; + + logger.debug(`${FILE_NAME}: Using Elasticsearch API URL: ${apiUrl}`); + + // Send the request to Elasticsearch + const response = await axios({ + method: 'post', + url: apiUrl, + headers: { + 'Content-Type': 'application/json', + 'kbn-xsrf': 'true' + }, + auth: { + username: elasticConfig.username, + password: elasticConfig.password + }, + data: rulePayload + }); + + // Process the response + if (response.status >= 200 && response.status < 300) { + logger.info(`${FILE_NAME}: Successfully sent rule to SIEM`); + return { + success: true, + status: response.status, + data: response.data + }; + } else { + logger.error(`${FILE_NAME}: Error sending rule to SIEM. Status: ${response.status}, Response: ${JSON.stringify(response.data)}`); + return { + success: false, + status: response.status, + message: `Failed to add rule to SIEM. Status: ${response.status}`, + data: response.data + }; + } + } catch (error) { + logger.error(`${FILE_NAME}: API error sending rule to SIEM: ${error.message}`); + logger.debug(`${FILE_NAME}: API error details: ${error.response ? JSON.stringify(error.response.data) : 'No response data'}`); + + const errorMessage = error.response && error.response.data && error.response.data.message + ? error.response.data.message + : error.message; + + return { + success: false, + message: errorMessage, + error: error + }; + } +}; + +/** + * Make a generic request to an Elasticsearch API endpoint + * + * @param {Object} options - Request options + * @param {string} options.method - HTTP method (get, post, put, delete) + * @param {string} options.endpoint - API endpoint (appended to base URL) + * @param {Object} options.data - Request payload + * @param {Object} options.params - URL parameters + * @param {Object} options.headers - Additional headers + * @returns {Promise} - Response object + */ +const makeElasticRequest = async (options) => { + try { + const elasticConfig = getElasticConfig(); + const baseUrl = elasticConfig.url; + + // Build the full URL - use provided endpoint or default API endpoint + const url = options.endpoint ? + `${baseUrl}${options.endpoint.startsWith('/') ? '' : '/'}${options.endpoint}` : + elasticConfig.apiEndpoint; + + logger.debug(`${FILE_NAME}: Making ${options.method} request to: ${url}`); + + // Set up default headers + const headers = { + 'Content-Type': 'application/json', + 'kbn-xsrf': 'true', + ...(options.headers || {}) + }; + + // Make the request + const response = await axios({ + method: options.method || 'get', + url: url, + headers: headers, + auth: { + username: elasticConfig.username, + password: elasticConfig.password + }, + data: options.data || null, + params: options.params || null + }); + + // Return a standardized response + return { + success: response.status >= 200 && response.status < 300, + status: response.status, + data: response.data + }; + } catch (error) { + logger.error(`${FILE_NAME}: Error in Elasticsearch API request: ${error.message}`); + + return { + success: false, + message: error.response?.data?.message || error.message, + status: error.response?.status, + error: error + }; + } +}; + +module.exports = { + sendRuleToSiem, + makeElasticRequest, + getElasticConfig +}; \ No newline at end of file diff --git a/src/services/elastic/rules.js b/src/services/elastic/rules.js new file mode 100644 index 0000000..e69de29 diff --git a/src/services/elastic/spaces.js b/src/services/elastic/spaces.js new file mode 100644 index 0000000..e69de29 diff --git a/src/services/sigma/sigma_backend_converter.js b/src/services/sigma/sigma_backend_converter.js new file mode 100644 index 0000000..b0330c4 --- /dev/null +++ b/src/services/sigma/sigma_backend_converter.js @@ -0,0 +1,153 @@ +/** + * sigma_backend_converter.js + * + * Service for converting Sigma rules to various backend SIEM formats + * Uses the sigma-cli tool for conversion operations + */ +const fs = require('fs'); +const path = require('path'); +const os = require('os'); +const { execSync } = require('child_process'); +const logger = require('../../utils/logger'); +const { SIGMA_CLI_PATH, SIGMA_CLI_CONFIG } = require('../../config/appConfig'); +const { convertSigmaRule } = require('./sigma_converter_service'); +const { getRuleYamlContent } = require('../../sigma_db/sigma_db_queries'); + +const { getFileName } = require('../../utils/file_utils'); +const FILE_NAME = getFileName(__filename); + +/** + * Convert a Sigma rule to a specific backend format using the sigma-cli + * + * @param {string} ruleId - The ID of the rule to convert + * @param {Object} config - Configuration for the conversion + * @param {string} config.backend - Target backend (default from YAML config) + * @param {string} config.target - Query target (default from YAML config) + * @param {string} config.format - Output format (default from YAML config) + * @returns {Promise} Conversion result with output or error + */ +async function convertRuleToBackend(ruleId, config = {}) { + try { + // Validate configuration and set defaults from YAML config + const backend = config.backend || SIGMA_CLI_CONFIG.backend; + const target = config.target || SIGMA_CLI_CONFIG.target; + const format = config.format || SIGMA_CLI_CONFIG.format; + + logger.info(`${FILE_NAME}: Converting rule ${ruleId} using backend: ${backend}, target: ${target}, format: ${format}`); + + // Verify sigma-cli path + if (!fs.existsSync(SIGMA_CLI_PATH)) { + logger.error(`${FILE_NAME}: Sigma CLI not found at path: ${SIGMA_CLI_PATH}`); + return { + success: false, + message: 'Sigma CLI tool not found' + }; + } + + // Get the rule YAML content + const yamlResult = await getRuleYamlContent(ruleId); + if (!yamlResult.success || !yamlResult.content) { + logger.warn(`${FILE_NAME}: Failed to retrieve YAML for rule ${ruleId}: ${yamlResult.message || 'No content'}`); + return { + success: false, + message: yamlResult.message || 'Failed to retrieve rule content' + }; + } + + // Save the YAML to a temporary file + const tempDir = os.tmpdir(); + const tempFilePath = path.join(tempDir, `sigma_rule_${ruleId}_${Date.now()}.yml`); + + logger.debug(`${FILE_NAME}: Writing rule YAML to temp file: ${tempFilePath}`); + + try { + fs.writeFileSync(tempFilePath, yamlResult.content); + } catch (fileError) { + logger.error(`${FILE_NAME}: Error writing temporary file: ${fileError.message}`); + return { + success: false, + message: `Error preparing rule for conversion: ${fileError.message}` + }; + } + + // Build the sigma-cli command + // Command syntax: sigma convert -t "$backend" -p "$target" -f "$format" + const command = `"${SIGMA_CLI_PATH}" convert -t ${backend} -p ${target} -f ${format} "${tempFilePath}"`; + + // Execute the command + logger.debug(`${FILE_NAME}: Executing sigma-cli command: ${command}`); + let result; + + try { + result = execSync(command, { encoding: 'utf8' }); + } catch (execError) { + logger.error(`${FILE_NAME}: Sigma-cli execution error: ${execError.message}`); + + // Clean up temporary file + try { + fs.unlinkSync(tempFilePath); + } catch (cleanupError) { + logger.warn(`${FILE_NAME}: Error removing temporary file: ${cleanupError.message}`); + } + + return { + success: false, + message: `Error during rule conversion: ${execError.message}` + }; + } + + // Clean up temporary file + try { + fs.unlinkSync(tempFilePath); + } catch (cleanupError) { + logger.warn(`${FILE_NAME}: Error removing temporary file: ${cleanupError.message}`); + } + + // Get rule metadata for context + const ruleData = await convertSigmaRule(ruleId); + + if (!ruleData.success || !ruleData.rule) { + logger.warn(`${FILE_NAME}: Failed to get metadata for rule ${ruleId}`); + + // return the conversion output + return { + success: true, + output: result.trim(), + rule: { + id: ruleId, + title: 'Unknown Rule', + description: 'Rule metadata could not be retrieved' + }, + conversionDetails: { + backend, + target, + format + } + }; + } + + // Return the output with rule metadata + return { + success: true, + output: result.trim(), + rule: ruleData.rule, + conversionDetails: { + backend, + target, + format + } + }; + } catch (error) { + logger.error(`${FILE_NAME}: Error converting rule ${ruleId} to backend: ${error.message}`); + logger.debug(`${FILE_NAME}: Error stack: ${error.stack}`); + + return { + success: false, + message: `Error converting rule: ${error.message}` + }; + } +} + +module.exports = { + convertRuleToBackend +}; \ No newline at end of file diff --git a/src/services/sigma/sigma_converter_service.js b/src/services/sigma/sigma_converter_service.js new file mode 100644 index 0000000..5ae5679 --- /dev/null +++ b/src/services/sigma/sigma_converter_service.js @@ -0,0 +1,422 @@ +// +// sigma_converter_service.js +// converts Sigma rules to a structured object +// +const logger = require('../../utils/logger'); +const yaml = require('js-yaml'); +const { findRuleById } = require('../../sigma_db/sigma_db_queries'); + +const { getFileName } = require('../../utils/file_utils'); +const FILE_NAME = getFileName(__filename); + +/** + * Convert a Sigma rule to a structured object + * Can be called with either a rule object or a rule ID + * + * @param {Object|String} input - Either a raw rule object or a rule ID + * @param {Object} [config] - Optional configuration + * @returns {Promise} Converted rule or result object + */ +async function convertSigmaRule(input, config = null) { + // Check if we're dealing with a rule ID (string) + if (typeof input === 'string') { + try { + const ruleId = input; + logger.info(`Converting rule by ID: ${ruleId}`); + // Find the rule in database + const rawRule = await findRuleById(ruleId); + if (!rawRule) { + logger.warn(`Rule with ID ${ruleId} not found`); + return { + success: false, + message: `Rule with ID ${ruleId} not found` + }; + } + + // Debug: Log what we found + logger.debug(`Retrieved rule ${ruleId} from database: content ${rawRule.content ? 'present' : 'missing'}, parameters ${rawRule.parameters ? Object.keys(rawRule.parameters).length : 0}`); + + // Check if content is missing (flag set by findRuleById) + if (rawRule.content_missing || !rawRule.content) { + logger.warn(`Rule with ID ${ruleId} has missing content, attempting to build from parameters`); + + // Try to build from parameters + if (rawRule.parameters && Object.keys(rawRule.parameters).length > 0) { + const builtRule = buildRuleFromParameters(rawRule); + + if (builtRule) { + logger.info(`Successfully built rule ${ruleId} from parameters`); + return { + success: true, + rule: builtRule, + built_from_parameters: true + }; + } + } + + logger.warn(`Could not build rule ${ruleId} from parameters, returning placeholder`); + return { + success: true, + rule: { + id: ruleId, + title: 'Rule Found But Content Missing', + description: `The rule with ID ${ruleId} exists in the database, but its content field is empty. This may indicate a problem with the rule import process.`, + author: 'Unknown', + level: 'unknown', + status: 'unknown', + logsource: {}, + detection: { condition: 'Content missing' }, + falsepositives: ['N/A - Content missing'], + tags: ['error', 'missing-content'], + references: [], + file_path: rawRule.file_path || 'unknown' + } + }; + } + + // Process the raw rule + const processedRule = processRuleContent(rawRule); + if (!processedRule) { + return { + success: false, + message: `Failed to process rule with ID ${ruleId}` + }; + } + + logger.debug(`Processing rule content for ${rawRule.id}:`); + + return { + success: true, + rule: processedRule + }; + } catch (error) { + logger.error(`Error converting rule by ID: ${error.message}`); + return { + success: false, + message: `Error converting rule: ${error.message}` + }; + } + } else { + try { + if (!input) { + return { + success: false, + message: 'No rule data provided' + }; + } + + // Check for missing content + if (!input.content) { + logger.warn('Rule object has missing content, attempting to build from parameters'); + + // Try to build from parameters + if (input.parameters && Object.keys(input.parameters).length > 0) { + const builtRule = buildRuleFromParameters(input); + + if (builtRule) { + logger.info(`Successfully built rule ${input.id} from parameters`); + return { + success: true, + rule: builtRule, + built_from_parameters: true + }; + } + } + + logger.warn(`Could not build rule from parameters, returning placeholder`); + return { + success: true, + rule: { + id: input.id || 'unknown', + title: 'Rule Found But Content Missing', + description: 'The rule exists in the database, but its content field is empty. This may indicate a problem with the rule import process.', + author: 'Unknown', + level: 'unknown', + status: 'unknown', + logsource: {}, + detection: { condition: 'Content missing' }, + falsepositives: ['N/A - Content missing'], + tags: ['error', 'missing-content'], + references: [], + file_path: input.file_path || 'unknown' + } + }; + } + + const processedRule = processRuleContent(input); + if (!processedRule) { + return { + success: false, + message: 'Failed to process rule object' + }; + } + + return { + success: true, + rule: processedRule + }; + } catch (error) { + logger.error(`Error processing rule object: ${error.message}`); + return { + success: false, + message: `Error processing rule: ${error.message}` + }; + } + } +} + +/** + * Process rule content into a structured object + * @param {Object} rawRule - The raw rule object + * @returns {Object|null} Processed rule object + */ +function processRuleContent(rawRule) { + if (!rawRule) { + logger.warn('Cannot convert rule: rule object is null'); + return null; + } + + if (!rawRule.content) { + logger.warn('Cannot convert rule: missing content in rule data'); + + // Check if we have parameters and try to build from them + if (rawRule.parameters && Object.keys(rawRule.parameters).length > 0) { + logger.info(`Attempting to build rule ${rawRule.id} from parameters`); + return buildRuleFromParameters(rawRule); + } + + return { + id: rawRule.id || 'unknown', + title: 'Error: Missing Rule Content', + description: 'The rule content could not be found in the database. This may indicate a problem with the rule import process or a corruption in the database.', + level: 'unknown', + file_path: rawRule.file_path || 'unknown', + falsepositives: ['N/A - Content missing'], + tags: ['error', 'missing-content'], + references: [], + detection: { condition: 'Content missing' } + }; + } + + try { + // Parse the YAML content + let parsedRule; + try { + // Log the content for debugging + logger.debug(`Parsing YAML content for rule ${rawRule.id}, content length: ${rawRule.content.length}`); + + // Try different YAML parsing approaches + try { + parsedRule = yaml.load(rawRule.content); + } catch (yamlError) { + logger.warn(`Standard YAML parsing failed for ${rawRule.id}: ${yamlError.message}`); + + // Try with more tolerant parsing + try { + // Try multi-document loading + const docs = []; + yaml.loadAll(rawRule.content, (doc) => { + if (doc) docs.push(doc); + }); + + if (docs.length > 0) { + parsedRule = docs[0]; // Take the first document + logger.debug(`Multi-document YAML parsing succeeded for ${rawRule.id}, found ${docs.length} documents`); + } else { + throw new Error('No documents found in multi-document parse'); + } + } catch (multiError) { + logger.warn(`Multi-document YAML parsing failed for ${rawRule.id}: ${multiError.message}`); + + // Last resort: manual extraction of key fields + parsedRule = extractFieldsManually(rawRule.content, rawRule.id); + } + } + + if (!parsedRule) { + logger.warn(`Rule parsing resulted in null object for ID: ${rawRule.id}`); + parsedRule = {}; + } + } catch (yamlError) { + logger.error(`YAML parsing error: ${yamlError.message}`); + logger.debug(`Problematic content (first 200 chars): ${rawRule.content.substring(0, 200)}`); + parsedRule = {}; + } + + // Create a new object combining database fields and parsed content + const convertedRule = { + id: rawRule.id || parsedRule.id || 'unknown', + title: parsedRule.title || 'Untitled Rule', + description: parsedRule.description || 'No description provided', + author: parsedRule.author || 'Unknown', + level: parsedRule.level || 'unknown', + status: parsedRule.status || 'unknown', + logsource: parsedRule.logsource || {}, + detection: parsedRule.detection || {}, + falsepositives: parsedRule.falsepositives || [], + tags: parsedRule.tags || [], + references: parsedRule.references || [], + file_path: rawRule.file_path || 'unknown' + }; + + logger.info(`Successfully converted rule ${convertedRule.id}`); + return convertedRule; + } catch (error) { + logger.error(`Error parsing rule: ${error.message}`); + return { + id: rawRule.id || 'unknown', + title: 'Error: Could not parse rule', + description: `Error parsing rule: ${error.message}`, + level: 'unknown', + file_path: rawRule.file_path || 'unknown', + falsepositives: [], + tags: ['error', 'parse-error'], + references: [], + detection: { condition: 'Parse error' } + }; + } +} + +/** + * Manual extraction of key fields from YAML content when parsing fails + * @param {string} content - The raw YAML content + * @param {string} ruleId - The rule ID + * @returns {Object} Extracted fields + */ +function extractFieldsManually(content, ruleId) { + logger.debug(`Attempting manual field extraction for rule ${ruleId}`); + + const result = { + id: ruleId + }; + + // Simple regex patterns to extract common fields + const patterns = { + title: /title:\s*(.+)$/m, + description: /description:\s*(.+)$/m, + author: /author:\s*(.+)$/m, + level: /level:\s*(.+)$/m, + status: /status:\s*(.+)$/m + }; + + // Extract fields using regex + Object.entries(patterns).forEach(([field, pattern]) => { + const match = content.match(pattern); + if (match && match[1]) { + result[field] = match[1].trim(); + } + }); + + logger.debug(`Manual extraction found ${Object.keys(result).length - 1} fields for rule ${ruleId}`); + + return result; +} + +/** + * Build a rule object from parameters when content is missing + * @param {Object} rawRule - The raw rule object with parameters + * @returns {Object} Reconstructed rule object + */ +function buildRuleFromParameters(rawRule) { + logger.info(`Building rule ${rawRule.id} from parameters`); + + if (!rawRule || !rawRule.parameters) { + logger.warn(`Cannot build rule: missing parameters for rule ${rawRule ? rawRule.id : 'unknown'}`); + return null; + } + + logger.debug(`Found ${Object.keys(rawRule.parameters).length} parameters for rule ${rawRule.id}`); + + // Initialize a new rule object with essential properties + const reconstructedRule = { + id: rawRule.id, + title: rawRule.parameters.title || 'Unknown Title', + description: rawRule.parameters.description || 'No description available', + author: rawRule.parameters.author || 'Unknown', + file_path: rawRule.file_path || 'unknown', + level: rawRule.parameters.level || 'unknown', + status: rawRule.parameters.status || 'unknown', + logsource: {}, + detection: { condition: rawRule.parameters['detection.condition'] || 'unknown' }, + falsepositives: [], + tags: [], + references: [] + }; + + // Process parameters to rebuild nested objects + Object.entries(rawRule.parameters).forEach(([key, value]) => { + // Handle array parameters + if (key === 'falsepositives' || key === 'tags' || key === 'references') { + if (Array.isArray(value)) { + reconstructedRule[key] = value; + } else if (typeof value === 'string') { + // Try to parse JSON string arrays + try { + const parsed = JSON.parse(value); + if (Array.isArray(parsed)) { + reconstructedRule[key] = parsed; + } else { + reconstructedRule[key] = [value]; + } + } catch (e) { + reconstructedRule[key] = [value]; + } + } + } + // Handle logsource properties + else if (key.startsWith('logsource.')) { + const prop = key.substring('logsource.'.length); + reconstructedRule.logsource[prop] = value; + } + // Handle detection properties + else if (key.startsWith('detection.') && key !== 'detection.condition') { + const prop = key.substring('detection.'.length); + const parts = prop.split('.'); + + let current = reconstructedRule.detection; + for (let i = 0; i < parts.length - 1; i++) { + if (!current[parts[i]]) { + current[parts[i]] = {}; + } + current = current[parts[i]]; + } + + current[parts[parts.length - 1]] = value; + } + }); + + logger.debug(`Reconstructed rule structure for ${rawRule.id}: ${JSON.stringify({ + id: reconstructedRule.id, + title: reconstructedRule.title, + fields: Object.keys(reconstructedRule) + })}`); + + return reconstructedRule; +} + +/** + * Extract a readable condition string from a rule + * @param {Object} rule - The converted rule object + * @returns {String} Human-readable condition + */ +function extractDetectionCondition(rule) { + if (!rule) { + return 'No rule data available'; + } + + if (!rule.detection) { + return 'No detection information available'; + } + + if (!rule.detection.condition) { + return 'No condition specified'; + } + + return rule.detection.condition; +} + +module.exports = { + convertSigmaRule, + extractDetectionCondition, + buildRuleFromParameters +}; \ No newline at end of file diff --git a/src/services/sigma/sigma_details_service.js b/src/services/sigma/sigma_details_service.js new file mode 100644 index 0000000..dad7db1 --- /dev/null +++ b/src/services/sigma/sigma_details_service.js @@ -0,0 +1,150 @@ +/** + * sigma_details_service.js + * + * This service provides functionality for retrieving and explaining Sigma rules. + */ +const logger = require('../../utils/logger'); +const { convertSigmaRule, extractDetectionCondition } = require('./sigma_converter_service'); +const { debugRuleContent, getRuleYamlContent } = require('../../sigma_db/sigma_db_queries'); + +const { getFileName } = require('../../utils/file_utils'); +const FILE_NAME = getFileName(__filename); + +/** + * Explains a Sigma rule by providing a simplified, human-readable format + * Performs diagnostics before explanation and handles error cases + * + * @param {string} ruleId - The ID of the rule to explain + * @returns {Promise} Result object with success flag and explanation or error message + */ +async function explainSigmaRule(ruleId) { + if (!ruleId) { + logger.warn(`${FILE_NAME}: Cannot explain rule: Missing rule ID`); + return { + success: false, + message: 'Missing rule ID' + }; + } + + logger.info(`${FILE_NAME}: Running diagnostics for rule: ${ruleId}`); + logger.info(`${FILE_NAME}: Explaining rule ${ruleId}`); + + try { + // Run diagnostics on the rule content first + const diagnosticResult = await debugRuleContent(ruleId); + logger.debug(`${FILE_NAME}: Diagnostic result: ${JSON.stringify(diagnosticResult || {})}`); + + // Convert the rule ID to a structured object + const conversionResult = await convertSigmaRule(ruleId); + if (!conversionResult.success) { + logger.warn(`${FILE_NAME}: Failed to convert rule ${ruleId}: ${conversionResult.message}`); + return { + success: false, + message: conversionResult.message || `Failed to parse rule with ID ${ruleId}` + }; + } + + const rule = conversionResult.rule; + + // Extra safety check + if (!rule) { + logger.error(`${FILE_NAME}: Converted rule is null for ID ${ruleId}`); + return { + success: false, + message: `Failed to process rule with ID ${ruleId}` + }; + } + + // Create a simplified explanation with safe access to properties + const explanation = { + id: rule.id || ruleId, + title: rule.title || 'Untitled Rule', + description: rule.description || 'No description provided', + author: rule.author || 'Unknown author', + severity: rule.level || 'Unknown', + detectionExplanation: extractDetectionCondition(rule), + falsePositives: Array.isArray(rule.falsepositives) ? rule.falsepositives : + typeof rule.falsepositives === 'string' ? [rule.falsepositives] : + ['None specified'], + tags: Array.isArray(rule.tags) ? rule.tags : [], + references: Array.isArray(rule.references) ? rule.references : [] + }; + + logger.info(`${FILE_NAME}: Successfully explained rule ${ruleId}`); + logger.debug(`${FILE_NAME}: Explanation properties: ${Object.keys(explanation).join(', ')}`); + + return { + success: true, + explanation + }; + } catch (error) { + logger.error(`${FILE_NAME}: Error explaining rule: ${error.message}`); + logger.debug(`${FILE_NAME}: Error stack: ${error.stack}`); + return { + success: false, + message: `Error explaining rule: ${error.message}` + }; + } +} + +/** + * Gets the raw YAML content of a Sigma rule + * Retrieves the content from the database + * + * @param {string} ruleId - The ID of the rule to get YAML for + * @returns {Promise} Result object with success flag and YAML content or error message + */ +async function getSigmaRuleYaml(ruleId) { + if (!ruleId) { + logger.warn(`${FILE_NAME}: Cannot get YAML: Missing rule ID`); + return { + success: false, + message: 'Missing rule ID' + }; + } + + logger.info(`${FILE_NAME}: Getting YAML content for rule: ${ruleId}`); + + try { + // Get YAML content from database + const yamlResult = await getRuleYamlContent(ruleId); + + if (!yamlResult.success) { + logger.warn(`${FILE_NAME}: Failed to retrieve YAML for rule ${ruleId}: ${yamlResult.message}`); + return { + success: false, + message: yamlResult.message || `Failed to retrieve YAML for rule with ID ${ruleId}` + }; + } + + // Add extra safety check for content + if (!yamlResult.content) { + logger.warn(`${FILE_NAME}: YAML content is empty for rule ${ruleId}`); + return { + success: true, + yaml: '', + warning: 'YAML content is empty for this rule' + }; + } + + logger.debug(`${FILE_NAME}: Successfully retrieved YAML content with length: ${yamlResult.content.length}`); + + // Return the YAML content + return { + success: true, + yaml: yamlResult.content + }; + } catch (error) { + logger.error(`${FILE_NAME}: Error retrieving YAML: ${error.message}`); + logger.debug(`${FILE_NAME}: Error stack: ${error.stack}`); + return { + success: false, + message: `Error retrieving YAML: ${error.message}` + }; + } +} + +module.exports = { + explainSigmaRule, + getSigmaRuleYaml +}; \ No newline at end of file diff --git a/src/services/sigma/sigma_repository_service.js b/src/services/sigma/sigma_repository_service.js new file mode 100644 index 0000000..a3874e6 --- /dev/null +++ b/src/services/sigma/sigma_repository_service.js @@ -0,0 +1,188 @@ +/** + * sigma_repository_service.js + * + * This service manages the Sigma rule repository and database updates. + * It provides functions to clone/update the repository and run the database + * initialization script. + */ +const { spawn } = require('child_process'); +const util = require('util'); +const { exec } = require('child_process'); +const fs = require('fs'); +const path = require('path'); +const { SIGMA_REPO_DIR } = require('../../config/appConfig'); +const appConfig = require('../../config/appConfig'); +const logger = require('../../utils/logger'); + +const { getFileName } = require('../../utils/file_utils'); +const FILE_NAME = getFileName(__filename); + +// Promisify exec for async/await usage +const execPromise = util.promisify(exec); + +/** + * Clones or updates the Sigma repository + * Creates the repository directory if it doesn't exist + * + * @returns {Promise} Success status of the operation + */ +async function updateSigmaRepo() { + logger.debug(`${FILE_NAME}: Starting Sigma repository update process`); + + try { + // Ensure the parent directory exists + const parentDir = path.dirname(SIGMA_REPO_DIR); + if (!fs.existsSync(parentDir)) { + logger.debug(`${FILE_NAME}: Creating parent directory: ${parentDir}`); + fs.mkdirSync(parentDir, { recursive: true }); + } + + if (!fs.existsSync(SIGMA_REPO_DIR)) { + logger.info(`${FILE_NAME}: Cloning Sigma repository...`); + + // Read config to get repo URL + const repoUrl = appConfig.SIGMA_REPO_CONFIG.url; + if (!repoUrl) { + throw new Error('Repository URL not found in configuration'); + } + + logger.debug(`${FILE_NAME}: Using repository URL: ${repoUrl}`); + const cloneResult = await execPromise(`git clone ${repoUrl} ${SIGMA_REPO_DIR}`); + + logger.debug(`${FILE_NAME}: Clone output: ${cloneResult.stdout}`); + } else { + logger.info(`${FILE_NAME}: Updating existing Sigma repository...`); + + // Check if it's actually a git repository + if (!fs.existsSync(path.join(SIGMA_REPO_DIR, '.git'))) { + logger.warn(`${FILE_NAME}: Directory exists but is not a git repository: ${SIGMA_REPO_DIR}`); + throw new Error('Directory exists but is not a git repository'); + } + + const pullResult = await execPromise(`cd ${SIGMA_REPO_DIR} && git pull`); + logger.debug(`${FILE_NAME}: Pull output: ${pullResult.stdout}`); + } + + logger.info(`${FILE_NAME}: Sigma repository is up-to-date`); + return true; + } catch (error) { + logger.error(`${FILE_NAME}: Error updating Sigma repository: ${error.message}`); + logger.debug(`${FILE_NAME}: Error stack: ${error.stack}`); + return false; + } +} + +/** + * Updates the Sigma database by running the initialization script + * Spawns a child process to run the database initialization + * + * @returns {Promise} Success status of the operation + */ +async function updateSigmaDatabase() { + logger.info(`${FILE_NAME}: Starting database update process`); + + return new Promise((resolve, reject) => { + const scriptPath = path.join(__dirname, '..', '..', 'db', 'init-sigma-db.js'); + + // Verify the script exists before trying to run it + if (!fs.existsSync(scriptPath)) { + logger.error(`${FILE_NAME}: Database initialization script not found at: ${scriptPath}`); + reject(new Error(`Database initialization script not found at: ${scriptPath}`)); + return; + } + + logger.info(`${FILE_NAME}: Running database update script: ${scriptPath}`); + + const updateProcess = spawn('node', [scriptPath], { + stdio: 'pipe' // Capture output instead of inheriting + }); + + // Capture and log stdout + updateProcess.stdout.on('data', (data) => { + logger.debug(`${FILE_NAME}: DB Update stdout: ${data.toString().trim()}`); + }); + + // Capture and log stderr + updateProcess.stderr.on('data', (data) => { + logger.warn(`${FILE_NAME}: DB Update stderr: ${data.toString().trim()}`); + }); + + updateProcess.on('close', (code) => { + if (code === 0) { + logger.info(`${FILE_NAME}: Database update completed successfully`); + resolve(true); + } else { + logger.error(`${FILE_NAME}: Database update failed with exit code ${code}`); + reject(new Error(`Update failed with exit code ${code}`)); + } + }); + + updateProcess.on('error', (err) => { + logger.error(`${FILE_NAME}: Failed to start database update process: ${err.message}`); + reject(err); + }); + }); +} + +/** + * Checks the status of the Sigma repository + * Returns information about the repository including last commit + * + * @returns {Promise} Repository status information + */ +async function getSigmaRepoStatus() { + logger.debug(`${FILE_NAME}: Checking Sigma repository status`); + + try { + if (!fs.existsSync(SIGMA_REPO_DIR)) { + logger.warn(`${FILE_NAME}: Sigma repository directory does not exist: ${SIGMA_REPO_DIR}`); + return { + exists: false, + message: 'Repository has not been cloned yet' + }; + } + + // Check if it's a git repository + if (!fs.existsSync(path.join(SIGMA_REPO_DIR, '.git'))) { + logger.warn(`${FILE_NAME}: Directory exists but is not a git repository: ${SIGMA_REPO_DIR}`); + return { + exists: true, + isRepo: false, + message: 'Directory exists but is not a git repository' + }; + } + + // Get last commit info + const lastCommitInfo = await execPromise(`cd ${SIGMA_REPO_DIR} && git log -1 --format="%h|%an|%ad|%s"`); + const [hash, author, date, subject] = lastCommitInfo.stdout.trim().split('|'); + + // Get branch info + const branchInfo = await execPromise(`cd ${SIGMA_REPO_DIR} && git branch --show-current`); + const currentBranch = branchInfo.stdout.trim(); + + return { + exists: true, + isRepo: true, + lastCommit: { + hash, + author, + date, + subject + }, + branch: currentBranch, + path: SIGMA_REPO_DIR + }; + } catch (error) { + logger.error(`${FILE_NAME}: Error getting repository status: ${error.message}`); + return { + exists: true, + error: error.message + }; + } +} + +module.exports = { + updateSigmaRepo, + updateSigmaDatabase, + getSigmaRepoStatus +}; \ No newline at end of file diff --git a/src/services/sigma/sigma_search_service.js b/src/services/sigma/sigma_search_service.js new file mode 100644 index 0000000..81b53dc --- /dev/null +++ b/src/services/sigma/sigma_search_service.js @@ -0,0 +1,214 @@ +/** + * sigma_search_service.js + * + * This service provides functionality for searching Sigma rules by keywords. + * It processes search results and returns them in a structured format. + * Supports pagination for large result sets. + */ +const { searchRules } = require('../../sigma_db/sigma_db_queries'); +const logger = require('../../utils/logger'); +const { convertSigmaRule } = require('./sigma_converter_service'); + +const { getFileName } = require('../../utils/file_utils'); +const FILE_NAME = getFileName(__filename); + +/** + * Searches for Sigma rules by keyword and processes the results + * Returns a structured result object with success status and paginated results + * + * @param {string} keyword - The keyword to search for + * @param {number} page - Page number (1-based index, default: 1) + * @param {number} pageSize - Number of results per page (default: 10) + * @returns {Promise} Result object with success flag and processed results with pagination info + */ +async function searchSigmaRules(keyword, page = 1, pageSize = 10) { + if (!keyword || typeof keyword !== 'string') { + logger.warn(`${FILE_NAME}: Cannot search rules: Missing or invalid keyword`); + return { + success: false, + message: 'Missing or invalid search keyword' + }; + } + + // Validate pagination parameters + if (typeof page !== 'number' || page < 1) { + logger.warn(`${FILE_NAME}: Invalid page number: ${page}, defaulting to 1`); + page = 1; + } + + if (typeof pageSize !== 'number' || pageSize < 1 || pageSize > 100) { + logger.warn(`${FILE_NAME}: Invalid page size: ${pageSize}, defaulting to 10`); + pageSize = 10; + } + + // Trim the keyword to prevent accidental whitespace issues + const trimmedKeyword = keyword.trim(); + if (trimmedKeyword.length === 0) { + logger.warn(`${FILE_NAME}: Cannot search rules: Empty keyword after trimming`); + return { + success: false, + message: 'Search keyword cannot be empty' + }; + } + + // Calculate the offset based on page number + const offset = (page - 1) * pageSize; + + logger.info(`${FILE_NAME}: Searching for Sigma rules with keyword: "${trimmedKeyword}" (page ${page}, size ${pageSize}, offset ${offset})`); + + try { + // Pass pageSize and offset to the database query + const searchResult = await searchRules(trimmedKeyword, pageSize, offset); + + // Defensive handling of possible return formats + let allResults = []; + let totalCount = 0; + + // Log what we actually received for debugging + logger.debug(`${FILE_NAME}: Search result type: ${typeof searchResult}, isArray: ${Array.isArray(searchResult)}`); + + // Handle different possible return formats + if (searchResult) { + if (Array.isArray(searchResult)) { + // Direct array of results + allResults = searchResult; + logger.debug(`${FILE_NAME}: Received array of ${allResults.length} results`); + } else if (typeof searchResult === 'object') { + // Object with results property + if (Array.isArray(searchResult.results)) { + allResults = searchResult.results; + totalCount = searchResult.totalCount || 0; + logger.debug(`${FILE_NAME}: Received object with ${allResults.length} results of ${totalCount} total matches`); + } else if (searchResult.totalCount !== undefined) { + // Object might have a different structure + totalCount = searchResult.totalCount; + logger.debug(`${FILE_NAME}: Received object with totalCount ${totalCount}`); + } + } + } + + // Log what we extracted + logger.debug(`${FILE_NAME}: Extracted ${allResults.length} results for page ${page} of total ${totalCount}`); + + if (allResults.length === 0 && totalCount === 0) { + logger.info(`${FILE_NAME}: No rules found matching "${trimmedKeyword}"`); + return { + success: true, + results: [], + message: `No rules found matching "${trimmedKeyword}"`, + pagination: { + currentPage: 1, + pageSize: pageSize, + totalPages: 0, + totalResults: 0, + hasMore: false + } + }; + } + + // Calculate total pages and pagination info based on total count from database + const totalPages = Math.ceil(totalCount / pageSize); + const hasMore = (offset + pageSize) < totalCount; + + // Check if the requested page is valid + if (offset >= totalCount && totalCount > 0) { + // Return empty results but with pagination info + logger.warn(`${FILE_NAME}: Page ${page} exceeds available results (total: ${totalCount})`); + return { + success: true, + results: [], + message: `No results on page ${page}. Try a previous page.`, + pagination: { + currentPage: page, + pageSize: pageSize, + totalPages: totalPages, + totalResults: totalCount, + hasMore: false + } + }; + } + + // If we have results, include them with pagination info + logger.debug(`${FILE_NAME}: Returning ${allResults.length} results with pagination info (page ${page}/${totalPages}, total: ${totalCount})`); + + return { + success: true, + results: allResults, + count: allResults.length, + pagination: { + currentPage: page, + pageSize: pageSize, + totalPages: totalPages, + totalResults: totalCount, + hasMore: hasMore + } + }; + } catch (error) { + logger.error(`${FILE_NAME}: Error searching for rules: ${error.message}`); + logger.debug(`${FILE_NAME}: Error stack: ${error.stack}`); + return { + success: false, + message: `Error searching for rules: ${error.message}` + }; + } +} + +/** + * Enhanced search that returns fully converted rule objects with pagination support + * This is a more expensive operation than basic search + * + * @param {string} keyword - The keyword to search for + * @param {number} page - Page number (1-based index, default: 1) + * @param {number} pageSize - Number of results per page (default: 10) + * @returns {Promise} Result object with success flag and fully converted rule objects with pagination info + */ +async function searchAndConvertRules(keyword, page = 1, pageSize = 10) { + try { + // First perform a basic search with pagination + const searchResult = await searchSigmaRules(keyword, page, pageSize); + + if (!searchResult.success || !searchResult.results || searchResult.results.length === 0) { + return searchResult; + } + + logger.debug(`${FILE_NAME}: Converting ${searchResult.results.length} search results to full rule objects`); + + // Convert each result to a full rule object + const convertedResults = []; + for (const result of searchResult.results) { + try { + const conversionResult = await convertSigmaRule(result.id); + if (conversionResult.success && conversionResult.rule) { + convertedResults.push(conversionResult.rule); + } else { + logger.warn(`${FILE_NAME}: Failed to convert rule ${result.id}: ${conversionResult.message || 'Unknown error'}`); + } + } catch (conversionError) { + logger.error(`${FILE_NAME}: Error converting rule ${result.id}: ${conversionError.message}`); + } + } + + logger.info(`${FILE_NAME}: Successfully converted ${convertedResults.length} of ${searchResult.results.length} search results`); + + // Include the pagination information from the search results + return { + success: true, + results: convertedResults, + count: convertedResults.length, + originalCount: searchResult.results.length, + pagination: searchResult.pagination + }; + } catch (error) { + logger.error(`${FILE_NAME}: Error in searchAndConvertRules: ${error.message}`); + logger.debug(`${FILE_NAME}: Error stack: ${error.stack}`); + return { + success: false, + message: `Error searching and converting rules: ${error.message}` + }; + } +} + +module.exports = { + searchSigmaRules, + searchAndConvertRules +}; \ No newline at end of file diff --git a/src/services/sigma/sigma_stats_service.js b/src/services/sigma/sigma_stats_service.js new file mode 100644 index 0000000..c6ac0d6 --- /dev/null +++ b/src/services/sigma/sigma_stats_service.js @@ -0,0 +1,53 @@ +/** + * sigma_stats_service.js + * + * Service for retrieving and processing Sigma rule database statistics + * Provides aggregated statistical information about the rule database + */ +const logger = require('../../utils/logger'); +const { getStatsFromDatabase } = require('../../sigma_db/sigma_db_queries'); + +const { getFileName } = require('../../utils/file_utils'); +const FILE_NAME = getFileName(__filename); + +/** + * Get database statistics + * Collects various statistics about the Sigma rule database + * + * @returns {Promise} Object with success flag and statistics or error message + */ +async function getSigmaStats() { + logger.info(`${FILE_NAME}: Getting Sigma rule database statistics`); + + try { + // Get statistics from database query function + const statsResult = await getStatsFromDatabase(); + + if (!statsResult.success) { + logger.error(`${FILE_NAME}: Failed to retrieve statistics: ${statsResult.message}`); + return { + success: false, + message: statsResult.message + }; + } + + logger.info(`${FILE_NAME}: Successfully collected database statistics`); + + return { + success: true, + stats: statsResult.stats + }; + } catch (error) { + logger.error(`${FILE_NAME}: Error processing statistics: ${error.message}`); + logger.debug(`${FILE_NAME}: Error stack: ${error.stack}`); + + return { + success: false, + message: `Error processing statistics: ${error.message}` + }; + } +} + +module.exports = { + getSigmaStats +}; \ No newline at end of file diff --git a/src/sigma_db/sigma_db_connection.js b/src/sigma_db/sigma_db_connection.js new file mode 100644 index 0000000..cbd2c1a --- /dev/null +++ b/src/sigma_db/sigma_db_connection.js @@ -0,0 +1,93 @@ +/** + * sigma_db_connection.js + * + * This module manages connections to the SQLite database for Sigma rules. + * It provides functions for creating and closing database connections with Promise support. + */ +const sqlite3 = require('sqlite3').verbose(); +const { DB_PATH } = require('../config/appConfig'); +const path = require('path'); +const logger = require('../utils/logger'); + +const { getFileName } = require('../utils/file_utils'); +const FILE_NAME = getFileName(__filename); + +/** + * Creates and returns a Promise-based connection to the SQLite database + * Adds promisified methods to the db object for easier async operations + * + * @returns {Promise} Promise resolving to the database connection object + */ +function getDbConnection() { + const absolutePath = path.resolve(DB_PATH); + logger.debug(`${FILE_NAME}: Attempting to connect to database at path: ${absolutePath}`); + + return new Promise((resolve, reject) => { + const db = new sqlite3.Database(DB_PATH, (err) => { + if (err) { + logger.error(`${FILE_NAME}: Database connection error: ${err.message}`); + reject(err); + } else { + logger.debug(`${FILE_NAME}: Successfully connected to database at path: ${DB_PATH}`); + + // Add promisified methods to db object + db.getAsync = function(sql, params) { + return new Promise((resolve, reject) => { + this.get(sql, params, function(err, row) { + if (err) { + logger.error(`${FILE_NAME}: Error in getAsync: ${err.message}`); + reject(err); + } + else resolve(row); + }); + }); + }; + + db.allAsync = function(sql, params) { + return new Promise((resolve, reject) => { + this.all(sql, params, function(err, rows) { + if (err) { + logger.error(`${FILE_NAME}: Error in allAsync: ${err.message}`); + reject(err); + } + else resolve(rows); + }); + }); + }; + + resolve(db); + } + }); + }); +} + +/** + * Safely closes a database connection + * + * @param {Object} db - The database connection to close + * @returns {Promise} Promise that resolves when the connection is closed + */ +function closeDbConnection(db) { + return new Promise((resolve, reject) => { + if (!db) { + logger.debug(`${FILE_NAME}: No database connection to close`); + resolve(); + return; + } + + db.close((err) => { + if (err) { + logger.error(`${FILE_NAME}: Error closing database: ${err.message}`); + reject(err); + } else { + logger.debug(`${FILE_NAME}: Database connection closed successfully`); + resolve(); + } + }); + }); +} + +module.exports = { + getDbConnection, + closeDbConnection +}; \ No newline at end of file diff --git a/src/sigma_db/sigma_db_initialize.js b/src/sigma_db/sigma_db_initialize.js new file mode 100644 index 0000000..934e7d5 --- /dev/null +++ b/src/sigma_db/sigma_db_initialize.js @@ -0,0 +1,560 @@ +// +// sigma_db_initialize.js +// This script initializes the Sigma database by importing rules from the Sigma repository. +// and creating the SQLite database. +// +const fs = require('fs'); +const util = require('util'); +const sqlite3 = require('sqlite3').verbose(); +const yaml = require('js-yaml'); +const glob = util.promisify(require('glob')); + +const logger = require('../utils/logger'); +const { SIGMA_REPO_DIR, DB_PATH } = require('../config/appConfig'); +const { updateSigmaRepo } = require('../services/sigma/sigma_repository_service'); + +const { getFileName } = require('../utils/file_utils'); +const FILE_NAME = getFileName(__filename); + + +// Create database connection +function createDbConnection() { + return new Promise((resolve, reject) => { + const db = new sqlite3.Database(DB_PATH, (err) => { + if (err) { + reject(err); + return; + } + + logger.info(`${FILE_NAME}: Connected to SQLite database at ${DB_PATH}`); + + // CRITICAL FIX: Enable foreign key constraints + db.run('PRAGMA foreign_keys = ON;', (pragmaErr) => { + if (pragmaErr) { + logger.error(`${FILE_NAME}: Failed to enable foreign key constraints: ${pragmaErr.message}`); + reject(pragmaErr); + } else { + logger.info(`${FILE_NAME}: Foreign key constraints enabled`); + resolve(db); + } + }); + }); + }); +} + +// Initialize database schema +async function initializeDatabase(db) { + return new Promise((resolve, reject) => { + // Drop existing tables if they exist + db.run('DROP TABLE IF EXISTS rule_parameters', (err) => { + if (err) { + reject(err); + return; + } + + db.run('DROP TABLE IF EXISTS sigma_rules', (err) => { + if (err) { + reject(err); + return; + } + + // Create rules table with basic information + const createRulesTableSql = ` + CREATE TABLE sigma_rules ( + id TEXT PRIMARY KEY, + file_path TEXT, + content TEXT, + date DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `; + + db.run(createRulesTableSql, (err) => { + if (err) { + reject(err); + return; + } + + // Create rule_parameters table for individual parameters + const createParamsTableSql = ` + CREATE TABLE rule_parameters ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + rule_id TEXT, + param_name TEXT, + param_value TEXT, + param_type TEXT, + FOREIGN KEY (rule_id) REFERENCES sigma_rules(id) ON DELETE CASCADE + ) + `; + + db.run(createParamsTableSql, (err) => { + if (err) { + reject(err); + } else { + logger.info(`${FILE_NAME}: Database schema initialized`); + resolve(); + } + }); + }); + }); + }); + }); +} + +// Determine if a YAML document is a Sigma rule +function isSigmaRule(doc) { + // Check for essential Sigma rule properties + return doc && doc.id && ( + doc.detection || + doc.logsource || + doc.title + ); +} + +// Parse a Sigma YAML file and extract relevant fields +function parseRuleFile(filePath) { + try { + // Read the file content + const content = fs.readFileSync(filePath, 'utf8'); + + // Try to load as a multi-document YAML + let documents = []; + try { + yaml.loadAll(content, (doc) => { + if (doc) documents.push(doc); + }); + } catch (e) { + // If multi-document parsing fails, try as a single document + logger.warn(`${FILE_NAME}: Multi-document parsing failed for ${filePath}: ${e.message}`); + try { + const doc = yaml.load(content); + if (doc) documents.push(doc); + } catch (singleError) { + logger.error(`Failed to parse ${filePath} as YAML: ${singleError.message}`); + } + } + + // Filter to only include documents that look like Sigma rules + const ruleDocuments = documents.filter(isSigmaRule); + + // Return array of rule objects + return ruleDocuments.map(rule => { + // Ensure rule.id exists - this is critical + if (!rule.id) { + logger.warn(`${FILE_NAME}: Rule in ${filePath} has no ID, skipping`); + return null; + } + + return { + id: rule.id, + file_path: filePath, + content: content, // Explicitly assign content + parameters: extractParameters(rule) + }; + }).filter(rule => rule !== null); // Remove null rules + } catch (error) { + logger.error(`${FILE_NAME}: Error parsing ${filePath}: ${error.message}`); + return []; + } +} + +// Extract all parameters from a rule object, including nested ones +function extractParameters(rule) { + const params = []; + + function processValue(name, value, parentKey = '') { + const fullKey = parentKey ? `${parentKey}.${name}` : name; + + if (value === null || value === undefined) { + params.push({ + param_name: fullKey, + param_value: '', + param_type: 'null' + }); + } else if (typeof value === 'object' && !Array.isArray(value)) { + // For objects, add a parameter for the object itself + params.push({ + param_name: fullKey, + param_value: JSON.stringify(value), + param_type: 'object' + }); + + // Then process all its properties + Object.entries(value).forEach(([k, v]) => { + processValue(k, v, fullKey); + }); + } else if (Array.isArray(value)) { + // For arrays, add a parameter for the array itself + params.push({ + param_name: fullKey, + param_value: JSON.stringify(value), + param_type: 'array' + }); + + // And also add individual array elements + value.forEach((item, index) => { + if (typeof item === 'object' && item !== null) { + processValue(index.toString(), item, fullKey); + } else { + params.push({ + param_name: `${fullKey}[${index}]`, + param_value: String(item), + param_type: typeof item + }); + } + }); + } else { + // Handle primitive types + params.push({ + param_name: fullKey, + param_value: String(value), + param_type: typeof value + }); + } + } + + // Process all properties in the rule + Object.entries(rule).forEach(([key, value]) => { + processValue(key, value); + }); + + return params; +} + +// Import rules into database +async function importRules(db) { + // Find all YAML files in the entire repository + logger.info('${FILE_NAME}: Looking for rule files...'); + + // Get all YAML files - include both .yml and .yaml extensions + const files = await glob(`${SIGMA_REPO_DIR}/**/*.{yml,yaml}`); + logger.info(`${FILE_NAME}: Found ${files.length} total YAML files`); + + // Prepare insert statements with explicit parameter naming + const insertRuleStmt = db.prepare(` + INSERT OR REPLACE INTO sigma_rules (id, file_path, content) + VALUES (?, ?, ?) + `); + + const insertParamStmt = db.prepare(` + INSERT INTO rule_parameters (rule_id, param_name, param_value, param_type) + VALUES (?, ?, ?, ?) + `); + + // Process each file + let importedRuleCount = 0; + let importedParamCount = 0; + let skippedFileCount = 0; + let errorCount = 0; + + // Keep track of content status + let rulesWithContent = 0; + let rulesWithoutContent = 0; + + for (const file of files) { + try { + const rules = parseRuleFile(file); + + if (rules.length === 0) { + skippedFileCount++; + continue; + } + + for (const rule of rules) { + // Begin transaction + await new Promise((resolve, reject) => { + db.run('BEGIN TRANSACTION', (err) => { + if (err) reject(err); + else resolve(); + }); + }); + + let transactionSuccessful = true; + + try { + // Debug: Check content before insertion + const hasContent = !!(rule.content && rule.content.length > 0); + + if (hasContent) { + rulesWithContent++; + } else { + rulesWithoutContent++; + logger.warn(`Rule ${rule.id} has no content!`); + // CRITICAL FIX: Ensure empty content is handled as empty string, not null + rule.content = ''; + } + + // Insert rule with explicit parameters + await new Promise((resolve, reject) => { + insertRuleStmt.run( + rule.id, // Parameter 1: id + rule.file_path, // Parameter 2: file_path + rule.content || '', // Parameter 3: content (ensure not null) + (err) => { + if (err) { + logger.error(`Error inserting rule ${rule.id}: ${err.message}`); + transactionSuccessful = false; + reject(err); + } else { + resolve(); + } + } + ); + }); + + // CRITICAL FIX: Only insert parameters if rule insertion was successful + if (transactionSuccessful) { + // Insert parameters + for (const param of rule.parameters) { + try { + await new Promise((resolve, reject) => { + insertParamStmt.run( + rule.id, // Parameter 1: rule_id + param.param_name, // Parameter 2: param_name + param.param_value, // Parameter 3: param_value + param.param_type, // Parameter 4: param_type + (err) => { + if (err) { + logger.error(`${FILE_NAME}: Error inserting parameter ${param.param_name} for rule ${rule.id}: ${err.message}`); + transactionSuccessful = false; + reject(err); + } else { + resolve(); + } + } + ); + }); + + // CRITICAL FIX: Stop processing parameters if any insertion fails + if (!transactionSuccessful) { + break; + } + + importedParamCount++; + } catch (paramError) { + logger.error(`${FILE_NAME}: Error processing parameter: ${paramError.message}`); + transactionSuccessful = false; + break; + } + } + } + + // CRITICAL FIX: Only commit if everything was successful + if (transactionSuccessful) { + // Commit transaction + await new Promise((resolve, reject) => { + db.run('COMMIT', (err) => { + if (err) { + transactionSuccessful = false; + reject(err); + } else { + resolve(); + } + }); + }); + + importedRuleCount++; + + // Log progress every 100 rules + if (importedRuleCount % 100 === 0) { + logger.info(`${FILE_NAME}: Imported ${importedRuleCount} rules with ${importedParamCount} parameters, ${rulesWithContent} have content, ${rulesWithoutContent} missing content`); + } + } else { + // Rollback transaction if not successful + await new Promise((resolve) => { + db.run('ROLLBACK', () => resolve()); + }); + + errorCount++; + } + } catch (error) { + // Rollback transaction on error + await new Promise((resolve) => { + db.run('ROLLBACK', () => resolve()); + }); + + logger.error(`${FILE_NAME}: Error importing rule ${rule.id} from ${file}: ${error.message}`); + errorCount++; + } + } + } catch (error) { + logger.error(`${FILE_NAME}: Error processing file ${file}: ${error.message}`); + errorCount++; + } + } + + insertRuleStmt.finalize(); + insertParamStmt.finalize(); + + logger.info(`${FILE_NAME}: Import summary: ${importedRuleCount} rules imported with ${importedParamCount} parameters, ${skippedFileCount} files skipped, ${errorCount} errors`); + logger.info(`${FILE_NAME}: Content status: ${rulesWithContent} rules with content, ${rulesWithoutContent} rules without content`); + + // Run diagnostics after import + await diagnoseContentImport(db); +} + +// Diagnose rule content import issues +async function diagnoseContentImport(db) { + logger.info(`${FILE_NAME}: Starting content import diagnosis...`); + + // Check total rule count + const ruleCount = await new Promise((resolve, reject) => { + db.get('SELECT COUNT(*) as count FROM sigma_rules', (err, row) => { + if (err) reject(err); + else resolve(row ? row.count : 0); + }); + }); + + logger.info(`${FILE_NAME}: Total rules in database: ${ruleCount}`); + + // Sample a few rules + const sampleRules = await new Promise((resolve, reject) => { + db.all('SELECT id, file_path, length(content) as content_length FROM sigma_rules LIMIT 5', (err, rows) => { + if (err) reject(err); + else resolve(rows || []); + }); + }); + + logger.info(`${FILE_NAME}: Sample rule content lengths: ${JSON.stringify(sampleRules.map(r => ({ id: r.id, content_length: r.content_length })))}`); + + // Check the content column type + const tableInfo = await new Promise((resolve, reject) => { + db.all('PRAGMA table_info(sigma_rules)', (err, rows) => { + if (err) reject(err); + else resolve(rows || []); + }); + }); + + const contentColumn = tableInfo.find(col => col.name === 'content'); + logger.info(`${FILE_NAME}: Content column info: ${JSON.stringify(contentColumn)}`); + + // Check a rule's content explicitly + if (sampleRules.length > 0) { + const firstRuleId = sampleRules[0].id; + const ruleContent = await new Promise((resolve, reject) => { + db.get('SELECT content FROM sigma_rules WHERE id = ?', [firstRuleId], (err, row) => { + if (err) reject(err); + else resolve(row ? (row.content || null) : null); + }); + }); + + logger.info(`${FILE_NAME}: First rule content check: id=${firstRuleId}, has_content=${ruleContent !== null}, type=${typeof ruleContent}, length=${ruleContent ? ruleContent.length : 0}`); + + if (ruleContent) { + logger.info(`Content sample: ${ruleContent.substring(0, 100)}...`); + } + } + + // Look for missing parameters + const paramCount = await new Promise((resolve, reject) => { + db.get('SELECT COUNT(*) as count FROM rule_parameters', (err, row) => { + if (err) reject(err); + else resolve(row ? row.count : 0); + }); + }); + + logger.info(`${FILE_NAME}: Total parameters: ${paramCount}`); + + // CRITICAL FIX: Check for orphaned parameters + const orphanedParamCount = await new Promise((resolve, reject) => { + db.get(` + SELECT COUNT(*) as count + FROM rule_parameters p + LEFT JOIN sigma_rules r ON p.rule_id = r.id + WHERE r.id IS NULL + `, (err, row) => { + if (err) reject(err); + else resolve(row ? row.count : 0); + }); + }); + + logger.info(`${FILE_NAME}: Orphaned parameters (should be 0): ${orphanedParamCount}`); + + if (orphanedParamCount > 0) { + logger.error(`Found ${orphanedParamCount} orphaned parameters! This indicates a foreign key constraint violation.`); + } + + return { + rule_count: ruleCount, + sample_rules: sampleRules, + content_column: contentColumn, + param_count: paramCount, + orphaned_param_count: orphanedParamCount + }; +} + +// Create indexes on the database for better performance +async function createIndexes(db) { + return new Promise((resolve, reject) => { + const indexes = [ + 'CREATE INDEX IF NOT EXISTS idx_param_rule_id ON rule_parameters(rule_id)', + 'CREATE INDEX IF NOT EXISTS idx_param_name ON rule_parameters(param_name)', + 'CREATE INDEX IF NOT EXISTS idx_param_value ON rule_parameters(param_value)', + 'CREATE INDEX IF NOT EXISTS idx_param_type ON rule_parameters(param_type)' + ]; + + let completed = 0; + + for (const indexSql of indexes) { + db.run(indexSql, (err) => { + if (err) { + reject(err); + } else { + completed++; + if (completed === indexes.length) { + logger.info('Database indexes created'); + resolve(); + } + } + }); + } + }); +} + +// Main function +async function main() { + try { + // Update Sigma repository + const repoUpdated = await updateSigmaRepo(); + if (!repoUpdated) { + logger.error(`${FILE_NAME}: Failed to update repository. Database may not be up-to-date.`); + } + + // Connect to database + const db = await createDbConnection(); + + // Initialize database schema + await initializeDatabase(db); + + // Import rules + await importRules(db); + + // Create indexes + await createIndexes(db); + + // Close database connection + db.close((err) => { + if (err) { + logger.error(`${FILE_NAME}: Error closing database: ${err.message}`); + } else { + logger.info(`${FILE_NAME}: Database connection closed`); + } + }); + + logger.info(`${FILE_NAME}: Database initialization completed successfully`); + process.exit(0); + } catch (error) { + logger.error(`${FILE_NAME}: Database initialization failed: ${error.message}`); + process.exit(1); + } +} + +// If this script is run directly (not imported) +if (require.main === module) { + main(); +} + +module.exports = { + initializeDatabase, + importRules, + createIndexes +}; \ No newline at end of file diff --git a/src/sigma_db/sigma_db_queries.js b/src/sigma_db/sigma_db_queries.js new file mode 100644 index 0000000..1867c79 --- /dev/null +++ b/src/sigma_db/sigma_db_queries.js @@ -0,0 +1,585 @@ +/** + * + * sigma_db_queries.js + * this script contains functions to interact with the Sigma database + * + * IMPORTANT: + * SQLite queries need explicit Promise handling when using db.all() + * + * We had an issue in that the Promise returned by db.all() wasn't being + * properly resolved in the async context. By wrapping the db.all() call in + * a new Promise and explicitly handling the callback, we ensure the query + * completes before continuing. This is important with SQLite where the + * connection state management can sometimes be tricky with async/await. + * + */ +const { getDbConnection } = require('./sigma_db_connection'); +const logger = require('../utils/logger'); +const { DB_PATH } = require('../config/appConfig'); +const path = require('path'); + +const { getFileName } = require('../utils/file_utils'); +const FILE_NAME = getFileName(__filename); + + +/** + * Get a list of all rule IDs in the database + * Useful for bulk operations and database integrity checks + * + * @returns {Promise} Array of rule IDs or empty array on error + */ +async function getAllRuleIds() { + let db; + try { + logger.info(`${FILE_NAME}: Retrieving all rule IDs from database`); + + db = await getDbConnection(); + logger.debug(`${FILE_NAME}: Connected to database for retrieving all rule IDs`); + + const result = await new Promise((resolve, reject) => { + db.all('SELECT id FROM sigma_rules ORDER BY id', [], (err, rows) => { + if (err) { + logger.error(`${FILE_NAME}: Error fetching all rule IDs: ${err.message}`); + reject(err); + } else { + resolve(rows || []); + } + }); + }); + + logger.debug(`${FILE_NAME}: Retrieved ${result.length} rule IDs from database`); + return result.map(row => row.id); + } catch (error) { + logger.error(`${FILE_NAME}: Error retrieving all rule IDs: ${error.message}`); + logger.debug(`${FILE_NAME}: Error stack: ${error.stack}`); + return []; + } finally { + if (db) { + try { + await db.close(); + logger.debug(`${FILE_NAME}: Database connection closed after retrieving all rule IDs`); + } catch (closeError) { + logger.warn(`${FILE_NAME}: Error closing database: ${closeError.message}`); + } + } + } +} + + +/** + * Find a Sigma rule by its ID + * Retrieves rule data and associated parameters from the database + * + * @param {string} ruleId - The ID of the rule to find + * @returns {Promise} The rule object or null if not found + */ +async function findRuleById(ruleId) { + if (!ruleId) { + logger.warn(`${FILE_NAME}: Cannot find rule: Missing rule ID`); + return null; + } + + let db; + try { + db = await getDbConnection(); + logger.debug(`${FILE_NAME}: Connected to database for rule lookup: ${ruleId}`); + + // Get the base rule using promisified method + const rule = await db.getAsync('SELECT * FROM sigma_rules WHERE id = ?', [ruleId]); + if (!rule) { + logger.warn(`${FILE_NAME}: Rule with ID ${ruleId} not found in database`); + return null; + } + + logger.debug(`${FILE_NAME}: Found base rule with ID ${ruleId}, content length: ${rule.content ? rule.content.length : 0}`); + + // Get parameters using promisified method + const paramsAsync = await db.allAsync('SELECT param_name, param_value, param_type FROM rule_parameters WHERE rule_id = ?', [ruleId]); + logger.debug(`${FILE_NAME}: Params query returned ${paramsAsync ? paramsAsync.length : 0} results via allAsync`); + + // Check if content is missing + if (!rule.content) { + logger.warn(`${FILE_NAME}: Rule with ID ${ruleId} exists but has no content`); + rule.content_missing = true; + } + + // Get all parameters for this rule with case-insensitive matching + try { + const params = await new Promise((resolve, reject) => { + db.all( + 'SELECT param_name, param_value, param_type FROM rule_parameters WHERE LOWER(rule_id) = LOWER(?)', + [ruleId], + (err, rows) => { + if (err) reject(err); + else resolve(rows); + } + ); + }); + + logger.debug(`${FILE_NAME}: Retrieved ${params ? params.length : 0} parameters for rule ${ruleId}`); + + // Validate params is an array + if (params && Array.isArray(params)) { + // Attach parameters to the rule object + rule.parameters = {}; + + for (const param of params) { + if (param && param.param_name) { + // Convert value based on type + let value = param.param_value; + + if (param.param_type === 'object' || param.param_type === 'array') { + try { + value = JSON.parse(param.param_value); + } catch (parseError) { + logger.warn(`${FILE_NAME}: Failed to parse JSON for parameter ${param.param_name}: ${parseError.message}`); + } + } else if (param.param_type === 'boolean') { + value = param.param_value === 'true'; + } else if (param.param_type === 'number') { + value = Number(param.param_value); + } + + rule.parameters[param.param_name] = value; + } + } + + logger.debug(`${FILE_NAME}: Successfully processed ${Object.keys(rule.parameters).length} parameters for rule ${ruleId}`); + } else { + logger.warn(`${FILE_NAME}: Parameters for rule ${ruleId} not available or not iterable`); + rule.parameters = {}; + } + } catch (paramError) { + logger.error(`${FILE_NAME}: Error fetching parameters for rule ${ruleId}: ${paramError.message}`); + logger.debug(`${FILE_NAME}: Parameter error stack: ${paramError.stack}`); + rule.parameters = {}; + } + + return rule; + } catch (error) { + logger.error(`${FILE_NAME}: Error finding rule ${ruleId}: ${error.message}`); + logger.debug(`${FILE_NAME}: Error stack: ${error.stack}`); + return null; + } finally { + // Close the database connection if it was opened + if (db && typeof db.close === 'function') { + try { + await db.close(); + logger.debug(`${FILE_NAME}: Database connection closed after rule lookup`); + } catch (closeError) { + logger.warn(`${FILE_NAME}: Error closing database connection: ${closeError.message}`); + } + } + } +} + +/** + * Search for Sigma rules by keyword in rule titles + * Performs a case-insensitive search and returns matching rules with pagination + * + * @param {string} keyword - The keyword to search for + * @param {number} limit - Maximum number of results to return (default: 10) + * @param {number} offset - Number of results to skip (for pagination, default: 0) + * @returns {Promise} Object with results array and total count + */ +async function searchRules(keyword, limit = 10, offset = 0) { + if (!keyword) { + logger.warn(`${FILE_NAME}: Empty search keyword provided`); + return { results: [], totalCount: 0 }; + } + + // Sanitize keyword to prevent SQL injection + const sanitizedKeyword = keyword.replace(/'/g, "''"); + logger.info(`${FILE_NAME}: Searching for rules with keyword in title: ${sanitizedKeyword} (limit: ${limit}, offset: ${offset})`); + + let db; + try { + // Make sure we properly await the DB connection + db = await getDbConnection(); + logger.debug(`${FILE_NAME}: Database connection established for search`); + + // First get the total count of matching rules (for pagination info) + const countQuery = ` + SELECT COUNT(*) as count + FROM rule_parameters + WHERE param_name = 'title' + AND INSTR(LOWER(param_value), LOWER(?)) > 0 + `; + + const countResult = await new Promise((resolve, reject) => { + db.get(countQuery, [sanitizedKeyword], (err, row) => { + if (err) { + logger.error(`${FILE_NAME}: Count query error: ${err.message}`); + reject(err); + } else { + resolve(row || { count: 0 }); + } + }); + }); + + const totalCount = countResult.count; + logger.debug(`${FILE_NAME}: Total matching rules for "${sanitizedKeyword}": ${totalCount}`); + + // Use parameterized query instead of string interpolation for better security + const instrQuery = ` + SELECT rule_id, param_value AS title + FROM rule_parameters + WHERE param_name = 'title' + AND INSTR(LOWER(param_value), LOWER(?)) > 0 + LIMIT ? OFFSET ? + `; + + const results = await new Promise((resolve, reject) => { + db.all(instrQuery, [sanitizedKeyword, limit, offset], (err, rows) => { + if (err) { + logger.error(`${FILE_NAME}: Search query error: ${err.message}`); + reject(err); + } else { + logger.debug(`${FILE_NAME}: Search query returned ${rows ? rows.length : 0} results`); + resolve(rows || []); + } + }); + }); + + logger.debug(`${FILE_NAME}: Search results page for keyword "${sanitizedKeyword}": ${results.length} matches (page ${Math.floor(offset / limit) + 1})`); + + return { + results: results.map(r => ({ id: r.rule_id, title: r.title })), + totalCount + }; + } catch (error) { + logger.error(`${FILE_NAME}: Error in search operation: ${error.message}`); + logger.debug(`${FILE_NAME}: Search error stack: ${error.stack}`); + return { results: [], totalCount: 0 }; + } finally { + // Make sure we properly close the connection + if (db) { + try { + await new Promise((resolve) => db.close(() => resolve())); + logger.debug(`${FILE_NAME}: Database connection closed after search operation`); + } catch (closeError) { + logger.error(`${FILE_NAME}: Error closing database connection after search: ${closeError.message}`); + } + } + } +} + +/** + * Debug function to retrieve detailed information about a rule's content + * Useful for diagnosing issues with rule retrieval and content parsing + * + * @param {string} ruleId - The ID of the rule to debug + * @returns {Promise} Object containing debug information or null on error + */ +async function debugRuleContent(ruleId) { + if (!ruleId) { + logger.warn(`${FILE_NAME}: Cannot debug rule: Missing rule ID`); + return null; + } + + let db; + try { + db = await getDbConnection(); + + const absolutePath = path.resolve(DB_PATH); + logger.debug(`${FILE_NAME}: Debug function connecting to DB at path: ${absolutePath}`); + + // Get raw rule record + const rule = await db.get('SELECT id, file_path, length(content) as content_length, typeof(content) as content_type FROM sigma_rules WHERE id = ?', [ruleId]); + + if (!rule) { + logger.warn(`${FILE_NAME}: Rule with ID ${ruleId} not found during debug operation`); + return { error: 'Rule not found', ruleId }; + } + + // Return just the rule information without the undefined variables + return { + rule, + ruleId + }; + } catch (error) { + logger.error(`${FILE_NAME}: Debug error for rule ${ruleId}: ${error.message}`); + logger.debug(`${FILE_NAME}: Debug error stack: ${error.stack}`); + return { + error: error.message, + stack: error.stack, + ruleId + }; + } finally { + if (db) { + try { + await db.close(); + logger.debug(`${FILE_NAME}: Database connection closed after debug operation`); + } catch (closeError) { + logger.warn(`${FILE_NAME}: Error closing database after debug: ${closeError.message}`); + } + } + } +} + +/** + * Get the raw YAML content of a Sigma rule + * Retrieves the content field from the database which should contain YAML + * + * @param {string} ruleId - The ID of the rule + * @returns {Promise} Object with success flag and content or error message + */ +async function getRuleYamlContent(ruleId) { + if (!ruleId) { + logger.warn(`${FILE_NAME}: Cannot get YAML content: Missing rule ID`); + return { success: false, message: 'Missing rule ID' }; + } + + let db; + try { + logger.info(`${FILE_NAME}: Fetching YAML content for rule: ${ruleId}`); + logger.debug(`${FILE_NAME}: Rule ID type: ${typeof ruleId}, length: ${ruleId.length}`); + + db = await getDbConnection(); + logger.debug(`${FILE_NAME}: Connected to database for YAML retrieval`); + + // Debug query before execution + const debugResult = await db.get('SELECT id, typeof(content) as content_type, length(content) as content_length FROM sigma_rules WHERE id = ?', [ruleId]); + logger.debug(`${FILE_NAME}: Debug query result: ${JSON.stringify(debugResult || 'not found')}`); + + if (!debugResult) { + logger.warn(`${FILE_NAME}: Rule with ID ${ruleId} not found in debug query`); + return { success: false, message: 'Rule not found' }; + } + + // Get actual content + const rule = await new Promise((resolve, reject) => { + db.get('SELECT content FROM sigma_rules WHERE id = ?', [ruleId], (err, row) => { + if (err) { + logger.error(`${FILE_NAME}: Content query error: ${err.message}`); + reject(err); + } else { + resolve(row || null); + } + }); + }); + logger.debug(`${FILE_NAME}: Content query result for ${ruleId}: ${rule ? 'Found' : 'Not found'}`); + + if (!rule) { + logger.warn(`${FILE_NAME}: Rule with ID ${ruleId} not found in content query`); + return { success: false, message: 'Rule not found' }; + } + + if (!rule.content) { + logger.warn(`${FILE_NAME}: Rule with ID ${ruleId} found but has no content`); + return { success: false, message: 'Rule found but content is empty' }; + } + + logger.debug(`${FILE_NAME}: Content retrieved successfully for ${ruleId}, type: ${typeof rule.content}, length: ${rule.content.length}`); + + return { success: true, content: rule.content }; + } catch (error) { + logger.error(`${FILE_NAME}: Error retrieving YAML content for ${ruleId}: ${error.message}`); + logger.debug(`${FILE_NAME}: YAML retrieval error stack: ${error.stack}`); + return { success: false, message: `Error retrieving YAML: ${error.message}` }; + } finally { + if (db) { + try { + await db.close(); + logger.debug(`${FILE_NAME}: Database connection closed after YAML retrieval`); + } catch (closeError) { + logger.warn(`${FILE_NAME}: Error closing database after YAML retrieval: ${closeError.message}`); + } + } + } +} + +/** + * Get statistics about Sigma rules in the database + * Collects counts, categories, and other aggregate information + * + * @returns {Promise} Object with various statistics about the rules + */ +async function getStatsFromDatabase() { + let db; + try { + db = await getDbConnection(); + logger.debug(`${FILE_NAME}: Connected to database for statistics`); + + // Get total rule count + const totalRulesResult = await new Promise((resolve, reject) => { + db.get('SELECT COUNT(*) as count FROM sigma_rules', (err, row) => { + if (err) reject(err); + else resolve(row); + }); + }); + const totalRules = totalRulesResult.count; + + // Get last update time + const lastUpdateResult = await new Promise((resolve, reject) => { + db.get('SELECT MAX(date) as last_update FROM sigma_rules', (err, row) => { + if (err) reject(err); + else resolve(row); + }); + }); + const lastUpdate = lastUpdateResult.last_update; + + // Get rules by log source count (Windows, Linux, macOS) + const windowsRulesResult = await new Promise((resolve, reject) => { + db.get(` + SELECT COUNT(DISTINCT rule_id) as count + FROM rule_parameters + WHERE param_name = 'logsource' AND + param_value LIKE '%"product":"windows"%'`, + (err, row) => { + if (err) reject(err); + else resolve(row); + }); + }); + const windowsRules = windowsRulesResult.count || 0; + + const linuxRulesResult = await new Promise((resolve, reject) => { + db.get(` + SELECT COUNT(DISTINCT rule_id) as count + FROM rule_parameters + WHERE param_name = 'logsource' AND + param_value LIKE '%"product":"linux"%'`, + (err, row) => { + if (err) reject(err); + else resolve(row); + }); + }); + const linuxRules = linuxRulesResult.count || 0; + + const macosRulesResult = await new Promise((resolve, reject) => { + db.get(` + SELECT COUNT(DISTINCT rule_id) as count + FROM rule_parameters + WHERE param_name = 'logsource' AND + param_value LIKE '%"product":"macos"%'`, + (err, row) => { + if (err) reject(err); + else resolve(row); + }); + }); + const macosRules = macosRulesResult.count || 0; + + // Get rules by severity level + const severityStats = await new Promise((resolve, reject) => { + db.all(` + SELECT param_value AS level, COUNT(DISTINCT rule_id) as count + FROM rule_parameters + WHERE param_name = 'level' + GROUP BY param_value + ORDER BY + CASE + WHEN param_value = 'critical' THEN 1 + WHEN param_value = 'high' THEN 2 + WHEN param_value = 'medium' THEN 3 + WHEN param_value = 'low' THEN 4 + WHEN param_value = 'informational' THEN 5 + ELSE 6 + END`, + (err, rows) => { + if (err) reject(err); + else resolve(rows); + }); + }); + + // Get top 5 rule authors + const topAuthors = await new Promise((resolve, reject) => { + db.all(` + SELECT param_value AS author, COUNT(*) as count + FROM rule_parameters + WHERE param_name = 'author' + GROUP BY param_value + ORDER BY count DESC + LIMIT 5`, + (err, rows) => { + if (err) reject(err); + else resolve(rows); + }); + }); + + // Get empty content count (rules with missing YAML) + const emptyContentResult = await new Promise((resolve, reject) => { + db.get(` + SELECT COUNT(*) as count + FROM sigma_rules + WHERE content IS NULL OR content = ''`, + (err, row) => { + if (err) reject(err); + else resolve(row); + }); + }); + const emptyContentCount = emptyContentResult.count; + + // Get MITRE ATT&CK tactics statistics + const mitreStats = await new Promise((resolve, reject) => { + db.all(` + SELECT param_value AS tag, COUNT(DISTINCT rule_id) as count + FROM rule_parameters + WHERE param_name = 'tags' AND param_value LIKE 'attack.%' + GROUP BY param_value + ORDER BY count DESC + LIMIT 10`, + (err, rows) => { + if (err) reject(err); + else resolve(rows); + }); + }); + + // Format MITRE tactics for display + const formattedMitreTactics = mitreStats.map(item => { + const tactic = item.tag.substring(7); // Remove 'attack.' prefix + return { + tactic: tactic, + count: item.count + }; + }); + + // Compile all statistics + const stats = { + totalRules, + lastUpdate, + operatingSystems: { + windows: windowsRules, + linux: linuxRules, + macos: macosRules, + other: totalRules - (windowsRules + linuxRules + macosRules) + }, + severityLevels: severityStats.map(s => ({ level: s.level, count: s.count })), + topAuthors: topAuthors.map(a => ({ name: a.author, count: a.count })), + databaseHealth: { + emptyContentCount, + contentPercentage: totalRules > 0 ? Math.round(((totalRules - emptyContentCount) / totalRules) * 100) : 0 + }, + mitreTactics: formattedMitreTactics + }; + + return { + success: true, + stats + }; + } catch (error) { + logger.error(`${FILE_NAME}: Error retrieving statistics: ${error.message}`); + logger.debug(`${FILE_NAME}: Error stack: ${error.stack}`); + return { + success: false, + message: `Error retrieving statistics: ${error.message}` + }; + } finally { + if (db) { + try { + await new Promise((resolve) => db.close(() => resolve())); + logger.debug(`${FILE_NAME}: Database connection closed after statistics retrieval`); + } catch (closeError) { + logger.warn(`${FILE_NAME}: Error closing database: ${closeError.message}`); + } + } + } +} + +module.exports = { + getAllRuleIds, + findRuleById, + searchRules, + debugRuleContent, + getRuleYamlContent, + getStatsFromDatabase +}; \ No newline at end of file diff --git a/src/utils/error_handler.js b/src/utils/error_handler.js new file mode 100644 index 0000000..df2d632 --- /dev/null +++ b/src/utils/error_handler.js @@ -0,0 +1,42 @@ +/** + * error_handler.js + * + * Provides standardized error handling for Slack responses + */ +const logger = require('./logger'); + +/** + * Handle errors consistently across handlers + * + * @param {Error} error - The error that occurred + * @param {string} source - Source file/function where error occurred + * @param {Function} respond - Slack respond function + * @param {Object} options - Additional options + * @param {boolean} options.replaceOriginal - Whether to replace original message + * @param {string} options.responseType - Response type (ephemeral/in_channel) + * @param {string} options.customMessage - Custom message to display instead of error + */ +const handleError = async (error, source, respond, options = {}) => { + const { + replaceOriginal = false, + responseType = 'ephemeral', + customMessage = null + } = options; + + // Log the error with consistent format + logger.error(`${source}: ${error.message}`); + logger.debug(`${source}: Error stack: ${error.stack}`); + + // Respond to user with appropriate message + const displayMessage = customMessage || `An unexpected error occurred: ${error.message}`; + + await respond({ + text: displayMessage, + replace_original: replaceOriginal, + response_type: responseType + }); +}; + +module.exports = { + handleError +}; \ No newline at end of file diff --git a/src/utils/file_utils.js b/src/utils/file_utils.js new file mode 100644 index 0000000..1c649bc --- /dev/null +++ b/src/utils/file_utils.js @@ -0,0 +1,10 @@ +// file_utils.js +const path = require('path'); + +function getFileName(filePath) { + return path.basename(filePath); +} + +module.exports = { + getFileName +}; \ No newline at end of file diff --git a/src/utils/formatters.js b/src/utils/formatters.js new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/logger.js b/src/utils/logger.js new file mode 100644 index 0000000..3225e2a --- /dev/null +++ b/src/utils/logger.js @@ -0,0 +1,103 @@ +/** + * logger.js + * Handles logging functionality + */ +const fs = require('fs'); +const path = require('path'); +const { LOGGING_CONFIG } = require('../config/appConfig'); + +// Define log levels and their priority (higher number = higher priority) +const LOG_LEVELS = { + DEBUG: 1, + INFO: 2, + WARN: 3, + ERROR: 4 +}; + +// Get configured log level from config, default to INFO if not specified +const configuredLevel = (LOGGING_CONFIG?.level || 'info').toUpperCase(); +const configuredLevelValue = LOG_LEVELS[configuredLevel] || LOG_LEVELS.INFO; + +// Ensure logs directory exists +const LOGS_DIR = path.join(__dirname, '..', '..', 'logs'); +if (!fs.existsSync(LOGS_DIR)) { + fs.mkdirSync(LOGS_DIR, { recursive: true }); +} + +// Use log file from config if available, otherwise use default +const LOG_FILE = LOGGING_CONFIG?.file + ? path.resolve(path.join(__dirname, '..', '..'), LOGGING_CONFIG.file) + : path.join(LOGS_DIR, 'fylgja.log'); + +// Create logger object +const logger = { + /** + * Internal method to write log entry to file and console if level meets threshold + * @param {string} level - Log level (DEBUG, INFO, WARN, ERROR) + * @param {string} message - Log message to write + */ + _writeToFile: (level, message) => { + // Check if this log level should be displayed based on configured level + const levelValue = LOG_LEVELS[level] || 0; + + if (levelValue >= configuredLevelValue) { + const timestamp = new Date().toISOString(); + const logEntry = `${timestamp} ${level}: ${message}\n`; + + // Append to log file + try { + fs.appendFileSync(LOG_FILE, logEntry); + } catch (err) { + console.error(`Failed to write to log file: ${err.message}`); + } + + // Also log to console with appropriate method + switch (level) { + case 'ERROR': + console.error(logEntry.trim()); + break; + case 'WARN': + console.warn(logEntry.trim()); + break; + case 'DEBUG': + console.debug(logEntry.trim()); + break; + case 'INFO': + default: + console.info(logEntry.trim()); + } + } + }, + + /** + * Log an info message + * @param {string} message - Message to log + */ + info: (message) => logger._writeToFile('INFO', message), + + /** + * Log an error message + * @param {string} message - Message to log + */ + error: (message) => logger._writeToFile('ERROR', message), + + /** + * Log a warning message + * @param {string} message - Message to log + */ + warn: (message) => logger._writeToFile('WARN', message), + + /** + * Log a debug message + * @param {string} message - Message to log + */ + debug: (message) => logger._writeToFile('DEBUG', message), + + /** + * Get the current log level + * @returns {string} Current log level + */ + getLogLevel: () => configuredLevel +}; + +module.exports = logger; \ No newline at end of file diff --git a/src/utils/validators.js b/src/utils/validators.js new file mode 100644 index 0000000..e69de29