From 3849f7dc1880ceecf32f662a91e16c8190e0ad47 Mon Sep 17 00:00:00 2001 From: PierreDemailly Date: Sun, 29 Oct 2023 19:35:31 +0100 Subject: [PATCH 1/6] feat(morphix): introduce new micro template package with function support --- .github/workflows/agent.yml | 2 +- .github/workflows/codeql.yml | 6 +- .github/workflows/config.yml | 2 +- .github/workflows/discord.yml | 2 +- .github/workflows/logql.yml | 2 +- .github/workflows/morphix.yml | 39 ++++++++ .github/workflows/scorecard.yml | 4 +- .github/workflows/slack.yml | 2 +- .github/workflows/teams.yml | 2 +- README.md | 1 + package-lock.json | 72 ++++++-------- package.json | 3 +- src/agent/package.json | 3 +- src/agent/src/notifiers/notifierQueue.ts | 9 +- src/discord/package.json | 3 +- src/discord/src/index.ts | 14 ++- src/morphix/README.md | 116 +++++++++++++++++++++++ src/morphix/package.json | 40 ++++++++ src/morphix/src/functions.ts | 17 ++++ src/morphix/src/index.ts | 77 +++++++++++++++ src/morphix/test/index.spec.ts | 84 ++++++++++++++++ src/morphix/tsconfig.json | 20 ++++ src/slack/package.json | 3 +- src/slack/src/index.ts | 12 +-- src/teams/package.json | 3 +- src/teams/src/index.ts | 10 +- tsconfig.json | 18 ++++ 27 files changed, 480 insertions(+), 86 deletions(-) create mode 100644 .github/workflows/morphix.yml create mode 100644 src/morphix/README.md create mode 100644 src/morphix/package.json create mode 100644 src/morphix/src/functions.ts create mode 100644 src/morphix/src/index.ts create mode 100644 src/morphix/test/index.spec.ts create mode 100644 src/morphix/tsconfig.json diff --git a/.github/workflows/agent.yml b/.github/workflows/agent.yml index 6a6e210..b73397f 100644 --- a/.github/workflows/agent.yml +++ b/.github/workflows/agent.yml @@ -28,7 +28,7 @@ jobs: egress-policy: audit - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 + uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 with: node-version: ${{ matrix.node-version }} - name: Install dependencies diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 52ef8a3..bac409c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -54,7 +54,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@49abf0ba24d0b7953cb586944e918a0b92074c80 # v2.22.4 + uses: github/codeql-action/init@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -68,7 +68,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@49abf0ba24d0b7953cb586944e918a0b92074c80 # v2.22.4 + uses: github/codeql-action/autobuild@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 # ℹī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -81,6 +81,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@49abf0ba24d0b7953cb586944e918a0b92074c80 # v2.22.4 + uses: github/codeql-action/analyze@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml index 7d04e1d..9319420 100644 --- a/.github/workflows/config.yml +++ b/.github/workflows/config.yml @@ -28,7 +28,7 @@ jobs: egress-policy: audit - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 + uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 with: node-version: ${{ matrix.node-version }} - name: Install dependencies diff --git a/.github/workflows/discord.yml b/.github/workflows/discord.yml index f30abd9..77581e9 100644 --- a/.github/workflows/discord.yml +++ b/.github/workflows/discord.yml @@ -28,7 +28,7 @@ jobs: egress-policy: audit - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 + uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 with: node-version: ${{ matrix.node-version }} - name: Install dependencies diff --git a/.github/workflows/logql.yml b/.github/workflows/logql.yml index df93a93..d10ab2e 100644 --- a/.github/workflows/logql.yml +++ b/.github/workflows/logql.yml @@ -28,7 +28,7 @@ jobs: egress-policy: audit - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 + uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 with: node-version: ${{ matrix.node-version }} - name: Install dependencies diff --git a/.github/workflows/morphix.yml b/.github/workflows/morphix.yml new file mode 100644 index 0000000..144f582 --- /dev/null +++ b/.github/workflows/morphix.yml @@ -0,0 +1,39 @@ +name: Morphix + +on: + push: + branches: [main] + paths: + - src/morphix/src/** + - src/morphix/test/** + pull_request: + paths: + - src/morphix/src/** + - src/morphix/test/** + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18.x, 20.x] + fail-fast: false + steps: + - name: Harden Runner + uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0 + with: + egress-policy: audit + allowed-endpoints: > + dns.google + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 + with: + node-version: ${{ matrix.node-version }} + - name: Install dependencies + run: npm ci + - name: Run tests + run: npm run test --workspace=src/morphix diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index c0d9e23..7221a12 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -43,7 +43,7 @@ jobs: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@483ef80eb98fb506c348f7d62e28055e49fe2398 # v2.3.0 + uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 with: results_file: results.sarif results_format: sarif @@ -73,6 +73,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@49abf0ba24d0b7953cb586944e918a0b92074c80 # v2.22.4 + uses: github/codeql-action/upload-sarif@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 with: sarif_file: results.sarif diff --git a/.github/workflows/slack.yml b/.github/workflows/slack.yml index ab8b446..31953bd 100644 --- a/.github/workflows/slack.yml +++ b/.github/workflows/slack.yml @@ -28,7 +28,7 @@ jobs: egress-policy: audit - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 + uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 with: node-version: ${{ matrix.node-version }} - name: Install dependencies diff --git a/.github/workflows/teams.yml b/.github/workflows/teams.yml index 2662098..177e8f7 100644 --- a/.github/workflows/teams.yml +++ b/.github/workflows/teams.yml @@ -28,7 +28,7 @@ jobs: egress-policy: audit - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 + uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 with: node-version: ${{ matrix.node-version }} - name: Install dependencies diff --git a/README.md b/README.md index b95fe72..786902d 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Click on one of the links to access the documentation of the package: | name | package and link | | --- | --- | | logql | [@sigyn/logql](./src/logql) | +| morphix | [@sigyn/morphix](./src/morphix) | ### Notifiers | name | package and link | diff --git a/package-lock.json b/package-lock.json index 23862a1..f53528d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,13 +12,14 @@ "src/config", "src/logql", "src/agent", + "src/morphix", "src/discord", "src/slack", "src/teams" ], "devDependencies": { "@nodesecure/eslint-config": "^1.8.0", - "@types/node": "^20.8.7", + "@types/node": "^20.8.9", "c8": "^8.0.1", "cross-env": "^7.0.3", "glob": "^10.3.10", @@ -1070,6 +1071,10 @@ "resolved": "src/logql", "link": true }, + "node_modules/@sigyn/morphix": { + "resolved": "src/morphix", + "link": true + }, "node_modules/@sigyn/slack": { "resolved": "src/slack", "link": true @@ -1100,12 +1105,12 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.8.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.7.tgz", - "integrity": "sha512-21TKHHh3eUHIi2MloeptJWALuCu5H7HQTdTrWIFReA8ad+aggoX+lRes3ex7/FtpC+sVUpFMQ+QTfYr74mruiQ==", + "version": "20.8.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.9.tgz", + "integrity": "sha512-UzykFsT3FhHb1h7yD4CA4YhBHq545JC0YnEz41xkipN88eKQtL6rSgocL5tbAP6Ola9Izm/Aw4Ora8He4x0BHg==", "dev": true, "dependencies": { - "undici-types": "~5.25.1" + "undici-types": "~5.26.4" } }, "node_modules/@types/semver": { @@ -2215,17 +2220,6 @@ "node": ">=6" } }, - "node_modules/escape-goat": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz", - "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -3825,9 +3819,9 @@ } }, "node_modules/pino": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-8.16.0.tgz", - "integrity": "sha512-UUmvQ/7KTZt/vHjhRrnyS7h+J7qPBQnpG80V56xmIC+o9IqYmQOw/UIny9S9zYDfRBR0ClouCr464EkBMIT7Fw==", + "version": "8.16.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.16.1.tgz", + "integrity": "sha512-3bKsVhBmgPjGV9pyn4fO/8RtoVDR8ssW1ev819FsRXlRNgW8gR/9Kx+gCK4UPWd4JjrRDLWpzd/pb1AyWm3MGA==", "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", @@ -3986,20 +3980,6 @@ "node": ">=6" } }, - "node_modules/pupa": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pupa/-/pupa-3.1.0.tgz", - "integrity": "sha512-FLpr4flz5xZTSJxSeaheeMKN/EDzMdK7b8PTOC6a5PYFKTucWbdqjgqaEyH0shFiSJrVB1+Qqi4Tk19ccU6Aug==", - "dependencies": { - "escape-goat": "^4.0.0" - }, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -4852,9 +4832,9 @@ } }, "node_modules/undici-types": { - "version": "5.25.3", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz", - "integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==", + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "dev": true }, "node_modules/update-browserslist-db": { @@ -5173,8 +5153,7 @@ "dayjs": "^1.11.10", "dotenv": "^16.3.1", "ms": "^2.1.3", - "pino": "^8.16.0", - "pupa": "^3.1.0", + "pino": "^8.16.1", "toad-scheduler": "^3.0.0" }, "devDependencies": { @@ -5264,8 +5243,7 @@ "version": "1.3.1", "license": "MIT", "dependencies": { - "@myunisoft/httpie": "^2.0.3", - "pupa": "^3.1.0" + "@myunisoft/httpie": "^2.0.3" }, "engines": { "node": ">=18" @@ -5279,13 +5257,20 @@ "node": ">=18" } }, + "src/morphix": { + "name": "@sigyn/morphix", + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "src/slack": { "name": "@sigyn/slack", "version": "1.6.1", "license": "MIT", "dependencies": { - "@myunisoft/httpie": "^2.0.3", - "pupa": "^3.1.0" + "@myunisoft/httpie": "^2.0.3" }, "engines": { "node": ">=18" @@ -5296,8 +5281,7 @@ "version": "1.3.0", "license": "MIT", "dependencies": { - "@myunisoft/httpie": "^2.0.3", - "pupa": "^3.1.0" + "@myunisoft/httpie": "^2.0.3" }, "engines": { "node": ">=18" diff --git a/package.json b/package.json index 1d6396e..e9bad6d 100644 --- a/package.json +++ b/package.json @@ -13,13 +13,14 @@ "src/config", "src/logql", "src/agent", + "src/morphix", "src/discord", "src/slack", "src/teams" ], "devDependencies": { "@nodesecure/eslint-config": "^1.8.0", - "@types/node": "^20.8.7", + "@types/node": "^20.8.9", "c8": "^8.0.1", "cross-env": "^7.0.3", "glob": "^10.3.10", diff --git a/src/agent/package.json b/src/agent/package.json index cd0af1d..d90d01b 100644 --- a/src/agent/package.json +++ b/src/agent/package.json @@ -47,8 +47,7 @@ "dayjs": "^1.11.10", "dotenv": "^16.3.1", "ms": "^2.1.3", - "pino": "^8.16.0", - "pupa": "^3.1.0", + "pino": "^8.16.1", "toad-scheduler": "^3.0.0" }, "devDependencies": { diff --git a/src/agent/src/notifiers/notifierQueue.ts b/src/agent/src/notifiers/notifierQueue.ts index aaf1fbb..e45112e 100644 --- a/src/agent/src/notifiers/notifierQueue.ts +++ b/src/agent/src/notifiers/notifierQueue.ts @@ -8,6 +8,9 @@ const kPrivateInstancier = Symbol("instancier"); type NotifierQueueAlert = T & { _id: symbol; _nonUniqueMatcher: (notification: T, newNotifications: T) => boolean; + notifierConfig: { + notifier: string; + } } export class NotifierQueue extends EventEmitter { @@ -38,7 +41,11 @@ export class NotifierQueue extends EventEmitter { for (const newNotification of notifications) { const { _id } = newNotification; const alreadyInQueue = this.#notificationAlerts - .find((notification) => notification._id === _id && notification._nonUniqueMatcher(newNotification, notification)); + .find( + (notification) => notification._id === _id && + notification._nonUniqueMatcher(newNotification, notification) && + notification.notifierConfig.notifier === newNotification.notifierConfig.notifier + ); if (alreadyInQueue === undefined) { this.#notificationAlerts.push(newNotification); diff --git a/src/discord/package.json b/src/discord/package.json index b209de0..c486067 100644 --- a/src/discord/package.json +++ b/src/discord/package.json @@ -38,7 +38,6 @@ "author": "GENTILHOMME Thomas ", "license": "MIT", "dependencies": { - "@myunisoft/httpie": "^2.0.3", - "pupa": "^3.1.0" + "@myunisoft/httpie": "^2.0.3" } } diff --git a/src/discord/src/index.ts b/src/discord/src/index.ts index d09a1a3..a49167c 100644 --- a/src/discord/src/index.ts +++ b/src/discord/src/index.ts @@ -1,6 +1,7 @@ // Import Third-party Dependencies import * as httpie from "@myunisoft/httpie"; import { NotifierFormattedSigynRule, SigynInitializedTemplate } from "@sigyn/config"; +import { morphix } from "@sigyn/morphix"; // CONSTANTS const kWebhookUsername = "Sigyn Agent"; @@ -40,8 +41,6 @@ interface ExecuteWebhookData { async function formatWebhook(options: ExecuteWebhookOptions) { const { agentFailure, counter, ruleConfig, label, severity, lokiUrl, rules } = options.data; - // pupa is ESM only, need a dynamic import for CommonJS. - const { default: pupa } = await import("pupa"); const { title: templateTitle = "", content: templateContent = [] } = options.template; if (templateTitle === "" && templateContent.length === 0) { @@ -75,15 +74,14 @@ async function formatWebhook(options: ExecuteWebhookOptions) { ignoreMissing: true }; - const content: string[] = templateContent.map((content) => pupa( - content, - templateData, - contentTemplateOptions - )); + const content: string[] = []; + for (const template of templateContent) { + content.push(await morphix(template, templateData, contentTemplateOptions)); + } return { embeds: [{ - title: pupa(`${kSeverityEmoji[severity]} ${templateTitle}`, templateData, titleTemplateOptions), + title: await morphix(`${kSeverityEmoji[severity]} ${templateTitle}`, templateData, titleTemplateOptions), description: content.join("\n"), color: kEmbedColor[severity] }], diff --git a/src/morphix/README.md b/src/morphix/README.md new file mode 100644 index 0000000..8243871 --- /dev/null +++ b/src/morphix/README.md @@ -0,0 +1,116 @@ +

