diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 0000000..96490fa --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,10 @@ +engines: + eslint: + enabled: true + channel: "eslint-8" + config: + config: ".eslintrc.yaml" + +ratings: + paths: + - "**.js" diff --git a/.eslintrc.yaml b/.eslintrc.yaml new file mode 100644 index 0000000..693b320 --- /dev/null +++ b/.eslintrc.yaml @@ -0,0 +1,7 @@ +env: + node: true + es6: true + mocha: true + es2022: true + +extends: ["@haraka"] diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..0dccc85 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,11 @@ +### system info + +Please report your OS, Node version, and Haraka version by running this shell script on your Haraka server and replacing this section with the output. + +echo "Haraka | $(haraka -v)"; echo " --- | :--- "; echo "Node | $(node -v)"; echo "OS | $(uname -a)"; echo "openssl | $(openssl version)" + +### Expected behavior + +### Observed behavior + +### Steps to reproduce diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..5ccd7ed --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,13 @@ +Changes proposed in this pull request: + +- +- + +Fixes # + +Checklist: + +- [ ] docs updated +- [ ] tests updated +- [ ] Changes.md updated +- [ ] package.json.version bumped diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..8c3ac4a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "monthly" + allow: + - dependency-type: production diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3d01042 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,22 @@ +name: CI + +on: [push, pull_request] + +env: + CI: true + +jobs: + lint: + uses: haraka/.github/.github/workflows/lint.yml@master + + # coverage: + # uses: haraka/.github/.github/workflows/coverage.yml@master + # secrets: inherit + + ubuntu: + needs: [lint] + uses: haraka/.github/.github/workflows/ubuntu.yml@master + + windows: + needs: [lint] + uses: haraka/.github/.github/workflows/windows.yml@master diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..2b614e3 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,13 @@ +name: "CodeQL" + +on: + push: + branches: [master] + pull_request: + branches: [master] + schedule: + - cron: "18 7 * * 4" + +jobs: + codeql: + uses: haraka/.github/.github/workflows/codeql.yml@master diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..e81c15f --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,16 @@ +name: publish + +on: + push: + branches: + - master + paths: + - package.json + +env: + CI: true + +jobs: + publish: + uses: haraka/.github/.github/workflows/publish.yml@master + secrets: inherit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..625981f --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules +jspm_packages + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +package-lock.json +bower_components +# Optional npm cache directory +.npmrc +.idea +.DS_Store +haraka-update.sh \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..a8e94cb --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule ".release"] + path = .release + url = git@github.com:msimerson/.release.git diff --git a/.release b/.release new file mode 160000 index 0000000..7cd5707 --- /dev/null +++ b/.release @@ -0,0 +1 @@ +Subproject commit 7cd5707f7d69f8d4dca1ec407ada911890e59d0a diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c745c17 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +The format is based on [Keep a Changelog](https://keepachangelog.com/). + +### Unreleased + +### [1.0.0] - 2024-05-08 + +- repackaged from haraka/Haraka + +[1.0.0]: https://github.com/haraka/haraka-plugin-template/releases/tag/v1.0.0 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..29f9810 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Haraka + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2cf9912 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +[![CI Test Status][ci-img]][ci-url] +[![Code Climate][clim-img]][clim-url] + +[![NPM][npm-img]][npm-url] + +# haraka-plugin-esets + +This plugin allows virus scanning with ESET Mail Security for Linux/BSD. + +Install the software as per the intructions from ESET and enable this plugin +and it will scan each message using the "esets_cli" command which defaults to +/opt/eset/esets/bin/esets_cli. + +### Configure + +``` +cp node_modules/haraka-plugin-esets/config/esets.ini config/esets.ini +$EDITOR config/esets.ini +``` + +## USAGE + + + +[ci-img]: https://github.com/haraka/haraka-plugin-esets/actions/workflows/ci.yml/badge.svg +[ci-url]: https://github.com/haraka/haraka-plugin-esets/actions/workflows/ci.yml +[clim-img]: https://codeclimate.com/github/haraka/haraka-plugin-esets/badges/gpa.svg +[clim-url]: https://codeclimate.com/github/haraka/haraka-plugin-esets +[npm-img]: https://nodei.co/npm/haraka-plugin-esets.png +[npm-url]: https://www.npmjs.com/package/haraka-plugin-esets diff --git a/config/esets.ini b/config/esets.ini new file mode 100644 index 0000000..2a92888 --- /dev/null +++ b/config/esets.ini @@ -0,0 +1,2 @@ + +[main] diff --git a/index.js b/index.js new file mode 100644 index 0000000..6fed6d7 --- /dev/null +++ b/index.js @@ -0,0 +1,73 @@ +// esets + +const fs = require('node:fs'); +const child_process = require('node:child_process'); + +const virus_re = new RegExp('virus="([^"]+)"'); + +exports.register = function () { + this.cfg = this.config.get('esets.ini'); +} + +exports.hook_data_post = function (next, connection) { + + // Write message to temporary file + const tmpdir = this.cfg.main.tmpdir || '/tmp'; + const tmpfile = `${tmpdir}/${connection?.transaction?.uuid}.esets`; + const ws = fs.createWriteStream(tmpfile); + + ws.once('error', err => { + connection.logerror(this, `Error writing temporary file: ${err.message}`); + next(); + }); + + let start_time; + + ws.once('close', () => { + start_time = Date.now(); + const execCmd = `LANG=C /opt/eset/esets/bin/esets_cli ${tmpfile}` + const execOpts = { encoding: 'utf8', timeout: 30 * 1000 } + child_process.exec(execCmd, execOpts, function (error, stdout, stderr) { + // Remove the temporary file + fs.unlink(tmpfile, () => {}); + + // Timing + const end_time = Date.now(); + const elapsed = end_time - start_time; + + // Debugging + for (const channel of [stdout, stderr]) { + if (channel) { + const lines = channel.split('\n'); + for (const line of lines) { + if (line) connection.logdebug(this, `recv: ${line}`); + } + } + } + + // Get virus name + let virus = virus_re.exec(stdout) + if (virus) virus = virus[1]; + + // Log a summary + const exit_code = parseInt((error) ? error.code : 0) + const rmsg = exit_code === 0 || (exit_code > 1 && exit_code < 4) + ? ` virus="${virus}"` + : ` error="${(stdout || stderr || 'UNKNOWN').replace('\n',' ').trim()}"` + + connection.loginfo(this, `elapsed=${elapsed}ms code=${exit_code}${rmsg}`); + + // esets_cli returns non-zero exit on virus/error + if (exit_code) { + if (exit_code > 1 && exit_code < 4) { + next(DENY, `Message is infected with ${virus || 'UNKNOWN'}`); + } + else { + next(DENYSOFT, 'Virus scanner error'); + } + } + }) + }) + + connection.transaction.message_stream.pipe(ws); +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..6d5da7b --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "haraka-plugin-esets", + "version": "1.0.0", + "description": "Haraka plugin that scans with ESETS", + "main": "index.js", + "files": [ + "CHANGELOG.md", + "config" + ], + "scripts": { + "format": "npm run prettier:fix && npm run lint:fix", + "lint": "npx eslint@^8 *.js test", + "lint:fix": "npx eslint@^8 *.js test --fix", + "prettier": "npx prettier . --check", + "prettier:fix": "npx prettier . --write --log-level=warn", + "test": "node --test", + "versions": "npx dependency-version-checker check", + "versions:fix": "npx dependency-version-checker update" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/haraka/haraka-plugin-esets.git" + }, + "keywords": [ + "haraka", + "plugin", + "esets" + ], + "author": "Welcome Member ", + "license": "MIT", + "bugs": { + "url": "https://github.com/haraka/haraka-plugin-esets/issues" + }, + "homepage": "https://github.com/haraka/haraka-plugin-esets#readme", + "devDependencies": { + "@haraka/eslint-config": "1.1.3", + "haraka-test-fixtures": "1.3.5" + } +} diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..72e20fd --- /dev/null +++ b/test/index.js @@ -0,0 +1,43 @@ +const assert = require("node:assert/strict"); +const { beforeEach, describe, it } = require("node:test"); + +// npm modules +const fixtures = require("haraka-test-fixtures"); + +// start of tests +// assert: https://nodejs.org/api/assert.html + +beforeEach(function () { + this.plugin = new fixtures.plugin("esets"); +}); + +describe("esets", function () { + it("loads", function () { + assert.ok(this.plugin); + }); +}); + +describe("load_esets_ini", function () { + it("loads esets.ini from config/esets.ini", function () { + this.plugin.load_esets_ini(); + assert.ok(this.plugin.cfg); + }); + + it("initializes enabled boolean", function () { + this.plugin.load_esets_ini(); + assert.equal(this.plugin.cfg.main.enabled, true, this.plugin.cfg); + }); +}); + +describe("uses text fixtures", function () { + it("sets up a connection", function () { + this.connection = fixtures.connection.createConnection({}); + assert.ok(this.connection.server); + }); + + it("sets up a transaction", function () { + this.connection = fixtures.connection.createConnection({}); + this.connection.transaction = fixtures.transaction.createTransaction({}); + assert.ok(this.connection.transaction.header); + }); +});