diff --git a/.codeclimate.yml b/.codeclimate.yml index 0e443ca..563da6b 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,10 +1,10 @@ engines: eslint: enabled: true - channel: "eslint-8" + channel: 'eslint-9' config: - config: ".eslintrc.yaml" + config: 'eslint.config.mjs' ratings: - paths: - - "**.js" + paths: + - '**.js' diff --git a/.eslintrc.yaml b/.eslintrc.yaml deleted file mode 100644 index b6d6abf..0000000 --- a/.eslintrc.yaml +++ /dev/null @@ -1,21 +0,0 @@ -env: - node: true - es6: true - mocha: true - -plugins: [ haraka ] - -extends: [ eslint:recommended, plugin:haraka/recommended ] - -root: true - -rules: - indent: [2, 2, { SwitchCase: 1 } ] - -globals: - OK: true - CONT: true - DENY: true - DENYSOFT: true - DENYDISCONNECT: true - DENYSOFTDISCONNECT: true diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 0449e4a..d450132 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -2,9 +2,9 @@ version: 2 updates: - - package-ecosystem: "npm" - directory: "/" + - package-ecosystem: 'npm' + directory: '/' schedule: - interval: "weekly" + interval: 'weekly' allow: - dependency-type: production diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5360933..3d01042 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,12 +1,11 @@ name: CI -on: [ push, pull_request ] +on: [push, pull_request] env: CI: true jobs: - lint: uses: haraka/.github/.github/workflows/lint.yml@master @@ -14,28 +13,10 @@ jobs: # uses: haraka/.github/.github/workflows/coverage.yml@master # secrets: inherit - test: - needs: [ lint, get-lts ] - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ ubuntu-latest, windows-latest ] - node-version: ${{ fromJson(needs.get-lts.outputs.active) }} - fail-fast: false - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - name: Node ${{ matrix.node-version }} on ${{ matrix.os }} - with: - node-version: ${{ matrix.node-version }} - - run: npm install - - run: npm test + ubuntu: + needs: [lint] + uses: haraka/.github/.github/workflows/ubuntu.yml@master - get-lts: - runs-on: ubuntu-latest - steps: - - id: get - uses: msimerson/node-lts-versions@v1 - outputs: - active: ${{ steps.get.outputs.active }} - lts: ${{ steps.get.outputs.lts }} + windows: + needs: [lint] + uses: haraka/.github/.github/workflows/windows.yml@master diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 383aca2..816e8c3 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,10 +1,10 @@ -name: "CodeQL" +name: 'CodeQL' on: push: - branches: [ master ] + branches: [master] pull_request: - branches: [ master ] + branches: [master] schedule: - cron: '18 7 * * 4' diff --git a/.release b/.release index 0890e94..7307651 160000 --- a/.release +++ b/.release @@ -1 +1 @@ -Subproject commit 0890e945e4e061c96c7b2ab45017525904c17728 +Subproject commit 73076513e83c2057a32515831b638771c15b1d83 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1ba5c37 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,90 @@ +# Changelog + +The format is based on [Keep a Changelog](https://keepachangelog.com/). + +### Unreleased + +### [1.0.15] - 2025-01-31 + +- doc: mv Changes.md CHANGELOG.md +- populate [files] in package.json. +- dep(eslint): upgrade to v9 +- doc(CONTRIBUTORS): added +- ci: use shared Haraka workflows +- style(prettier): added + +### [1.0.14] - 2023-12-12 + +- style: remove useless variable +- ci(publish): constrain to when package.json changes +- ci: run on PR + +### [1.0.13] - 2022-06-05 + +- doc(README): update CI badge URL +- ci: update dependabot config to production only + +### [1.0.12] - 2022-06-05 + +- ci: update GHA workflow with shared +- ci: add submodule .release + +### 1.0.11 - 2022-03-31 + +- node 12 EOL, drop testing. + +### 1.0.10 - 2019-03-22 + +- Add an 'if' to "blocked message" HTML header +- CI testing updates (node.js versions) +- moved config/karma.ini to test/config/karma.ini + +### 1.0.9 - 2017-08-17 + +- also prune syslog hostname when no PID in entry + +### 1.0.8 - 2017-06-16 + +- depend on haraka-eslint for rules +- lint fixes + +### 1.0.7 - 2017-05-04 + +- add --text flag to grep call, in case log file has binary chars + +### 1.0.6 - 2017-01-23 + +- remove host & pid detail from syslog lines + +### 1.0.5 - 2016-11-04 + +- display log entries for transactions +- display just transaction ID in place of full UUID.id +- refactored most of get logs into grepWithShell & asHtml + - with test coverage for the latter two + +### 1.0.4 - 2016-10-25 + +- remove useless $UUID token from display + +### Oct 2 12:57:42 2016 + +- trim uuids more reliably (#4) + +### Oct 2 12:08:44 2016 + +- Duplicate resolutions (#2) +- suppress duplicate actions + +### Sep 29 23:10:33 2016 + +- add missing eslint definition +- added README +- add .travis.yml +- initial commit + +[1.0.11]: https://github.com/haraka/haraka-plugin-log-reader/releases/tag/1.0.11 +[1.0.12]: https://github.com/haraka/haraka-plugin-log-reader/releases/tag/1.0.12 +[1.0.13]: https://github.com/haraka/haraka-plugin-log-reader/releases/tag/1.0.13 +[1.0.14]: https://github.com/haraka/haraka-plugin-log-reader/releases/tag/v1.0.14 +[1.0.15]: https://github.com/haraka/haraka-plugin-log-reader/releases/tag/v1.0.15 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 0000000..b6b5e58 --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,9 @@ +# Contributors + +This handcrafted artisinal software is brought to you by: + +|
msimerson (32) | +| :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | + +this file is generated by [.release](https://github.com/msimerson/.release). +Contribute to this project to get your GitHub profile included here. diff --git a/Changes.md b/Changes.md deleted file mode 100644 index 5cb0ea9..0000000 --- a/Changes.md +++ /dev/null @@ -1,90 +0,0 @@ -### Unreleased - - -### [1.0.14] - 2023-12-12 - -- style: remove useless variable -- ci(publish): constrain to when package.json changes -- ci: run on PR - - -### [1.0.13] - 2022-06-05 - -- doc(README): update CI badge URL -- ci: update dependabot config to production only - - -### [1.0.12] - 2022-06-05 - -- ci: update GHA workflow with shared -- ci: add submodule .release - - -### 1.0.11 - 2022-03-31 - -- node 12 EOL, drop testing. - - -### 1.0.10 - 2019-03-22 - -* Add an 'if' to "blocked message" HTML header -* CI testing updates (node.js versions) -* moved config/karma.ini to test/config/karma.ini - - -### 1.0.9 - 2017-08-17 - -* also prune syslog hostname when no PID in entry - - -### 1.0.8 - 2017-06-16 - -* depend on haraka-eslint for rules -* lint fixes - - -### 1.0.7 - 2017-05-04 - -* add --text flag to grep call, in case log file has binary chars - - -### 1.0.6 - 2017-01-23 - -* remove host & pid detail from syslog lines - - -### 1.0.5 - 2016-11-04 - -* display log entries for transactions -* display just transaction ID in place of full UUID.id -* refactored most of get logs into grepWithShell & asHtml - * with test coverage for the latter two - - -### 1.0.4 - 2016-10-25 - -* remove useless $UUID token from display - - -### Oct 2 12:57:42 2016 - -* trim uuids more reliably (#4) - - -### Oct 2 12:08:44 2016 - -* Duplicate resolutions (#2) -* suppress duplicate actions - - -### Sep 29 23:10:33 2016 - -* add missing eslint definition -* added README -* add .travis.yml -* initial commit - - -[1.0.12]: https://github.com/haraka/haraka-plugin-log-reader/releases/tag/1.0.12 -[1.0.13]: https://github.com/haraka/haraka-plugin-log-reader/releases/tag/1.0.13 -[1.0.14]: https://github.com/haraka/haraka-plugin-log-reader/releases/tag/1.0.14 diff --git a/README.md b/README.md index b52cee1..d61d444 100644 --- a/README.md +++ b/README.md @@ -3,60 +3,54 @@ # haraka-plugin-log-reader - extracts matching log entries from the haraka log file - # Install - -```` +``` cd /my/haraka/config/dir npm install haraka-plugin-log-reader -```` - +``` ## Enable Add `log-reader` to `haraka/config/plugins` file. - # Usage When enabled, this plugin registers two URL routes in Haraka's http server: -* karma/rules -* /logs/:uuid +- karma/rules +- /logs/:uuid The former rule simply returns a list of the Haraka rules in use. The http client uses those rules (the ID, reason, and value) to display the `Policy Rules` and `Steps to Resolve` sections in the web page. # Example - ### Sorry we blocked your message: Our filters mistook your server for a malicious computer attempting to send spam. To improve your mail servers reputation, please contact your IT helpdesk or Systems Administrator and ask them for help. ----------- +--- ### Policy Rules -* -7, DNS Blacklist (b.barracudacentral.org) -* -5, DNS Blacklist (zen.spamhaus.org) -* -3, DNS Blacklist (dnsbl-1.uceprotect.net) -* -3, DNS Blacklist (bl.spamcop.net) -* -3, ASN reputation is spam-only (asn_all_bad) -* -1, Geographic distance is unusual for ham (4000) -* -1, Geographic distance is unusual for ham (8000) -* -1, ASN reputation is bad (karma) +- -7, DNS Blacklist (b.barracudacentral.org) +- -5, DNS Blacklist (zen.spamhaus.org) +- -3, DNS Blacklist (dnsbl-1.uceprotect.net) +- -3, DNS Blacklist (bl.spamcop.net) +- -3, ASN reputation is spam-only (asn_all_bad) +- -1, Geographic distance is unusual for ham (4000) +- -1, Geographic distance is unusual for ham (8000) +- -1, ASN reputation is bad (karma) ----------- +--- ### Steps to Resolve -* Disinfect your host/network +- Disinfect your host/network ----------- +--- ## Raw Logs @@ -75,12 +69,9 @@ Our filters mistook your server for a malicious computer attempting to send spam [NOTICE] [core] disconnect ip=95.160.74.108 rdns="095160074108.gdansk.vectranet.pl" helo="" relay=N early=N esmtp=N tls=N pipe=N errors=0 txns=0 rcpts=0/0/0 msgs=0/0/0 bytes=0 lr="" time=12.752 - [ci-img]: https://github.com/haraka/haraka-plugin-log-reader/actions/workflows/ci.yml/badge.svg [ci-url]: https://github.com/haraka/haraka-plugin-log-reader/actions/workflows/ci.yml [cov-img]: https://codecov.io/github/haraka/haraka-plugin-log-reader/coverage.svg [cov-url]: https://codecov.io/github/haraka/haraka-plugin-log-reader [clim-img]: https://codeclimate.com/github/haraka/haraka-plugin-log-reader/badges/gpa.svg [clim-url]: https://codeclimate.com/github/haraka/haraka-plugin-log-reader -[npm-img]: https://nodei.co/npm/haraka-plugin-log-reader.png -[npm-url]: https://www.npmjs.com/package/haraka-plugin-log-reader diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..32383fe --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,35 @@ +import globals from 'globals' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import js from '@eslint/js' +import { FlatCompat } from '@eslint/eslintrc' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}) + +export default [ + ...compat.extends('@haraka'), + { + languageOptions: { + globals: { + ...globals.node, + ...globals.mocha, + }, + }, + + rules: { + indent: [ + 2, + 2, + { + SwitchCase: 1, + }, + ], + }, + }, +] diff --git a/index.js b/index.js index 229359e..7301059 100644 --- a/index.js +++ b/index.js @@ -1,167 +1,165 @@ -'use strict'; +'use strict' // node.js built-in modules -const spawn = require('child_process').spawn; +const spawn = require('child_process').spawn -let log = '/var/log/haraka.log'; -let plugin; +let log = '/var/log/haraka.log' +let plugin exports.register = function () { - plugin = this; - this.get_logreader_ini(); - this.load_karma_ini(); + plugin = this + this.get_logreader_ini() + this.load_karma_ini() } exports.hook_init_http = function (next, server) { - server.http.app.use('/logs/:uuid', exports.get_logs); - server.http.app.use('/karma/rules', exports.get_rules); - next(); + server.http.app.use('/logs/:uuid', exports.get_logs) + server.http.app.use('/karma/rules', exports.get_rules) + next() } exports.get_logreader_ini = function () { plugin.cfg = plugin.config.get('log.reader.ini', function () { - plugin.get_logreader_ini(); + plugin.get_logreader_ini() }) if (plugin.cfg.log && plugin.cfg.log.file) { - log = plugin.cfg.log.file; + log = plugin.cfg.log.file } } exports.load_karma_ini = function () { plugin.karma_cfg = plugin.config.get('karma.ini', function () { - plugin.load_karma_ini(); + plugin.load_karma_ini() }) - if (!plugin.karma_cfg.result_awards) return; - if (!plugin.result_awards) plugin.result_awards = {}; + if (!plugin.karma_cfg.result_awards) return + if (!plugin.result_awards) plugin.result_awards = {} Object.keys(plugin.karma_cfg.result_awards).forEach(function (anum) { const parts = plugin.karma_cfg.result_awards[anum] .replace(/\s+/, ' ') - .split(/(?:\s*\|\s*)/); + .split(/(?:\s*\|\s*)/) plugin.result_awards[anum] = { - pi_name : parts[0], - property : parts[1], - operator : parts[2], - value : parts[3], - award : parts[4], - reason : parts[5], - resolution : parts[6], - }; - }); + pi_name: parts[0], + property: parts[1], + operator: parts[2], + value: parts[3], + award: parts[4], + reason: parts[5], + resolution: parts[6], + } + }) } exports.get_rules = function (req, res) { - res.send(JSON.stringify(plugin.result_awards)); + res.send(JSON.stringify(plugin.result_awards)) } exports.get_logs = function (req, res) { - - const uuid = req.params.uuid; + const uuid = req.params.uuid if (!/-/.test(uuid)) { - return res.send('Invalid Request'); + return res.send('Invalid Request') } if (!/^[0-9A-F\-.]{12,40}$/.test(uuid)) { - return res.send('Invalid Request'); + return res.send('Invalid Request') } // spawning a grep process is quite a lot faster than fs.read // (yes, I benchmarked it) exports.grepWithShell(log, uuid, function (err, matched) { - if (err) return res.send(`

${err}

`); + if (err) return res.send(`

${err}

`) exports.asHtml(uuid, matched, function (html) { - res.send(html); - }); - }); + res.send(html) + }) + }) } exports.grepWithShell = function (file, uuid, done) { - - let matched = ''; - let searchString = uuid; + let matched = '' + let searchString = uuid if (/\.[0-9]{1,2}$/.test(uuid)) { // strip transaction off ID, so connection properties are included - searchString = uuid.replace(/\.[0-9]{1,2}$/, ''); + searchString = uuid.replace(/\.[0-9]{1,2}$/, '') } // var child = spawn('grep', [ '-e', regex, file ]); - const child = spawn('grep', [ '--text', searchString, file ]); + const child = spawn('grep', ['--text', searchString, file]) child.stdout.on('data', function (buffer) { - matched += buffer.toString(); - }); + matched += buffer.toString() + }) child.stdout.on('end', function (err) { - done(err, matched); - }); -}; + done(err, matched) + }) +} exports.asHtml = function (uuid, matched, done) { - let rawLogs = '' let lastKarmaLine let monthDay = '' - const matchMonthDay = new RegExp('^([A-Z][a-z]{2}[ ]{1,2}[0-9]{1,2}) '); + const matchMonthDay = new RegExp('^([A-Z][a-z]{2}[ ]{1,2}[0-9]{1,2}) ') matched.split('\n').forEach((line) => { + if (!line) return - if (!line) return; - - let transId; - let replaceString = ''; + let transId + let replaceString = '' if (!monthDay) { try { - [, monthDay] = matchMonthDay.exec(line) - } - catch (err) { + ;[, monthDay] = matchMonthDay.exec(line) + } catch (err) { plugin.loginfo(line) plugin.logerror(err) } } - const uuidMatch = line.match(/ \[([A-F0-9\-.]{12,40})\] /); + const uuidMatch = line.match(/ \[([A-F0-9\-.]{12,40})\] /) if (uuidMatch && uuidMatch[1]) { - transId = uuidMatch[1].match(/\.([0-9]{1,2})$/); + transId = uuidMatch[1].match(/\.([0-9]{1,2})$/) } - if (transId && transId[1]) replaceString = `[${transId[1]}] `; + if (transId && transId[1]) replaceString = `[${transId[1]}] ` let trimmed = line - .replace(/\[[A-F0-9\-.]{12,40}\] /, replaceString) // UUID - .replace(matchMonthDay, ''); // Mon DD + .replace(/\[[A-F0-9\-.]{12,40}\] /, replaceString) // UUID + .replace(matchMonthDay, '') // Mon DD // strip prepended hostname - if ( / haraka\[[0-9]+\]: /.test(trimmed) ) { // with PID - trimmed = trimmed.replace(/(?: [a-z.-]+)? haraka\[[0-9]+\]: /, ' '); - } - else if ( / haraka: \[/.test(trimmed) ) { // w/o PID - trimmed = trimmed.replace(/(?: [a-z.-]+)? haraka: /, ' '); + if (/ haraka\[[0-9]+\]: /.test(trimmed)) { + // with PID + trimmed = trimmed.replace(/(?: [a-z.-]+)? haraka\[[0-9]+\]: /, ' ') + } else if (/ haraka: \[/.test(trimmed)) { + // w/o PID + trimmed = trimmed.replace(/(?: [a-z.-]+)? haraka: /, ' ') } - rawLogs += `${trimmed}
`; + rawLogs += `${trimmed}
` if (/\[karma/.test(line) && /awards/.test(line)) { - lastKarmaLine = line; + lastKarmaLine = line } }) - let awardNums = []; + let awardNums = [] if (lastKarmaLine) { - const bits = lastKarmaLine.match(/awards: ([0-9,]+)?\s*/); - if (bits && bits[1]) awardNums = bits[1].split(','); + const bits = lastKarmaLine.match(/awards: ([0-9,]+)?\s*/) + if (bits && bits[1]) awardNums = bits[1].split(',') } done( - `${htmlHead() + - htmlBody( - `for connection ${uuid} on ${monthDay}`, - getAwards(awardNums).join(''), - getResolutions(awardNums).join('') - ) + - rawLogs}` - ); + `${ + htmlHead() + + htmlBody( + `for connection ${uuid} on ${monthDay}`, + getAwards(awardNums).join(''), + getResolutions(awardNums).join(''), + ) + + rawLogs + }`, + ) } // exports.grepWithFs = function (file, regex, done) { @@ -178,55 +176,55 @@ exports.asHtml = function (uuid, matched, done) { // }); // }; -function getAwards (awardNums) { - if (!awardNums || awardNums.length === 0) return []; +function getAwards(awardNums) { + if (!awardNums || awardNums.length === 0) return [] - const awards = []; + const awards = [] awardNums.forEach(function (a) { - if (!a || !plugin.result_awards[a]) return; - plugin.result_awards[a].id = a; - awards.push(plugin.result_awards[a]); - }); + if (!a || !plugin.result_awards[a]) return + plugin.result_awards[a].id = a + awards.push(plugin.result_awards[a]) + }) - const listItems = []; + const listItems = [] awards.sort(sortByAward).forEach(function (a) { - const start = `
  • ${a.award}, `; + const start = `
  • ${a.award}, ` if (a.reason) { - listItems.push(`${start + a.reason} (${a.value})
  • `); - return; + listItems.push(`${start + a.reason} (${a.value})`) + return } - listItems.push(`${start + a.pi_name} ${a.property} ${a.value}`); - }); - return listItems; + listItems.push(`${start + a.pi_name} ${a.property} ${a.value}`) + }) + return listItems } -function getResolutions (awardNums) { - if (!awardNums || awardNums.length === 0) return []; +function getResolutions(awardNums) { + if (!awardNums || awardNums.length === 0) return [] - const awards = []; + const awards = [] awardNums.forEach(function (a) { - if (!a || !plugin.result_awards[a]) return; - awards.push(plugin.result_awards[a]); - }); + if (!a || !plugin.result_awards[a]) return + awards.push(plugin.result_awards[a]) + }) - const listItems = []; - const resolutionSeen = {}; + const listItems = [] + const resolutionSeen = {} awards.sort(sortByAward).forEach(function (a) { - if (!a.resolution) return; - if (resolutionSeen[a.resolution]) return; - resolutionSeen[a.resolution] = true; - listItems.push(`
  • ${a.resolution}
  • `); - }); - return listItems; + if (!a.resolution) return + if (resolutionSeen[a.resolution]) return + resolutionSeen[a.resolution] = true + listItems.push(`
  • ${a.resolution}
  • `) + }) + return listItems } -function sortByAward (a, b) { - if (parseFloat(b.award) > parseFloat(a.award)) return -1; - if (parseFloat(b.award) < parseFloat(a.award)) return 1; - return 0; +function sortByAward(a, b) { + if (parseFloat(b.award) > parseFloat(a.award)) return -1 + if (parseFloat(b.award) < parseFloat(a.award)) return 1 + return 0 } -function htmlHead () { +function htmlHead() { return ' \ \ \ @@ -235,31 +233,32 @@ function htmlHead () { \ - '; + ' } -function htmlBody (uuid, awards, resolve) { - let str = ' \ +function htmlBody(uuid, awards, resolve) { + let str = + ' \
    \

    Sorry if we blocked your message:

    \

    Our filters mistook your server for a malicious computer attempting \ to send spam. To improve your mail servers reputation, please contact \ - your IT helpdesk or Systems Administrator and ask them for help.

    '; + your IT helpdesk or Systems Administrator and ask them for help.

    ' if (awards) { str += `

    Policy Rules Matched

    \ - `; + ` } if (resolve) { str += `

    Steps to Resolve

    \ - `; + ` } str += `
    \

    Raw Logs

    \

    ${uuid}

    \
     \
    -        \n`;
    -  return str;
    +        \n`
    +  return str
     }
    diff --git a/package.json b/package.json
    index de40a3d..692fcaa 100644
    --- a/package.json
    +++ b/package.json
    @@ -1,14 +1,22 @@
     {
       "name": "haraka-plugin-log-reader",
    -  "version": "1.0.14",
    +  "version": "1.0.15",
       "description": "display log entries from haraka log files via HTTP",
       "main": "index.js",
    +  "files": [
    +    "CHANGELOG.md",
    +    "config"
    +  ],
       "scripts": {
    -    "test": "npx mocha",
    -    "lint": "npx eslint *.js test",
    -    "lintfix": "npx eslint --fix *.js test",
         "cover": "npx istanbul cover npm test",
    -    "versions": "npx dependency-version-checker check"
    +    "format": "npm run prettier:fix && npm run lint:fix",
    +    "lint": "npx eslint *.js test",
    +    "lint:fix": "npx eslint --fix *.js test",
    +    "prettier": "npx prettier . --check",
    +    "prettier:fix": "npx prettier . --write --log-level=warn",
    +    "test": "npx mocha@^11",
    +    "versions": "npx dependency-version-checker check",
    +    "versions:fix": "npx dependency-version-checker update"
       },
       "repository": {
         "type": "git",
    @@ -27,9 +35,11 @@
       },
       "homepage": "https://github.com/haraka/haraka-plugin-log-reader#readme",
       "devDependencies": {
    -    "eslint": "^8",
    -    "eslint-plugin-haraka": "*",
    -    "haraka-test-fixtures": "*",
    -    "mocha": "*"
    +    "haraka-test-fixtures": "^1.3.9",
    +    "@haraka/eslint-config": "^2.0.2"
    +  },
    +  "prettier": {
    +    "singleQuote": true,
    +    "semi": false
       }
     }
    diff --git a/test/index.js b/test/index.js
    index d5a44b6..a7a06af 100644
    --- a/test/index.js
    +++ b/test/index.js
    @@ -1,11 +1,11 @@
    -'use strict';
    +'use strict'
     
    -process.env.NODE_ENV = 'test';
    +process.env.NODE_ENV = 'test'
     
    -const assert   = require('assert');
    -const path     = require('path');
    +const assert = require('assert')
    +const path = require('path')
     
    -const fixtures = require('haraka-test-fixtures');
    +const fixtures = require('haraka-test-fixtures')
     
     beforeEach((done) => {
       this.reader = new fixtures.plugin('index')
    @@ -13,7 +13,6 @@ beforeEach((done) => {
     })
     
     describe('register', () => {
    -
       it('is a function', (done) => {
         assert.equal('function', typeof this.reader.register)
         done()
    @@ -30,15 +29,15 @@ describe('register', () => {
         assert.deepEqual(this.reader.cfg, {
           main: {},
           log: {
    -        file: '/var/log/haraka.log'
    -      }
    +        file: '/var/log/haraka.log',
    +      },
         })
         done()
       })
     
       it('loads karma.ini', (done) => {
         this.reader.register()
    -    this.reader.config = this.reader.config.module_config(path.resolve('test'));
    +    this.reader.config = this.reader.config.module_config(path.resolve('test'))
         this.reader.load_karma_ini()
         assert.equal(this.reader.karma_cfg.tarpit.delay, 0)
         done()
    @@ -54,45 +53,52 @@ describe('log.reader.ini', () => {
     })
     
     describe('grepWithShell', () => {
    -
       beforeEach((done) => {
         this.reader = new fixtures.plugin('index')
    -    this.reader.register();
    -    this.reader.config = this.reader.config.module_config(path.resolve('test'));
    +    this.reader.register()
    +    this.reader.config = this.reader.config.module_config(path.resolve('test'))
         this.reader.load_karma_ini()
         done()
       })
     
       it('reads matching connection entries from a log file', (done) => {
    -    const logfile = path.join('test','fixtures','haraka.log');
    -    this.reader.grepWithShell(logfile, '3E6A027F-8307-4DA4-B105-2A39EC4B58D4', (err, r) => {
    -      assert.ifError(err);
    -      // console.log(r);
    -      assert.equal(r.split('\n').length - 1, 36);
    -      done();
    -    })
    +    const logfile = path.join('test', 'fixtures', 'haraka.log')
    +    this.reader.grepWithShell(
    +      logfile,
    +      '3E6A027F-8307-4DA4-B105-2A39EC4B58D4',
    +      (err, r) => {
    +        assert.ifError(err)
    +        // console.log(r);
    +        assert.equal(r.split('\n').length - 1, 36)
    +        done()
    +      },
    +    )
       })
     
       it('reads matching transaction entries from a log file', (done) => {
    -    const logfile = path.join('test','fixtures','haraka.log');
    -    this.reader.grepWithShell(logfile, '3E6A027F-8307-4DA4-B105-2A39EC4B58D4.1', (err, r) => {
    -      assert.ifError(err);
    -      // console.log(r);
    -      assert.equal(r.split('\n').length - 1, 36);
    -      done();
    -    })
    +    const logfile = path.join('test', 'fixtures', 'haraka.log')
    +    this.reader.grepWithShell(
    +      logfile,
    +      '3E6A027F-8307-4DA4-B105-2A39EC4B58D4.1',
    +      (err, r) => {
    +        assert.ifError(err)
    +        // console.log(r);
    +        assert.equal(r.split('\n').length - 1, 36)
    +        done()
    +      },
    +    )
       })
     
       it('formats matching entries as HTML', (done) => {
    -    const uuid = '3E6A027F-8307-4DA4-B105-2A39EC4B58D4.1';
    -    const logfile = path.join('test','fixtures','haraka.log');
    +    const uuid = '3E6A027F-8307-4DA4-B105-2A39EC4B58D4.1'
    +    const logfile = path.join('test', 'fixtures', 'haraka.log')
         this.reader.grepWithShell(logfile, uuid, (err, r) => {
    -      assert.ifError(err);
    +      assert.ifError(err)
           this.reader.asHtml(uuid, r, (html) => {
             // console.log(html);
    -        assert.ok(/^/.test(html));
    -        assert.ok(/<\/html>$/.test(html));
    -        done();
    +        assert.ok(/^/.test(html))
    +        assert.ok(/<\/html>$/.test(html))
    +        done()
           })
         })
       })
    @@ -101,15 +107,15 @@ describe('grepWithShell', () => {
     describe('asHtml', () => {
       beforeEach((done) => {
         this.reader = new fixtures.plugin('index')
    -    this.reader.register();
    -    this.reader.config = this.reader.config.module_config(path.resolve('test'));
    +    this.reader.register()
    +    this.reader.config = this.reader.config.module_config(path.resolve('test'))
         this.reader.load_karma_ini()
         done()
       })
     
       it('formats a block of log lines for HTML presentation', (done) => {
         const uuid = '9613CD00-7145-4ABC-8CA8-79CD9E39BB4F'
    -    const logfile = path.join('test','fixtures','haraka.log');
    +    const logfile = path.join('test', 'fixtures', 'haraka.log')
         this.reader.grepWithShell(logfile, uuid, (err, r) => {
           this.reader.asHtml(uuid, r, (html) => {
             assert.ok(/^/.test(html))
    @@ -126,6 +132,6 @@ describe('asHtml', () => {
     
     describe('get_rules', () => {
       it.skip('returns rules section from karma.ini', (done) => {
    -    done();
    +    done()
       })
     })