+ Morphix +

+ +

+ Micro templating with function pipes support +

+ +

+ + npm version + + + size + + ossf scorecard + + + + + + license + +

+ +## 🚀 Getting Started + +This package is available in the Node Package Repository and can be easily installed with [npm](https://doc.npmjs.com/getting-started/what-is-npm) or [yarn](https://yarnpkg.com) + +```bash +$ npm i @sigyn/morphix +# or +$ yarn add @sigyn/morphix +``` + +## 📚 Usage + +```ts +import { morphix } from "@sigyn/morphix"; + +await morphix("Hello {name | capitalize}", { name: "john" }); +``` + +> [!NOTE] +> `morphix()` is async because it supports async **functions** + +## 🌐 API + +### `morphix` + +```ts +async function morphix( + template: string, + data: Record | unknown[], + options: MorphixOptions = {} +): Promise +``` + +**template** +Type: `string` + +Text with placeholders for data properties. + +**data** +Type: `object | unknown[]` + +Data to interpolate into template. + +The keys should be a valid JS identifier or number (a-z, A-Z, 0-9). + +**options** +Type: `object` + +**ignoreMissing** +Type: `boolean` +Default: `false` + +By default, Morphix throws a MissingValueError when a placeholder resolves to undefined. With this option set to true, it simply ignores it and leaves the placeholder as is. + +**transform** +Type: `(data: { value: unknown; key: string }) => unknown` (default: `({value}) => value)`) + +Performs arbitrary operation for each interpolation. If the returned value was undefined, it behaves differently depending on the ignoreMissing option. Otherwise, the returned value will be interpolated into a string and embedded into the template. + +**MissingValueError** +Exposed for instance checking. + +## đŸ“Ļ Functions + +**capitalize** + +Capitalize the first letter. + +**dnsresolve** + +Retrieve host of a given IP. It uses `dns.reverse`. + +If it fails to retrieve the host, it returns the ip instead. + +## 🖋ī¸ Interfaces + +```ts +interface MorphixOptions { + transform?: (data: { + value: unknown; + key: string; + }) => unknown; + ignoreMissing?: boolean; +} +``` + +## Credits +This package is heavily inspired by [pupa](https://github.com/sindresorhus/pupa). Morphix is a fork with function support and doesn't support HTML escape. + +## License +MIT diff --git a/src/morphix/package.json b/src/morphix/package.json new file mode 100644 index 0000000..3168e50 --- /dev/null +++ b/src/morphix/package.json @@ -0,0 +1,40 @@ +{ + "name": "@sigyn/morphix", + "version": "1.0.0", + "description": "Micro templating with function pipes support", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "require": "./dist/index.js", + "import": "./dist/index.mjs", + "types": "./dist/index.d.ts" + } + }, + "engines": { + "node": ">=18" + }, + "scripts": { + "build": "tsup src/index.ts --format cjs,esm --dts --clean", + "prepublishOnly": "npm run build", + "test": "glob -c \"tsx --test\" \"./test/**/*.spec.ts\"", + "coverage": "c8 -r html npm test", + "lint": "cross-env eslint src/**/*.ts" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/MyUnisoft/sigyn.git", + "directory": "src/morphix" + }, + "bugs": { + "url": "https://github.com/MyUnisoft/sigyn/issues" + }, + "homepage": "https://github.com/MyUnisoft/sigyn/blob/main/src/morphix/README.md", + "files": [ + "dist" + ], + "keywords": [], + "author": "GENTILHOMME Thomas ", + "license": "MIT" +} diff --git a/src/morphix/src/functions.ts b/src/morphix/src/functions.ts new file mode 100644 index 0000000..f84e0be --- /dev/null +++ b/src/morphix/src/functions.ts @@ -0,0 +1,17 @@ +// Import Node.js Dependencies +import dns from "node:dns/promises"; + +export function capitalize(value: string): string { + return `${value.charAt(0).toUpperCase()}${value.slice(1)}`; +} + +export async function dnsresolve(ip: string): Promise { + try { + const hostNames = await dns.reverse(ip); + + return hostNames.join(", "); + } + catch { + return ip; + } +} diff --git a/src/morphix/src/index.ts b/src/morphix/src/index.ts new file mode 100644 index 0000000..071106c --- /dev/null +++ b/src/morphix/src/index.ts @@ -0,0 +1,77 @@ +/* eslint-disable func-style */ + +// Import Internal Dependencies +import { capitalize, dnsresolve } from "./functions"; + +// CONSTANTS +const kFunctions = { + capitalize, + dnsresolve +}; + +export interface MorphixOptions { + transform?: (data: { value: unknown; key: string }) => unknown; + ignoreMissing?: boolean; +} + +export class MissingValueError extends Error { + key: string; + + constructor(key: string) { + super(`Missing a value for ${key ? `the placeholder: ${key}` : "a placeholder"}`); + this.name = "MissingValueError"; + this.key = key; + } +} + +export async function morphix( + template: string, + data: Record | unknown[], + options: MorphixOptions = {} +) { + const { + transform = ({ value }) => value, + ignoreMissing = false + } = options; + + if (typeof template !== "string") { + throw new TypeError(`Expected a \`string\` in the first argument, got \`${typeof template}\``); + } + + if (typeof data !== "object") { + throw new TypeError(`Expected an \`object\` or \`Array\` in the second argument, got \`${typeof data}\``); + } + + const replace = async(placeholder: string, key: string, func: string | undefined) => { + let value: string | undefined = undefined; + for (const property of key?.split(".")) { + // eslint-disable-next-line no-nested-ternary + value = value ? value[property] : data ? data[property] : undefined; + } + + const transformedValue = transform({ value, key }); + if (transformedValue === undefined) { + if (ignoreMissing) { + return placeholder; + } + + throw new MissingValueError(key); + } + + if (func === undefined) { + return String(transformedValue); + } + + return await kFunctions[func](String(transformedValue)); + }; + + const braceFnRegex = /{\s*([a-z0-9-.]*)\s*(?:\|\s*((?:(?!{)[a-z0-9-.]*)*?)\s*)?}/gi; + let formattedTemplate = template; + + for (const [match, placeholder, func] of template.matchAll(braceFnRegex)) { + const replaceText = await replace(match, placeholder, func); + formattedTemplate = formattedTemplate.replace(match, () => replaceText); + } + + return formattedTemplate; +} diff --git a/src/morphix/test/index.spec.ts b/src/morphix/test/index.spec.ts new file mode 100644 index 0000000..9800071 --- /dev/null +++ b/src/morphix/test/index.spec.ts @@ -0,0 +1,84 @@ +// Import Node.js Dependencies +import assert from "node:assert"; +import { describe, it } from "node:test"; + +// Import Internal Dependencies +import { morphix } from "../src/index"; + +describe("Morphix", () => { + it("main", async() => { + // Normal placeholder + assert.equal(await morphix("{foo}", { foo: "!" }), "!"); + assert.equal(await morphix("{foo}", { foo: 10 }), "10"); + assert.equal(await morphix("{foo}", { foo: 0 }), "0"); + assert.equal(await morphix("{fo-o}", { "fo-o": 0 }), "0"); + assert.equal(await morphix("{foo}{foo}", { foo: "!" }), "!!"); + assert.equal(await morphix("{foo}{bar}{foo}", { foo: "!", bar: "#" }), "!#!"); + assert.equal(await morphix("yo {foo} lol {bar} sup", { foo: "đŸĻ„", bar: "🌈" }), "yo đŸĻ„ lol 🌈 sup"); + + assert.equal(await morphix("{foo}{deeply.nested.valueFoo}", { + foo: "!", + deeply: { + nested: { + valueFoo: "#" + } + } + }), "!#"); + + assert.equal(await morphix("{0}{1}", ["!", "#"]), "!#"); + + assert.equal(await morphix("{0}{1}", ["!", "#"]), "!#"); + }); + + it("do not match non-identifiers", async() => { + const fixture = "\"*.{json,md,css,graphql,html}\""; + assert.equal(await morphix(fixture, []), fixture); + }); + + it("ignore missing", async() => { + const template = "foo{{bar}}{undefined}"; + const options = { ignoreMissing: true }; + assert.equal(await morphix(template, {}, options), template); + }); + + it("throw on undefined by default", async() => { + assert.rejects(async() => { + await morphix("{foo}", {}); + }, { + message: "Missing a value for the placeholder: foo" + }); + }); + + it("transform and ignore missing", async() => { + const options = { + ignoreMissing: true, + transform: ({ value }) => (Number.isNaN(Number.parseInt(value, 10)) ? undefined : value) + }; + assert.equal(await morphix("{0} {1} {2}", ["0", 42, 3.14], options), "0 42 3.14"); + assert.equal(await morphix("{0} {1} {2}", ["0", null, 3.14], options), "0 {1} 3.14"); + }); + + it("transform and throw on undefined", async() => { + const options = { + transform: ({ value }) => (Number.isNaN(Number.parseInt(value, 10)) ? undefined : value) + }; + + await assert.doesNotReject(async() => { + await morphix("{0} {1} {2}", ["0", 42, 3.14], options); + }); + + await assert.rejects(async() => { + await morphix("{0} {1} {2}", ["0", null, 3.14], options); + }, { + message: "Missing a value for the placeholder: 1" + }); + }); + + it("should capitalize value", async() => { + assert.equal(await morphix("{foo | capitalize}", { foo: "foo" }), "Foo"); + }); + + it("should find ip hostname", async() => { + assert.equal(await morphix("{foo | dnsresolve}", { foo: "8.8.8.8" }), "dns.google"); + }); +}); diff --git a/src/morphix/tsconfig.json b/src/morphix/tsconfig.json new file mode 100644 index 0000000..e9d6a60 --- /dev/null +++ b/src/morphix/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "declaration": true, + "strictNullChecks": true, + "target": "ES2022", + "outDir": "dist", + "module": "ES2022", + "moduleResolution": "node", + "esModuleInterop": true, + "resolveJsonModule": false, + "skipDefaultLibCheck": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "sourceMap": true, + "rootDir": "./src", + "types": ["node"] + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/src/slack/package.json b/src/slack/package.json index fb20f9b..0b6af06 100644 --- a/src/slack/package.json +++ b/src/slack/package.json @@ -38,7 +38,6 @@ "author": "GENTILHOMME Thomas ", "license": "MIT", "dependencies": { - "@myunisoft/httpie": "^2.0.3", - "pupa": "^3.1.0" + "@myunisoft/httpie": "^2.0.3" } } diff --git a/src/slack/src/index.ts b/src/slack/src/index.ts index 9df7d17..3b6fd69 100644 --- a/src/slack/src/index.ts +++ b/src/slack/src/index.ts @@ -1,6 +1,7 @@ // Import Third-party Dependencies import * as httpie from "@myunisoft/httpie"; import { NotifierFormattedSigynRule, SigynInitializedTemplate } from "@sigyn/config"; +import { morphix } from "@sigyn/morphix"; // CONSTANTS const kAttachmentColor = { @@ -38,9 +39,6 @@ interface ExecuteWebhookData { async function formatWebhook(options: ExecuteWebhookOptions) { const { agentFailure, counter, ruleConfig, label, severity, lokiUrl, rules } = options.data; - // pupa is ESM only, need a dynamic import for CommonJS. - const { default: pupa } = await import("pupa"); - const { title: templateTitle = "", content: templateContent = [] } = options.template; if (templateTitle === "" && templateContent.length === 0) { throw new Error("Invalid rule template: one of the title or content is required."); @@ -81,10 +79,10 @@ async function formatWebhook(options: ExecuteWebhookOptions) { { mrkdwn_in: ["text"], color: kAttachmentColor[severity], - title: pupa(formattedTitle, templateData, titleTemplateOptions), + title: morphix(formattedTitle, templateData, titleTemplateOptions), fields: [ { - value: templateContent.map((text) => { + value: (await Promise.all(templateContent.map(async(text) => { if (text === "") { return ""; } @@ -97,12 +95,12 @@ async function formatWebhook(options: ExecuteWebhookOptions) { formattedText = formattedText.replace(url, `<${link}|${label}>`); } - return pupa( + return await morphix( formattedText, templateData, templateOptions ); - }).join("\n").replaceAll(/>(?!\s|$)/g, "â€ē"), + }))).join("\n").replaceAll(/>(?!\s|$)/g, "â€ē"), short: false } ] diff --git a/src/teams/package.json b/src/teams/package.json index 2774a9b..510873b 100644 --- a/src/teams/package.json +++ b/src/teams/package.json @@ -38,7 +38,6 @@ "author": "GENTILHOMME Thomas ", "license": "MIT", "dependencies": { - "@myunisoft/httpie": "^2.0.3", - "pupa": "^3.1.0" + "@myunisoft/httpie": "^2.0.3" } } diff --git a/src/teams/src/index.ts b/src/teams/src/index.ts index a081848..f2142ce 100644 --- a/src/teams/src/index.ts +++ b/src/teams/src/index.ts @@ -1,6 +1,7 @@ // Import Third-party Dependencies import * as httpie from "@myunisoft/httpie"; import { NotifierFormattedSigynRule, SigynInitializedTemplate } from "@sigyn/config"; +import { morphix } from "@sigyn/morphix"; // CONSTANTS const kSeverityEmoji = { @@ -32,9 +33,6 @@ interface ExecuteWebhookData { async function formatWebhook(options: ExecuteWebhookOptions) { const { agentFailure, counter, ruleConfig, label, severity, lokiUrl, rules } = options.data; - // pupa is ESM only, need a dynamic import for CommonJS. - const { default: pupa } = await import("pupa"); - const { title: templateTitle = "", content: templateContent = [] } = options.template; if (templateTitle === "" && templateContent.length === 0) { throw new Error("Invalid rule template: one of the title or content is required."); @@ -61,14 +59,14 @@ async function formatWebhook(options: ExecuteWebhookOptions) { ignoreMissing: true }; - const content: string[] = templateContent.map((content) => pupa( + const content: string[] = await Promise.all(templateContent.map(async(content) => await morphix( content, templateData, textTemplateOptions - )); + ))); return { - title: pupa(`${kSeverityEmoji[severity]} ${templateTitle}`, templateData, titleTemplateOptions), + title: await morphix(`${kSeverityEmoji[severity]} ${templateTitle}`, templateData, titleTemplateOptions), text: content.join("\n") }; } diff --git a/tsconfig.json b/tsconfig.json index fc8da69..cc286e6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,26 @@ { "files": [], "references": [ + { + "path": "./src/config" + }, + { + "path": "./src/logql" + }, { "path": "./src/agent" + }, + { + "path": "./src/morphix" + }, + { + "path": "./src/discord" + }, + { + "path": "./src/slack" + }, + { + "path": "./src/teams" } ] } From ae3584d3c1891bc7eb9ce2906ac138f4a37aefd2 Mon Sep 17 00:00:00 2001 From: PierreDemailly Date: Sun, 29 Oct 2023 19:48:00 +0100 Subject: [PATCH 2/6] fix(ci): build notifiers --- .github/workflows/discord.yml | 2 ++ .github/workflows/slack.yml | 2 ++ .github/workflows/teams.yml | 2 ++ 3 files changed, 6 insertions(+) diff --git a/.github/workflows/discord.yml b/.github/workflows/discord.yml index 77581e9..e9bbcbc 100644 --- a/.github/workflows/discord.yml +++ b/.github/workflows/discord.yml @@ -33,5 +33,7 @@ jobs: node-version: ${{ matrix.node-version }} - name: Install dependencies run: npm ci + - name: Build packages + run: npm run build - name: Run tests run: npm run test --workspace=src/discord diff --git a/.github/workflows/slack.yml b/.github/workflows/slack.yml index 31953bd..174ac96 100644 --- a/.github/workflows/slack.yml +++ b/.github/workflows/slack.yml @@ -33,5 +33,7 @@ jobs: node-version: ${{ matrix.node-version }} - name: Install dependencies run: npm ci + - name: Build packages + run: npm run build - name: Run tests run: npm run test --workspace=src/slack diff --git a/.github/workflows/teams.yml b/.github/workflows/teams.yml index 177e8f7..cf85787 100644 --- a/.github/workflows/teams.yml +++ b/.github/workflows/teams.yml @@ -33,5 +33,7 @@ jobs: node-version: ${{ matrix.node-version }} - name: Install dependencies run: npm ci + - name: Build packages + run: npm run build - name: Run tests run: npm run test --workspace=src/teams From eb7f2b17323ed3dd5d802c360dc8fb6da3bf1b62 Mon Sep 17 00:00:00 2001 From: PierreDemailly Date: Sun, 29 Oct 2023 19:48:46 +0100 Subject: [PATCH 3/6] test without harden runner --- .github/workflows/morphix.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/morphix.yml b/.github/workflows/morphix.yml index 144f582..2bbcce5 100644 --- a/.github/workflows/morphix.yml +++ b/.github/workflows/morphix.yml @@ -22,12 +22,6 @@ jobs: node-version: [18.x, 20.x] fail-fast: false steps: - - name: Harden Runner - uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0 - with: - egress-policy: audit - allowed-endpoints: > - dns.google - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 From a1e6edccb8e8e9ec15489768f87ff4f3cf00fb89 Mon Sep 17 00:00:00 2001 From: PierreDemailly Date: Sun, 29 Oct 2023 20:02:22 +0100 Subject: [PATCH 4/6] chore(morphix): improve regexp --- src/morphix/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/morphix/src/index.ts b/src/morphix/src/index.ts index 071106c..f16a53d 100644 --- a/src/morphix/src/index.ts +++ b/src/morphix/src/index.ts @@ -65,7 +65,7 @@ export async function morphix( return await kFunctions[func](String(transformedValue)); }; - const braceFnRegex = /{\s*([a-z0-9-.]*)\s*(?:\|\s*((?:(?!{)[a-z0-9-.]*)*?)\s*)?}/gi; + const braceFnRegex = /{\s{0,1}([a-z0-9-.]*)\s{0,1}(?:\|\s{0,1}((?:(?!{)[a-z0-9-]*)*?)\s{0,1})?}/gi; let formattedTemplate = template; for (const [match, placeholder, func] of template.matchAll(braceFnRegex)) { From 6d23514629da2eae45f84692e80858a2ca83afd2 Mon Sep 17 00:00:00 2001 From: PierreDemailly Date: Sun, 29 Oct 2023 20:28:48 +0100 Subject: [PATCH 5/6] chore: mock dns --- .github/workflows/morphix.yml | 4 ++++ package-lock.json | 12 ++++++++++ src/morphix/package.json | 7 ++++-- .../test/{index.spec.ts => index.spec.mts} | 23 +++++++++++++++++-- 4 files changed, 42 insertions(+), 4 deletions(-) rename src/morphix/test/{index.spec.ts => index.spec.mts} (79%) diff --git a/.github/workflows/morphix.yml b/.github/workflows/morphix.yml index 2bbcce5..555b092 100644 --- a/.github/workflows/morphix.yml +++ b/.github/workflows/morphix.yml @@ -22,6 +22,10 @@ jobs: node-version: [18.x, 20.x] fail-fast: false steps: + - name: Harden Runner + uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0 + with: + egress-policy: audit - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 diff --git a/package-lock.json b/package-lock.json index f53528d..57309be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2439,6 +2439,15 @@ "node": ">=8" } }, + "node_modules/esmock": { + "version": "2.5.8", + "resolved": "https://registry.npmjs.org/esmock/-/esmock-2.5.8.tgz", + "integrity": "sha512-Lvn8OhAvdM+OBgIkljnG8WNPayDN4vMyoHaKGnXnM4J7T3bE6qztUWHd7cnbnyW0ueLQRT/3bS6SoHnBN1hEsg==", + "dev": true, + "engines": { + "node": ">=14.16.0" + } + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -5261,6 +5270,9 @@ "name": "@sigyn/morphix", "version": "1.0.0", "license": "MIT", + "devDependencies": { + "esmock": "^2.5.8" + }, "engines": { "node": ">=18" } diff --git a/src/morphix/package.json b/src/morphix/package.json index 3168e50..309bc4b 100644 --- a/src/morphix/package.json +++ b/src/morphix/package.json @@ -18,7 +18,7 @@ "scripts": { "build": "tsup src/index.ts --format cjs,esm --dts --clean", "prepublishOnly": "npm run build", - "test": "glob -c \"tsx --test\" \"./test/**/*.spec.ts\"", + "test": "npm run build && glob -c \"tsx --test --loader=esmock\" \"./test/**/*.spec.mts\"", "coverage": "c8 -r html npm test", "lint": "cross-env eslint src/**/*.ts" }, @@ -36,5 +36,8 @@ ], "keywords": [], "author": "GENTILHOMME Thomas ", - "license": "MIT" + "license": "MIT", + "devDependencies": { + "esmock": "^2.5.8" + } } diff --git a/src/morphix/test/index.spec.ts b/src/morphix/test/index.spec.mts similarity index 79% rename from src/morphix/test/index.spec.ts rename to src/morphix/test/index.spec.mts index 9800071..f2f3cb6 100644 --- a/src/morphix/test/index.spec.ts +++ b/src/morphix/test/index.spec.mts @@ -2,8 +2,11 @@ import assert from "node:assert"; import { describe, it } from "node:test"; +// Import Third-party Dependencies +import esmock from "esmock"; + // Import Internal Dependencies -import { morphix } from "../src/index"; +import { morphix } from "../dist/index.mjs"; describe("Morphix", () => { it("main", async() => { @@ -79,6 +82,22 @@ describe("Morphix", () => { }); it("should find ip hostname", async() => { - assert.equal(await morphix("{foo | dnsresolve}", { foo: "8.8.8.8" }), "dns.google"); + const { morphix } = await esmock("../dist/index.mjs", { + "node:dns/promises": { + reverse: async() => ["dns.google"] + } + }); + assert.equal(await morphix("host: {foo | dnsresolve}", { foo: "8.8.8.8" }), "host: dns.google"); + }); + + it("should not find ip hostname", async() => { + const { morphix } = await esmock("../dist/index.mjs", { + "node:dns/promises": { + reverse: async() => { + throw new Error("Not found"); + } + } + }); + assert.equal(await morphix("host: {foo | dnsresolve}", { foo: "8.8.8.8" }), "host: 8.8.8.8"); }); }); From 20f866954c0a31f1cb175086fc84f77a880765bd Mon Sep 17 00:00:00 2001 From: PierreDemailly Date: Mon, 30 Oct 2023 10:18:09 +0100 Subject: [PATCH 6/6] chore(morphix): split functions into multiple files --- src/morphix/src/functions/capitalize.ts | 3 +++ src/morphix/src/{functions.ts => functions/dnsresolve.ts} | 4 ---- src/morphix/src/functions/index.ts | 2 ++ 3 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 src/morphix/src/functions/capitalize.ts rename src/morphix/src/{functions.ts => functions/dnsresolve.ts} (68%) create mode 100644 src/morphix/src/functions/index.ts diff --git a/src/morphix/src/functions/capitalize.ts b/src/morphix/src/functions/capitalize.ts new file mode 100644 index 0000000..7efdbde --- /dev/null +++ b/src/morphix/src/functions/capitalize.ts @@ -0,0 +1,3 @@ +export function capitalize(value: string): string { + return `${value.charAt(0).toUpperCase()}${value.slice(1)}`; +} diff --git a/src/morphix/src/functions.ts b/src/morphix/src/functions/dnsresolve.ts similarity index 68% rename from src/morphix/src/functions.ts rename to src/morphix/src/functions/dnsresolve.ts index f84e0be..897bbbe 100644 --- a/src/morphix/src/functions.ts +++ b/src/morphix/src/functions/dnsresolve.ts @@ -1,10 +1,6 @@ // Import Node.js Dependencies import dns from "node:dns/promises"; -export function capitalize(value: string): string { - return `${value.charAt(0).toUpperCase()}${value.slice(1)}`; -} - export async function dnsresolve(ip: string): Promise { try { const hostNames = await dns.reverse(ip); diff --git a/src/morphix/src/functions/index.ts b/src/morphix/src/functions/index.ts new file mode 100644 index 0000000..d3f5ef2 --- /dev/null +++ b/src/morphix/src/functions/index.ts @@ -0,0 +1,2 @@ +export { capitalize } from "./capitalize"; +export { dnsresolve } from "./dnsresolve";