From 2aa33ed65e578d74138ca4116106fbdc086f8943 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Sun, 7 Apr 2024 11:26:09 -0700 Subject: [PATCH] Release 2.1.4 (#57) - eslint: reduce config to depending on @haraka - package.json: added scripts - doc(CHANGELOG): ordered urls - prettier, eslint config - chore: populate package.json [files] --- .codeclimate.yml | 8 +- .eslintrc.yaml | 21 +- .github/ISSUE_TEMPLATE/bug_report.md | 3 - .github/ISSUE_TEMPLATE/custom.md | 3 - .github/ISSUE_TEMPLATE/feature_request.md | 1 - .github/workflows/ci.yml | 9 +- .github/workflows/codeql.yml | 4 +- .prettierrc | 2 + .release | 2 +- Changes.md => CHANGELOG.md | 58 ++-- README.md | 34 +- index.js | 405 ++++++++++++---------- package.json | 33 +- test/karma.js | 350 ++++++++++++------- 14 files changed, 521 insertions(+), 412 deletions(-) create mode 100644 .prettierrc rename Changes.md => CHANGELOG.md (68%) diff --git a/.codeclimate.yml b/.codeclimate.yml index a90fa85..4b41208 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,9 +1,9 @@ engines: eslint: enabled: true - channel: "eslint-8" + channel: 'eslint-8' config: - config: ".eslintrc.yaml" + config: '.eslintrc.yaml' checks: return-statements: @@ -21,5 +21,5 @@ checks: threshold: 900 ratings: - paths: - - "**.js" + paths: + - '**.js' diff --git a/.eslintrc.yaml b/.eslintrc.yaml index bb74f3c..df162d8 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -2,23 +2,6 @@ env: node: true es6: true mocha: true - es2020: true + es2022: true -plugins: [ haraka ] - -extends: [ eslint:recommended, plugin:haraka/recommended ] - -root: true - -rules: - indent: [2, 2, { SwitchCase: 1 } ] - semi: [ error, never ] - semi-style: [ error, last ] - -globals: - OK: true - CONT: true - DENY: true - DENYSOFT: true - DENYDISCONNECT: true - DENYSOFTDISCONNECT: true +extends: '@haraka' diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index d025e7f..ba6cc49 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -4,7 +4,6 @@ about: Create a report to help us improve title: '' labels: '' assignees: '' - --- **System Info:** @@ -21,9 +20,7 @@ A clear and concise description of what you expected to happen. **Observed behavior** - **Steps To Reproduce** - **Additional context** Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md index 588d8a2..ebda151 100644 --- a/.github/ISSUE_TEMPLATE/custom.md +++ b/.github/ISSUE_TEMPLATE/custom.md @@ -4,7 +4,4 @@ about: Issues that aren't bug reports or feature requests title: '' labels: '' assignees: '' - --- - - diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index bbcbbe7..2f28cea 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -4,7 +4,6 @@ about: Suggest an idea for this project title: '' labels: '' assignees: '' - --- **Is your feature request related to a problem? Please describe.** diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 25b3030..b283bcf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,19 +1,18 @@ name: CI -on: [ push, pull_request ] +on: [push, pull_request] env: CI: true jobs: - lint: uses: haraka/.github/.github/workflows/lint.yml@master ubuntu: - needs: [ lint ] + needs: [lint] uses: haraka/.github/.github/workflows/ubuntu.yml@master windows: - needs: [ lint ] - uses: haraka/.github/.github/workflows/windows.yml@master \ No newline at end of file + needs: [lint] + uses: haraka/.github/.github/workflows/windows.yml@master diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 3627451..8314a66 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -2,10 +2,10 @@ name: CodeQL on: push: - branches: [ master ] + branches: [master] pull_request: # The branches below must be a subset of the branches above - branches: [ master ] + branches: [master] schedule: - cron: '18 7 * * 4' diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..8ded5e0 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,2 @@ +singleQuote: true +semi: false diff --git a/.release b/.release index 0890e94..2ed3ca5 160000 --- a/.release +++ b/.release @@ -1 +1 @@ -Subproject commit 0890e945e4e061c96c7b2ab45017525904c17728 +Subproject commit 2ed3ca5c98d35688925cd7011b3ec26b32a3a101 diff --git a/Changes.md b/CHANGELOG.md similarity index 68% rename from Changes.md rename to CHANGELOG.md index 9929c7a..0bf8627 100644 --- a/Changes.md +++ b/CHANGELOG.md @@ -1,126 +1,126 @@ +# Changelog -#### N.N.N - YYYY-MM-DD +The format is based on [Keep a Changelog](https://keepachangelog.com/). + +### Unreleased + +### [2.1.4] - 2024-04-06 + +- eslint: reduce config to depending on @haraka +- package.json: added scripts +- doc(CHANGELOG): ordered urls +- doc(Changes) -> CHANGELOG +- prettier & eslint configs +- chore: populate package.json [files] ### [2.1.3] - 2023-12-12 - ci: use shared configs - style(es6): replace for..i with for...of -- deps(*): bump versions to latest - +- deps(\*): bump versions to latest ### [2.1.2] - 2023-12-11 - config: update several plugin names - style(es6): refer to plugin as 'this' - ### [2.1.1] - 2023-08-22 - fix: check_result unexpected return #50 - ### [2.1.0] - 2022-11-29 - fix: in disconnect, call redis_unsub after skip check - dep(redis): 4 -> 4.1 - dep(pi-redis): 2 -> 2.0.5 - -#### 2.0.4 - 2022-05-28 +#### [2.0.4] - 2022-05-28 - use .release as submodule - -#### 2.0.3 - 2022-05-28 +#### [2.0.3] - 2022-05-28 - fix: depend directly on redis - fix: update redis command names for v4 compatibility - fix: update redis commands to be async - -#### 2.0.1 - 2022-05-27 +#### [2.0.1] - 2022-05-27 - chore(ci): depend on shared GHA workflows - -#### 2.0.0 - 2022-03-29 +#### [2.0.0] - 2022-03-29 - remove lots of plugin=this - remove unnecessary braces and trailing ; - some promises. - #### 1.0.14 - 2022-02-14 - try to unsubscribe in case connection is marked to skip during transaction - #### 1.0.13 - 2019-04-23 - add 'exists' pattern - #### 1.0.12 - 2019-03-08 - don't interfere with STARTLS and AUTH when karma is listed above those plugins in config/plugins - #### 1.0.11 - 2017-10-25 - private addresses and flagged connections exemption - #### 1.0.10 - 2017-08-30 - add TLS awards #19 - #### 1.0.9 - 2017-07-29 - splash on some es6 - add AppVeyor CI testing - #### 1.0.8 - 2017-06-26 - revert #9, it breaks current Haraka deployments - #### 1.0.7 - 2017-06-16 - update for eslint 4 compat - Add results_redis_publish=true for haraka-results changes #9 - #### 1.0.6 - 2017-05-04 - emit error if redis plugin didn't create connection - #### 1.0.5 - 2017-02-06 - move merge_redis_ini into load_karma_ini, so it also gets applied after a karma.ini change - skip redis operations when no connection exists - #### 1.0.4 - 2017-01-29 - use the new haraka-plugin-redis - remove exceptions for soft denials. This makes denial time simpler. - rules updates - #### 1.0.3 - 2017-01-27 - add rule #280 for known-senders - add support for 'length' type, with eq, gt, and lt operators - use shared haraka-eslint - #### 1.0.2 - 2017-01-24 - use redis.merge_redis_ini() -[2.1.0]: https://github.com/haraka/haraka-plugin-karma/releases/tag/2.1.0 -[2.1.1]: https://github.com/haraka/haraka-plugin-karma/releases/tag/2.1.1 -[2.1.2]: https://github.com/haraka/haraka-plugin-karma/releases/tag/2.1.2 + +[2.0.0]: https://github.com/haraka/haraka-plugin-karma/releases/tag/2.0.0 +[2.0.1]: https://github.com/haraka/haraka-plugin-karma/releases/tag/2.0.1 +[2.0.2]: https://github.com/haraka/haraka-plugin-karma/releases/tag/2.0.2 +[2.0.3]: https://github.com/haraka/haraka-plugin-karma/releases/tag/2.0.3 +[2.0.4]: https://github.com/haraka/haraka-plugin-karma/releases/tag/2.0.4 +[2.1.0]: https://github.com/haraka/haraka-plugin-karma/releases/tag/v2.1.0 +[2.1.1]: https://github.com/haraka/haraka-plugin-karma/releases/tag/v2.1.1 +[2.1.2]: https://github.com/haraka/haraka-plugin-karma/releases/tag/v2.1.2 +[2.1.3]: https://github.com/haraka/haraka-plugin-karma/releases/tag/v2.1.3 +[2.1.4]: https://github.com/haraka/haraka-plugin-karma/releases/tag/v2.1.4 diff --git a/README.md b/README.md index 2de8632..7e4b198 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,6 @@ One challenge for mail filtering is that filters (or plugins, in Haraka's case) In order to score a plugins results, plugins must save their results to the [Result Store][results-url]. Karma will see that and apply the awards specified in `karma.ini`. - ## How Karma Works Karma takes a holistic view of **connections**. During the connection, karma collects these results and applies the [result_awards](#awards) defined in `karma.ini`. Once a connection/message exceeds the threshold.negative score (default: -8), karma rejects it at the next [deny]hook. @@ -32,14 +31,12 @@ The scoring mechanism is not dissimilar to [SpamAssassin][sa-url], but Karma has * Access to raw SMTP commands (data + formatting inspection) * Can reject connections before DATA (save lots of bandwidth) -Karma is not a replacement for content filters. Karma focuses on the quality of the **connection**. Content filters (bayes\*) focus on the content of the **message**. Karma works best *with* content filters. - +Karma is not a replacement for content filters. Karma focuses on the quality of the **connection**. Content filters (bayes\*) focus on the content of the **message**. Karma works best _with_ content filters. # CONFIG See config/karma.ini for options and inline documentation. - ## AWARDS Karma allows the site administrator to control how much weight to assign to @@ -50,7 +47,6 @@ Karma begins scoring the connection when the first packet arrives. The IP reputa Karma performs checks early and often, maximizing the penality it can exact upon bad mailers. - ## Penalties ### Deny / Reject @@ -61,7 +57,7 @@ When connections become worse than [thresholds]negative, they are denied during Karma history is computed as the number of good - bad connections. -When each connection ends, *karma* records the result. When a sufficient history has been built for an IP or ASN, future connections from that address(es) will get a positive or negative karma award. +When each connection ends, _karma_ records the result. When a sufficient history has been built for an IP or ASN, future connections from that address(es) will get a positive or negative karma award. The reward is purposefully small, to permit good senders in bad neighborhoods to still send. @@ -98,21 +94,20 @@ When using `karma`, do not use Haraka's `tarpit` plugin. Connection data that karma considers: -* [IP Reputation](#IP_Reputation) -* [ASN reputation](#Neighbor_Reputation) -* DENY events by other plugins -* envelope sender from a spammy TLD -* [malformed envelope addresses](#malformed_env) -* [unrecognized SMTP commands](#unrecognized) -* matching *env from* and *env to* name (rare in ham, frequent in spam) +- [IP Reputation](#IP_Reputation) +- [ASN reputation](#Neighbor_Reputation) +- DENY events by other plugins +- envelope sender from a spammy TLD +- [malformed envelope addresses](#malformed_env) +- [unrecognized SMTP commands](#unrecognized) +- matching _env from_ and _env to_ name (rare in ham, frequent in spam) The data from these tests are helpful but the real power of karma is [scoring the results](#awards) of other plugins. See karma.ini for a rich set of examples. - ### IP Reputation -Karma records the number of good, bad, and total connections. The results +Karma records the number of good, bad, and total connections. The results are accessible to other plugins as well. var karma = connection.results.get('karma'); @@ -142,14 +137,14 @@ connections from each ASN. ASNs with less than 5 karma points in either direction are ignored. -#### report\_as +#### report_as Store the ASN results as another plugin. Example: I set `report_as=asn`, so that karma history for an ASN is reported with the ASN plugin data. A practical consequence of changing report_as is that the award location in karma.ini would need to change from: NNN karma | pass | equals | asn_all_good | 2 NNN karma | fail | equals | asn_all_bad | -3 -to: +to: NNN asn | pass | equals | asn_all_good | 2 NNN asn | fail | equals | asn_all_bad | -3 @@ -158,7 +153,6 @@ to: Very old versions of Outlook Express and some malware senders don't bother complying with the RFC (5321, 2821, 821) address format. Karma checks the envelope from and to addresses for a common RFC ignorant pattern that is highly correlated with malware. - ### Unrecognized SMTP verbs/commands Certain bruteforce password hacking tools have a pre-programmed SMTP path @@ -190,9 +184,7 @@ Karma plugin is disabled for all private IP addresses according to RFC1918. Opti Karma is most effective at filtering mail delivered by bots and rogue servers. Spam delivered by servers with good reputations normally pass karma's checks. -Expect to use karma *with* content filters. - - +Expect to use karma _with_ content filters. [p0f-url]: /manual/plugins/connect.p0f.html [geoip-url]: https://github.com/haraka/haraka-plugin-geoip diff --git a/index.js b/index.js index a285dba..e319040 100644 --- a/index.js +++ b/index.js @@ -2,31 +2,44 @@ // karma - reward good and penalize bad mail senders const constants = require('haraka-constants') -const redis = require('redis') -const utils = require('haraka-utils') +const redis = require('redis') +const utils = require('haraka-utils') const phase_prefixes = utils.to_object([ - 'connect','helo','mail_from','rcpt_to','data' + 'connect', + 'helo', + 'mail_from', + 'rcpt_to', + 'data', ]) exports.register = function () { - this.inherits('haraka-plugin-redis') // set up defaults - this.deny_hooks = utils.to_object( - ['unrecognized_command','helo','data','data_post','queue','queue_outbound'] - ) + this.deny_hooks = utils.to_object([ + 'unrecognized_command', + 'helo', + 'data', + 'data_post', + 'queue', + 'queue_outbound', + ]) this.deny_exclude_hooks = utils.to_object('rcpt_to queue queue_outbound') this.deny_exclude_plugins = utils.to_object([ - 'access', 'helo.checks', 'data.headers', 'spamassassin', - 'mail_from.is_resolvable', 'clamd', 'tls' + 'access', + 'helo.checks', + 'data.headers', + 'spamassassin', + 'mail_from.is_resolvable', + 'clamd', + 'tls', ]) this.load_karma_ini() - this.register_hook('init_master', 'init_redis_plugin') - this.register_hook('init_child', 'init_redis_plugin') + this.register_hook('init_master', 'init_redis_plugin') + this.register_hook('init_child', 'init_redis_plugin') this.register_hook('connect_init', 'results_init') this.register_hook('connect_init', 'ip_history_from_redis') @@ -35,13 +48,15 @@ exports.register = function () { exports.load_karma_ini = function () { const plugin = this - plugin.cfg = plugin.config.get('karma.ini', { - booleans: [ - '+asn.enable', - ], - }, function () { - plugin.load_karma_ini() - }) + plugin.cfg = plugin.config.get( + 'karma.ini', + { + booleans: ['+asn.enable'], + }, + function () { + plugin.load_karma_ini() + }, + ) plugin.merge_redis_ini() @@ -73,7 +88,6 @@ exports.load_karma_ini = function () { } exports.results_init = async function (next, connection) { - if (this.should_we_skip(connection)) { connection.logdebug(this, 'skipping') return next() @@ -81,7 +95,7 @@ exports.results_init = async function (next, connection) { if (connection.results.get('karma')) { connection.logerror(this, 'this should never happen') - return next() // init once per connection + return next() // init once per connection } if (this.cfg.awards) { @@ -92,10 +106,9 @@ exports.results_init = async function (next, connection) { const award = this.cfg.awards[key].toString() todo[key] = award } - connection.results.add(this, { score:0, todo }) - } - else { - connection.results.add(this, { score:0 }) + connection.results.add(this, { score: 0, todo }) + } else { + connection.results.add(this, { score: 0 }) } if (!connection.server.notes.redis) { @@ -103,7 +116,7 @@ exports.results_init = async function (next, connection) { return next() } - if (!this.result_awards) return next() // not configured + if (!this.result_awards) return next() // not configured if (connection.notes.redis) { connection.logdebug(this, `redis already subscribed`) @@ -128,9 +141,8 @@ exports.preparse_result_awards = function () { // arrange results for rapid traversal by check_result() : // ex: karma.result_awards.clamd.fail = { .... } for (const anum of Object.keys(cra)) { - - const [pi_name, prop, operator, value, award, reason, resolv] - = cra[anum].split(/(?:\s*\|\s*)/) + const [pi_name, prop, operator, value, award, reason, resolv] = + cra[anum].split(/(?:\s*\|\s*)/) const ra = this.result_awards @@ -143,7 +155,6 @@ exports.preparse_result_awards = function () { } exports.check_result = function (connection, message) { - // connection.loginfo(this, message); // {"plugin":"karma","result":{"fail":"spamassassin.hits"}} // {"plugin":"geoip","result":{"country":"CN"}} @@ -152,13 +163,14 @@ exports.check_result = function (connection, message) { if (m && m.result && m.result.asn) { this.check_result_asn(m.result.asn, connection) } - if (!this.result_awards[m.plugin]) return // no awards for plugin + if (!this.result_awards[m.plugin]) return // no awards for plugin - for (const r of Object.keys(m.result)) { // each result in mess - if (r === 'emit') continue // r: pass, fail, skip, err, ... + for (const r of Object.keys(m.result)) { + // each result in mess + if (r === 'emit') continue // r: pass, fail, skip, err, ... const pi_prop = this.result_awards[m.plugin][r] - if (!pi_prop) continue // no award for this plugin property + if (!pi_prop) continue // no award for this plugin property const thisResult = m.result[r] // ignore empty arrays, objects, and strings @@ -169,7 +181,8 @@ exports.check_result = function (connection, message) { if (typeof thisResult === 'string' && !thisResult) continue // empty // do any award conditions match this result? - for (const thisAward of pi_prop) { // each award... + for (const thisAward of pi_prop) { + // each award... // { id: '011', operator: 'equals', value: 'all_bad', award: '-2'} const thisResArr = this.result_as_array(thisResult) switch (thisAward.operator) { @@ -196,14 +209,13 @@ exports.check_result = function (connection, message) { } exports.result_as_array = function (result) { - if (typeof result === 'string') return [result] if (typeof result === 'number') return [result] if (typeof result === 'boolean') return [result] if (Array.isArray(result)) return result if (typeof result === 'object') { const array = [] - Object.keys(result).forEach(tr => { + Object.keys(result).forEach((tr) => { array.push(result[tr]) }) return array @@ -216,41 +228,37 @@ exports.check_result_asn = function (asn, conn) { if (!this.cfg.asn_awards) return if (!this.cfg.asn_awards[asn]) return - conn.results.incr(this, {score: this.cfg.asn_awards[asn]}) - conn.results.push(this, {fail: 'asn_awards'}) + conn.results.incr(this, { score: this.cfg.asn_awards[asn] }) + conn.results.push(this, { fail: 'asn_awards' }) } exports.check_result_lt = function (thisResult, thisAward, conn) { - for (const element of thisResult) { const tr = parseFloat(element) if (tr >= parseFloat(thisAward.value)) continue if (conn.results.has('karma', 'awards', thisAward.id)) continue - conn.results.incr(this, {score: thisAward.award}) - conn.results.push(this, {awards: thisAward.id}) + conn.results.incr(this, { score: thisAward.award }) + conn.results.push(this, { awards: thisAward.id }) } } exports.check_result_gt = function (thisResult, thisAward, conn) { - for (const element of thisResult) { const tr = parseFloat(element) if (tr <= parseFloat(thisAward.value)) continue if (conn.results.has('karma', 'awards', thisAward.id)) continue - conn.results.incr(this, {score: thisAward.award}) - conn.results.push(this, {awards: thisAward.id}) + conn.results.incr(this, { score: thisAward.award }) + conn.results.push(this, { awards: thisAward.id }) } } exports.check_result_equal = function (thisResult, thisAward, conn) { - for (const element of thisResult) { if (thisAward.value === 'true') { if (!element) continue - } - else { + } else { if (element != thisAward.value) continue } if (!/auth/.test(thisAward.plugin)) { @@ -258,8 +266,8 @@ exports.check_result_equal = function (thisResult, thisAward, conn) { if (conn.results.has('karma', 'awards', thisAward.id)) continue } - conn.results.incr(this, {score: thisAward.award}) - conn.results.push(this, {awards: thisAward.id}) + conn.results.incr(this, { score: thisAward.award }) + conn.results.push(this, { awards: thisAward.id }) } } @@ -270,13 +278,12 @@ exports.check_result_match = function (thisResult, thisAward, conn) { if (!re.test(element)) continue if (conn.results.has('karma', 'awards', thisAward.id)) continue - conn.results.incr(this, {score: thisAward.award}) - conn.results.push(this, {awards: thisAward.id}) + conn.results.incr(this, { score: thisAward.award }) + conn.results.push(this, { awards: thisAward.id }) } } exports.check_result_length = function (thisResult, thisAward, conn) { - for (const element of thisResult) { const [operator, qty] = thisAward.value.split(/\s+/) // requires node 6+ @@ -297,13 +304,12 @@ exports.check_result_length = function (thisResult, thisAward, conn) { continue } - conn.results.incr(this, {score: thisAward.award}) - conn.results.push(this, {awards: thisAward.id }) + conn.results.incr(this, { score: thisAward.award }) + conn.results.push(this, { awards: thisAward.id }) } } exports.check_result_exists = function (thisResult, thisAward, conn) { - /* eslint-disable no-unused-vars */ for (const r of thisResult) { const [operator, qty] = thisAward.value.split(/\s+/) @@ -317,18 +323,17 @@ exports.check_result_exists = function (thisResult, thisAward, conn) { continue } - conn.results.incr(this, {score: thisAward.award}) - conn.results.push(this, {awards: thisAward.id}) + conn.results.incr(this, { score: thisAward.award }) + conn.results.push(this, { awards: thisAward.id }) } } exports.apply_tarpit = function (connection, hook, score, next) { - if (!this.cfg.tarpit) return next() // tarpit disabled in config // If tarpit is enabled on the reset_transaction hook, Haraka doesn't // wait. Then bad things happen, like a Haraka crash. - if (utils.in_array(hook, ['reset_transaction','queue'])) return next() + if (utils.in_array(hook, ['reset_transaction', 'queue'])) return next() // no delay for senders with good karma const k = connection.results.get('karma') @@ -347,17 +352,18 @@ exports.apply_tarpit = function (connection, hook, score, next) { } exports.tarpit_delay = function (score, connection, hook, k) { - if (this.cfg.tarpit.delay && parseFloat(this.cfg.tarpit.delay)) { connection.logdebug(this, 'static tarpit') return parseFloat(this.cfg.tarpit.delay) } - const delay = score * -1 // progressive tarpit + const delay = score * -1 // progressive tarpit // detect roaming users based on MSA ports that require auth - if (utils.in_array(connection.local.port, [587,465]) && - utils.in_array(hook, ['ehlo','connect'])) { + if ( + utils.in_array(connection.local.port, [587, 465]) && + utils.in_array(hook, ['ehlo', 'connect']) + ) { return this.tarpit_delay_msa(connection, delay, k) } @@ -376,7 +382,7 @@ exports.tarpit_delay_msa = function (connection, delay, k) { delay = parseFloat(delay) // Reduce delay for good history - const history = ((k.good || 0) - (k.bad || 0)) + const history = (k.good || 0) - (k.bad || 0) if (history > 0) { delay = delay - 2 connection.logdebug(this, `${trg} history: ${delay}`) @@ -409,12 +415,12 @@ exports.should_we_deny = function (next, connection, hook) { const r = connection.results.get('karma') if (!r) return next() - this.check_awards(connection) // update awards first + this.check_awards(connection) // update awards first const score = parseFloat(r.score) - if (isNaN(score)) { + if (isNaN(score)) { connection.logerror(this, 'score is NaN') - connection.results.add(this, {score: 0}) + connection.results.add(this, { score: 0 }) return next() } @@ -450,10 +456,10 @@ exports.hook_deny = function (next, connection, params) { // let pi_deny = params[0]; // (constants.deny, denysoft, ok) // let pi_message = params[1]; - const pi_name = params[2] + const pi_name = params[2] // let pi_function = params[3]; // let pi_params = params[4]; - const pi_hook = params[5] + const pi_hook = params[5] // exceptions, whose 'DENY' should not be captured if (pi_name) { @@ -468,7 +474,7 @@ exports.hook_deny = function (next, connection, params) { connection.results.add(this, { msg: `deny: ${pi_name}` }) connection.results.incr(this, { score: -2 }) - next(constants.OK) // resume the connection + next(constants.OK) // resume the connection } exports.hook_connect = function (next, connection) { @@ -526,12 +532,11 @@ exports.hook_queue_outbound = function (next, connection) { exports.hook_reset_transaction = function (next, connection) { if (this.should_we_skip(connection)) return next() - connection.results.add(this, {emit: true}) + connection.results.add(this, { emit: true }) this.should_we_deny(next, connection, 'reset_transaction') } exports.hook_unrecognized_command = function (next, connection, params) { - if (this.should_we_skip(connection)) return next() // in case karma is in config/plugins before tls @@ -540,8 +545,8 @@ exports.hook_unrecognized_command = function (next, connection, params) { // in case karma is in config/plugins before AUTH plugin(s) if (connection.notes.authenticating) return next() - connection.results.incr(this, {score: -1}) - connection.results.add(this, {fail: `cmd:(${params})`}) + connection.results.incr(this, { score: -1 }) + connection.results.add(this, { fail: `cmd:(${params})` }) return this.should_we_deny(next, connection, 'unrecognized_command') } @@ -552,72 +557,74 @@ exports.ip_history_from_redis = function (next, connection) { if (this.should_we_skip(connection)) return next() const expire = (this.cfg.redis.expire_days || 60) * 86400 // to days - const dbkey = `karma|${connection.remote.ip}` + const dbkey = `karma|${connection.remote.ip}` // redis plugin is emitting errors, no need to here if (!this.db) return next() - this.db.hGetAll(dbkey).then(dbr => { - if (dbr === null) { - plugin.init_ip(dbkey, connection.remote.ip, expire) - return next() - } + this.db + .hGetAll(dbkey) + .then((dbr) => { + if (dbr === null) { + plugin.init_ip(dbkey, connection.remote.ip, expire) + return next() + } - plugin.db.multi() - .hIncrBy(dbkey, 'connections', 1) // increment total conn - .expire(dbkey, expire) // extend expiration - .exec() - .catch(err => { - connection.results.add(plugin, { err }) - }) - - const results = { - good: dbr.good, - bad: dbr.bad, - connections: dbr.connections, - history: parseInt((dbr.good || 0) - (dbr.bad || 0)), - emit: true, - } + plugin.db + .multi() + .hIncrBy(dbkey, 'connections', 1) // increment total conn + .expire(dbkey, expire) // extend expiration + .exec() + .catch((err) => { + connection.results.add(plugin, { err }) + }) + + const results = { + good: dbr.good, + bad: dbr.bad, + connections: dbr.connections, + history: parseInt((dbr.good || 0) - (dbr.bad || 0)), + emit: true, + } - // Careful: don't become self-fulfilling prophecy. - if (parseInt(dbr.good) > 5 && parseInt(dbr.bad) === 0) { - results.pass = 'all_good' - } - if (parseInt(dbr.bad) > 5 && parseInt(dbr.good) === 0) { - results.fail = 'all_bad' - } + // Careful: don't become self-fulfilling prophecy. + if (parseInt(dbr.good) > 5 && parseInt(dbr.bad) === 0) { + results.pass = 'all_good' + } + if (parseInt(dbr.bad) > 5 && parseInt(dbr.good) === 0) { + results.fail = 'all_bad' + } - connection.results.add(plugin, results) + connection.results.add(plugin, results) - plugin.check_awards(connection) - next() - }) - .catch(err => { + plugin.check_awards(connection) + next() + }) + .catch((err) => { connection.results.add(plugin, { err }) next() }) } exports.hook_mail = function (next, connection, params) { - if (this.should_we_skip(connection)) return next() this.check_spammy_tld(params[0], connection) // look for invalid (RFC 5321,(2)821) space in envelope from const full_from = connection.current_line - if (full_from.toUpperCase().substring(0,11) !== 'MAIL FROM:<') { + if (full_from.toUpperCase().substring(0, 11) !== 'MAIL FROM:<') { connection.loginfo(this, `RFC ignorant env addr format: ${full_from}`) - connection.results.add(this, {fail: 'rfc5321.MailFrom'}) + connection.results.add(this, { fail: 'rfc5321.MailFrom' }) } // apply TLS awards (if defined) if (this.cfg.tls !== undefined) { if (this.cfg.tls.set && connection.tls.enabled) { - connection.results.incr(this, {score: this.cfg.tls.set}) + connection.results.incr(this, { score: this.cfg.tls.set }) } if (this.cfg.tls.unset && !connection.tls.enabled) { - connection.results.incr(this, {score: this.cfg.tls.unset}) + connection.results.incr(this, { score: this.cfg.tls.unset }) } } @@ -625,7 +632,6 @@ exports.hook_mail = function (next, connection, params) { } exports.hook_rcpt = function (next, connection, params) { - if (this.should_we_skip(connection)) return next() const rcpt = params[0] @@ -636,23 +642,22 @@ exports.hook_rcpt = function (next, connection, params) { // odds of from_user=rcpt_user in ham: < 1%, in spam > 40% // 2015-05 30-day sample: 84% spam correlation if (connection?.transaction?.mail_from?.user === rcpt.user) { - connection.results.add(this, {fail: 'env_user_match'}) + connection.results.add(this, { fail: 'env_user_match' }) } this.check_syntax_RcptTo(connection) - connection.results.add(this, {fail: 'rcpt_to'}) + connection.results.add(this, { fail: 'rcpt_to' }) return this.should_we_deny(next, connection, 'rcpt') } exports.hook_rcpt_ok = function (next, connection, rcpt) { - if (this.should_we_skip(connection)) return next() const txn = connection.transaction if (txn && txn.mail_from && txn.mail_from.user === rcpt.user) { - connection.results.add(this, {fail: 'env_user_match'}) + connection.results.add(this, { fail: 'env_user_match' }) } this.check_syntax_RcptTo(connection) @@ -665,7 +670,7 @@ exports.hook_data_post = function (next, connection) { if (this.should_we_skip(connection)) return next() - this.check_awards(connection) // update awards + this.check_awards(connection) // update awards const results = connection.results.collate(this) connection.logdebug(this, `adding header: ${results}`) @@ -691,13 +696,13 @@ exports.hook_disconnect = function (next, connection) { const k = connection.results.get('karma') if (!k || k.score === undefined) { - connection.results.add(this, {err: 'karma results missing'}) + connection.results.add(this, { err: 'karma results missing' }) return next() } if (!this.cfg.thresholds) { this.check_awards(connection) - connection.results.add(this, {msg: 'no action', emit: true }) + connection.results.add(this, { msg: 'no action', emit: true }) return next() } @@ -708,12 +713,11 @@ exports.hook_disconnect = function (next, connection) { this.increment(connection, 'bad', 1) } - connection.results.add(this, {emit: true }) + connection.results.add(this, { emit: true }) next() } exports.get_award_loc_from_note = function (connection, award) { - if (connection.transaction) { const obj = this.assemble_note_obj(connection.transaction, award) if (obj) return obj @@ -728,7 +732,6 @@ exports.get_award_loc_from_note = function (connection, award) { } exports.get_award_loc_from_results = function (connection, loc_bits) { - let pi_name = loc_bits[1] let notekey = loc_bits[2] @@ -755,16 +758,22 @@ exports.get_award_location = function (connection, award_key) { const loc_bits = bits[0].split('.') if (loc_bits.length === 1) return connection[bits[0]] // ex: relaying - if (loc_bits[0] === 'notes') { // ex: notes.spf_mail_helo + if (loc_bits[0] === 'notes') { + // ex: notes.spf_mail_helo return this.get_award_loc_from_note(connection, bits[0]) } - if (loc_bits[0] === 'results') { // ex: results.geoip.distance + if (loc_bits[0] === 'results') { + // ex: results.geoip.distance return this.get_award_loc_from_results(connection, loc_bits) } // ex: transaction.results.spf - if (connection.transaction && loc_bits[0] === 'transaction' && loc_bits[1] === 'results') { + if ( + connection.transaction && + loc_bits[0] === 'transaction' && + loc_bits[1] === 'results' + ) { loc_bits.shift() return this.get_award_loc_from_results(connection.transaction, loc_bits) } @@ -775,11 +784,13 @@ exports.get_award_location = function (connection, award_key) { exports.get_award_condition = function (note_key, note_val) { let wants const keybits = note_key.split('@') - if (keybits[1]) { wants = keybits[1] } + if (keybits[1]) { + wants = keybits[1] + } const valbits = note_val.split(/\s+/) if (!valbits[1]) return wants - if (valbits[1] !== 'if') return wants // no if condition + if (valbits[1] !== 'if') return wants // no if condition if (valbits[2].match(/^(equals|gt|lt|match)$/)) { if (valbits[3]) wants = valbits[3] @@ -805,14 +816,16 @@ exports.check_awards = function (connection) { // test the desired condition const bits = award_terms.split(/\s+/) const award = parseFloat(bits[0]) - if (!bits[1] || bits[1] !== 'if') { // no if conditions - if (!note) continue // failed truth test - if (!wants) { // no wants, truth matches + if (!bits[1] || bits[1] !== 'if') { + // no if conditions + if (!note) continue // failed truth test + if (!wants) { + // no wants, truth matches this.apply_award(connection, key, award) delete karma.todo[key] continue } - if (note !== wants) continue // didn't match + if (note !== wants) continue // didn't match } // connection.loginfo(this, `check_awards, case matching for: ${wants}` @@ -839,7 +852,9 @@ exports.check_awards = function (connection) { continue case 'length': { const operator = bits[3] - if (bits[4]) { wants = bits[4] } + if (bits[4]) { + wants = bits[4] + } switch (operator) { case 'gt': if (note.length <= parseFloat(wants)) continue @@ -851,14 +866,19 @@ exports.check_awards = function (connection) { if (note.length !== parseFloat(wants)) continue break default: - connection.logerror(this, `length operator "${operator}" not supported.`) + connection.logerror( + this, + `length operator "${operator}" not supported.`, + ) continue } break } - case 'in': // if in pass whitelisted + case 'in': // if in pass whitelisted // let list = bits[3]; - if (bits[4]) { wants = bits[4] } + if (bits[4]) { + wants = bits[4] + } if (!Array.isArray(note)) continue if (!wants) continue if (note.indexOf(wants) !== -1) break // found! @@ -873,25 +893,31 @@ exports.check_awards = function (connection) { exports.apply_award = function (connection, nl, award) { if (!award) return - if (isNaN(award)) { // garbage in config + if (isNaN(award)) { + // garbage in config connection.logerror(this, `non-numeric award from: ${nl}:${award}`) return } - const bits = nl.split('@'); nl = bits[0] // strip off @... if present + const bits = nl.split('@') + nl = bits[0] // strip off @... if present - connection.results.incr(this, {score: award}) + connection.results.incr(this, { score: award }) connection.logdebug(this, `applied ${nl}:${award}`) - let trimmed = nl.substring(0, 5) === 'notes' ? nl.substring(6) : - nl.substring(0, 7) === 'results' ? nl.substring(8) : - nl.substring(0,19) === 'transaction.results' ? - nl.substring(20) : nl + let trimmed = + nl.substring(0, 5) === 'notes' + ? nl.substring(6) + : nl.substring(0, 7) === 'results' + ? nl.substring(8) + : nl.substring(0, 19) === 'transaction.results' + ? nl.substring(20) + : nl - if (trimmed.substring(0,7) === 'rcpt_to') trimmed = trimmed.substring(8) - if (trimmed.substring(0,7) === 'mail_from') trimmed = trimmed.substring(10) - if (trimmed.substring(0,7) === 'connect') trimmed = trimmed.substring(8) - if (trimmed.substring(0,4) === 'data') trimmed = trimmed.substring(5) + if (trimmed.substring(0, 7) === 'rcpt_to') trimmed = trimmed.substring(8) + if (trimmed.substring(0, 7) === 'mail_from') trimmed = trimmed.substring(10) + if (trimmed.substring(0, 7) === 'connect') trimmed = trimmed.substring(8) + if (trimmed.substring(0, 4) === 'data') trimmed = trimmed.substring(5) if (award > 0) connection.results.add(this, { pass: trimmed }) if (award < 0) connection.results.add(this, { fail: trimmed }) @@ -899,7 +925,7 @@ exports.apply_award = function (connection, nl, award) { exports.check_spammy_tld = function (mail_from, connection) { if (!this.cfg.spammy_tlds) return - if (mail_from.isNull()) return // null sender (bounce) + if (mail_from.isNull()) return // null sender (bounce) const from_tld = mail_from.host.split('.').pop() // connection.logdebug(this, `from_tld: ${from_tld}`); @@ -907,17 +933,17 @@ exports.check_spammy_tld = function (mail_from, connection) { const tld_penalty = parseFloat(this.cfg.spammy_tlds[from_tld] || 0) if (tld_penalty === 0) return - connection.results.incr(this, {score: tld_penalty}) - connection.results.add(this, {fail: 'spammy.TLD'}) + connection.results.incr(this, { score: tld_penalty }) + connection.results.add(this, { fail: 'spammy.TLD' }) } exports.check_syntax_RcptTo = function (connection) { // look for an illegal (RFC 5321,(2)821) space in envelope recipient const full_rcpt = connection.current_line - if (full_rcpt.toUpperCase().substring(0,9) === 'RCPT TO:<') return + if (full_rcpt.toUpperCase().substring(0, 9) === 'RCPT TO:<') return connection.loginfo(this, `illegal envelope address format: ${full_rcpt}`) - connection.results.add(this, {fail: 'rfc5321.RcptTo'}) + connection.results.add(this, { fail: 'rfc5321.RcptTo' }) } exports.assemble_note_obj = function (prefix, key) { @@ -941,50 +967,52 @@ exports.check_asn = function (connection, asnkey) { if (this.cfg.asn.report_as) report_as.name = this.cfg.asn.report_as - this.db.hGetAll(asnkey).then(res => { - if (res === null) { - const expire = (this.cfg.redis.expire_days || 60) * 86400 // days - this.init_asn(asnkey, expire) - return - } - - this.db.hIncrBy(asnkey, 'connections', 1) - const asn_score = parseInt(res.good || 0) - (res.bad || 0) - const asn_results = { - asn_score, - asn_connections: res.connections, - asn_good: res.good, - asn_bad: res.bad, - emit: true, - } + this.db + .hGetAll(asnkey) + .then((res) => { + if (res === null) { + const expire = (this.cfg.redis.expire_days || 60) * 86400 // days + this.init_asn(asnkey, expire) + return + } - if (asn_score) { - if (asn_score < -5) { - asn_results.fail = 'asn:history' + this.db.hIncrBy(asnkey, 'connections', 1) + const asn_score = parseInt(res.good || 0) - (res.bad || 0) + const asn_results = { + asn_score, + asn_connections: res.connections, + asn_good: res.good, + asn_bad: res.bad, + emit: true, } - else if (asn_score > 5) { - asn_results.pass = 'asn:history' + + if (asn_score) { + if (asn_score < -5) { + asn_results.fail = 'asn:history' + } else if (asn_score > 5) { + asn_results.pass = 'asn:history' + } } - } - if (parseInt(res.bad) > 5 && parseInt(res.good) === 0) { - asn_results.fail = 'asn:all_bad' - } - if (parseInt(res.good) > 5 && parseInt(res.bad) === 0) { - asn_results.pass = 'asn:all_good' - } + if (parseInt(res.bad) > 5 && parseInt(res.good) === 0) { + asn_results.fail = 'asn:all_bad' + } + if (parseInt(res.good) > 5 && parseInt(res.bad) === 0) { + asn_results.pass = 'asn:all_good' + } - connection.results.add(report_as, asn_results) - }) - .catch(err => { + connection.results.add(report_as, asn_results) + }) + .catch((err) => { connection.results.add(this, { err }) }) } exports.init_ip = async function (dbkey, rip, expire) { if (!this.db) return - await this.db.multi() - .hmSet(dbkey, {'bad': 0, 'good': 0, 'connections': 1}) + await this.db + .multi() + .hmSet(dbkey, { bad: 0, good: 0, connections: 1 }) .expire(dbkey, expire) .exec() } @@ -999,8 +1027,9 @@ exports.get_asn_key = function (connection) { exports.init_asn = function (asnkey, expire) { if (!this.db) return - this.db.multi() - .hmSet(asnkey, {'bad': 0, 'good': 0, 'connections': 1}) - .expire(asnkey, expire * 2) // keep ASN longer + this.db + .multi() + .hmSet(asnkey, { bad: 0, good: 0, connections: 1 }) + .expire(asnkey, expire * 2) // keep ASN longer .exec() } diff --git a/package.json b/package.json index 9987134..bef687b 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,22 @@ { "name": "haraka-plugin-karma", - "version": "2.1.3", + "version": "2.1.4", "description": "A heuristics scoring and reputation engine for SMTP connections", "main": "index.js", + "files": [ + "CHANGELOG.md", + "config", + "test" + ], "scripts": { - "test": "npx mocha", - "lint": "npx eslint index.js test", - "lintfix": "npx eslint --fix index.js test", - "versions": "npx dependency-version-checker check" + "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": "npx mocha@10", + "versions": "npx @msimerson/dependency-version-checker check", + "versions:fix": "npx @msimerson/dependency-version-checker update" }, "repository": { "type": "git", @@ -23,16 +32,14 @@ }, "homepage": "https://github.com/haraka/haraka-plugin-karma#readme", "dependencies": { - "address-rfc2821": "^2.1.1", - "haraka-constants": "^1.0.2", - "haraka-utils": "^1.0.3", + "address-rfc2821": "^2.1.2", + "haraka-constants": "^1.0.6", + "haraka-utils": "^1.1.1", "haraka-plugin-redis": "^2.0.6", - "redis": "^4.6.11" + "redis": "^4.6.13" }, "devDependencies": { - "eslint": "^8.55.0", - "eslint-plugin-haraka": "*", - "haraka-test-fixtures": "*", - "mocha": "^10.2.0" + "@haraka/eslint-config": "^1.1.2", + "haraka-test-fixtures": "^1.3.4" } } diff --git a/test/karma.js b/test/karma.js index ad81c0e..00e93dd 100644 --- a/test/karma.js +++ b/test/karma.js @@ -1,19 +1,18 @@ 'use strict' -const assert = require('assert') +const assert = require('assert') -const Address = require('address-rfc2821').Address -const fixtures = require('haraka-test-fixtures') -const constants = require('haraka-constants') +const Address = require('address-rfc2821').Address +const fixtures = require('haraka-test-fixtures') +const constants = require('haraka-constants') -const stub = fixtures.stub.stub - -function _set_up (done) { +const stub = fixtures.stub.stub +function _set_up(done) { this.plugin = new fixtures.plugin('karma') this.plugin.cfg = { main: {} } - this.plugin.deny_hooks = {'connect': true} + this.plugin.deny_hooks = { connect: true } this.plugin.tarpit_hooks = ['connect'] this.connection = fixtures.connection.createConnection({}, { notes: {} }) @@ -22,7 +21,6 @@ function _set_up (done) { done() } - describe('karma_init', function () { beforeEach(function (done) { this.plugin = new fixtures.plugin('karma') @@ -84,15 +82,21 @@ describe('assemble_note_obj', function () { beforeEach(_set_up) it('no auth fails', function (done) { - const obj = this.plugin.assemble_note_obj(this.connection, 'notes.auth_fails') + const obj = this.plugin.assemble_note_obj( + this.connection, + 'notes.auth_fails', + ) assert.equal(undefined, obj) done() }) it('has auth fails', function (done) { - this.connection.notes.auth_fails=[1,2] - const obj = this.plugin.assemble_note_obj(this.connection, 'notes.auth_fails') - assert.deepEqual([1,2], obj) + this.connection.notes.auth_fails = [1, 2] + const obj = this.plugin.assemble_note_obj( + this.connection, + 'notes.auth_fails', + ) + assert.deepEqual([1, 2], obj) done() }) }) @@ -105,7 +109,7 @@ describe('hook_deny', function () { assert.equal(constants.OK, rc, rc) done() } - this.plugin.hook_deny(next, this.connection, ['','','','']) + this.plugin.hook_deny(next, this.connection, ['', '', '', '']) }) it('pi_name=karma', function (done) { @@ -113,7 +117,7 @@ describe('hook_deny', function () { assert.equal(undefined, rc) done() } - this.plugin.hook_deny(next, this.connection, ['','','karma','']) + this.plugin.hook_deny(next, this.connection, ['', '', 'karma', '']) }) it('pi_name=access', function (done) { @@ -122,7 +126,7 @@ describe('hook_deny', function () { done() } this.plugin.deny_exclude_plugins = { access: true } - this.plugin.hook_deny(next, this.connection, ['','','access','']) + this.plugin.hook_deny(next, this.connection, ['', '', 'access', '']) }) it('pi_hook=rcpt_to', function (done) { @@ -131,8 +135,14 @@ describe('hook_deny', function () { done() } this.plugin.deny_exclude_hooks = { rcpt_to: true } - this.plugin.hook_deny(next, this.connection, - ['','','','','','rcpt_to']) + this.plugin.hook_deny(next, this.connection, [ + '', + '', + '', + '', + '', + 'rcpt_to', + ]) }) it('pi_hook=queue', function (done) { @@ -141,7 +151,7 @@ describe('hook_deny', function () { done() } this.plugin.deny_exclude_hooks = { queue: true } - this.plugin.hook_deny(next, this.connection, ['','','','','','queue']) + this.plugin.hook_deny(next, this.connection, ['', '', '', '', '', 'queue']) }) it('denysoft', function (done) { @@ -149,7 +159,14 @@ describe('hook_deny', function () { assert.equal(constants.OK, rc) done() } - this.plugin.hook_deny(next, this.connection, [constants.DENYSOFT,'','','','','']) + this.plugin.hook_deny(next, this.connection, [ + constants.DENYSOFT, + '', + '', + '', + '', + '', + ]) }) }) @@ -157,14 +174,14 @@ describe('get_award_location', function () { beforeEach(_set_up) it('relaying=false', function (done) { - this.connection.relaying=false + this.connection.relaying = false const r = this.plugin.get_award_location(this.connection, 'relaying') assert.equal(false, r) done() }) it('relaying=true', function (done) { - this.connection.relaying=true + this.connection.relaying = true const r = this.plugin.get_award_location(this.connection, 'relaying') assert.equal(true, r) done() @@ -211,7 +228,10 @@ describe('get_award_location', function () { it('txn.results.karma', function (done) { // these results shouldn't be found, b/c txn specified this.connection.results.add('karma', { score: -1 }) - const r = this.plugin.get_award_location(this.connection, 'transaction.results.karma') + const r = this.plugin.get_award_location( + this.connection, + 'transaction.results.karma', + ) // console.log(r); assert.equal(undefined, r) done() @@ -219,7 +239,10 @@ describe('get_award_location', function () { it('results.auth/auth_base', function (done) { this.connection.results.add('auth/auth_base', { fail: 'PLAIN' }) - const r = this.plugin.get_award_location(this.connection, 'results.auth/auth_base') + const r = this.plugin.get_award_location( + this.connection, + 'results.auth/auth_base', + ) assert.equal('PLAIN', r.fail[0]) done() }) @@ -228,19 +251,31 @@ describe('get_award_location', function () { describe('get_award_condition', function () { beforeEach(_set_up) it('geoip.distance', function (done) { - assert.equal(4000, this.plugin.get_award_condition( - 'results.geoip.distance@4000', '-1 if gt' - )) - assert.equal(4000, this.plugin.get_award_condition( - 'results.geoip.distance@uniq', '-1 if gt 4000' - )) + assert.equal( + 4000, + this.plugin.get_award_condition( + 'results.geoip.distance@4000', + '-1 if gt', + ), + ) + assert.equal( + 4000, + this.plugin.get_award_condition( + 'results.geoip.distance@uniq', + '-1 if gt 4000', + ), + ) done() }) it('auth/auth_base', function (done) { - assert.equal('plain', this.plugin.get_award_condition( - 'results.auth/auth_base.fail@plain', '-1 if in' - )) + assert.equal( + 'plain', + this.plugin.get_award_condition( + 'results.auth/auth_base.fail@plain', + '-1 if in', + ), + ) done() }) }) @@ -255,17 +290,16 @@ describe('check_awards', function () { }) it('no todo', function (done) { - this.connection.results.add('karma', { todo: { } }) + this.connection.results.add('karma', { todo: {} }) const r = this.plugin.check_awards(this.connection) assert.equal(undefined, r) done() }) it('geoip gt', function (done) { - // populate the karma result with a todo item this.connection.results.add('karma', { - todo: { 'results.geoip.distance@4000': '-1 if gt 4000' } + todo: { 'results.geoip.distance@4000': '-1 if gt 4000' }, }) // test a non-matching criteria this.connection.results.add('geoip', { distance: 4000 }) @@ -285,21 +319,23 @@ describe('check_awards', function () { it('auth failure', function (done) { this.connection.results.add('karma', { - todo: { 'results.auth/auth_base.fail@PLAIN': '-1 if in' } + todo: { 'results.auth/auth_base.fail@PLAIN': '-1 if in' }, }) - this.connection.results.add('auth/auth_base', - {fail: 'PLAIN'}) + this.connection.results.add('auth/auth_base', { fail: 'PLAIN' }) const r = this.plugin.check_awards(this.connection) assert.equal(undefined, r) - assert.equal('auth/auth_base.fail', this.connection.results.get('karma').fail[0]) + assert.equal( + 'auth/auth_base.fail', + this.connection.results.get('karma').fail[0], + ) done() }) it('valid recipient', function (done) { this.connection.results.add('karma', { - todo: { 'results.rcpt_to.qmd.pass@exist': '1 if in' } + todo: { 'results.rcpt_to.qmd.pass@exist': '1 if in' }, }) - this.connection.results.add('rcpt_to.qmd', {pass: 'exist'}) + this.connection.results.add('rcpt_to.qmd', { pass: 'exist' }) const r = this.plugin.check_awards(this.connection) assert.equal(undefined, r) assert.equal('qmd.pass', this.connection.results.get('karma').pass[0]) @@ -427,7 +463,7 @@ describe('should_we_deny', function () { done() }.bind(this) this.plugin.cfg.tarpit = { max: 1, delay: 0 } - this.plugin.deny_hooks = { connect: true} + this.plugin.deny_hooks = { connect: true } this.connection.results.add(this.plugin, { score: -6 }) this.plugin.should_we_deny(next, this.connection, 'connect') }) @@ -450,9 +486,12 @@ describe('check_result_equal', function () { it('equal match is scored', function (done) { const award = { - id : 1, award : 2, - operator : 'equals', value : 'clean', - reason : 'testing', resolution : 'never', + id: 1, + award: 2, + operator: 'equals', + value: 'clean', + reason: 'testing', + resolution: 'never', } this.plugin.check_result_equal(['clean'], award, this.connection) assert.equal(this.connection.results.store.karma.score, 2) @@ -462,9 +501,12 @@ describe('check_result_equal', function () { it('not equal match is not scored', function (done) { const award = { - id : 1, award : 2, - operator : 'equals', value : 'dirty', - reason : 'testing', resolution : 'never', + id: 1, + award: 2, + operator: 'equals', + value: 'dirty', + reason: 'testing', + resolution: 'never', } this.plugin.check_result_equal(['clean'], award, this.connection) assert.equal(this.connection.results.store.karma, undefined) @@ -477,9 +519,12 @@ describe('check_result_gt', function () { it('gt match is scored', function (done) { const award = { - id : 5, award : 3, - operator : 'gt', value : 3, - reason : 'testing', resolution : 'never', + id: 5, + award: 3, + operator: 'gt', + value: 3, + reason: 'testing', + resolution: 'never', } this.plugin.check_result_gt([4], award, this.connection) // console.log(this.connection.results.store); @@ -494,9 +539,12 @@ describe('check_result_lt', function () { it('lt match is scored', function (done) { const award = { - id : 2, award : 3, - operator : 'lt', value : 5, - reason : 'testing', resolution : 'never', + id: 2, + award: 3, + operator: 'lt', + value: 5, + reason: 'testing', + resolution: 'never', } this.plugin.check_result_lt([4], award, this.connection) // console.log(this.connection.results.store); @@ -507,9 +555,12 @@ describe('check_result_lt', function () { it('lt match not scored', function (done) { const award = { - id : 3, award : 3, - operator : 'lt', value : 3, - reason : 'testing', resolution : 'never', + id: 3, + award: 3, + operator: 'lt', + value: 3, + reason: 'testing', + resolution: 'never', } this.plugin.check_result_lt([4], award, this.connection) // console.log(this.connection.results.store); @@ -523,9 +574,12 @@ describe('check_result_match', function () { it('match pattern is scored', function (done) { const award = { - id : 1, award : 2, - operator : 'match', value : 'phish', - reason : 'testing', resolution : 'never', + id: 1, + award: 2, + operator: 'match', + value: 'phish', + reason: 'testing', + resolution: 'never', } this.plugin.check_result_match(['isphishing'], award, this.connection) // console.log(this.connection.results.store); @@ -536,9 +590,12 @@ describe('check_result_match', function () { it('mismatch is not scored', function (done) { const award = { - id : 1, award : 2, - operator : 'match', value : 'dirty', - reason : 'testing', resolution : 'never', + id: 1, + award: 2, + operator: 'match', + value: 'dirty', + reason: 'testing', + resolution: 'never', } this.plugin.check_result_match(['clean'], award, this.connection) // console.log(this.connection.results.store); @@ -548,11 +605,18 @@ describe('check_result_match', function () { it('FCrDNS match is scored', function (done) { const award = { - id : 89, award : 2, - operator : 'match', value : 'google.com', - reason : 'testing', resolution : 'never', - } - this.plugin.check_result_match(['mail-yk0-f182.google.com'], award, this.connection) + id: 89, + award: 2, + operator: 'match', + value: 'google.com', + reason: 'testing', + resolution: 'never', + } + this.plugin.check_result_match( + ['mail-yk0-f182.google.com'], + award, + this.connection, + ) // console.log(this.connection.results.store); assert.equal(this.connection.results.store.karma.score, 2) assert.equal(this.connection.results.store.karma.awards[0], 89) @@ -564,9 +628,12 @@ describe('check_result_length', function () { beforeEach(_set_up) it('eq pattern is scored', function (done) { const award = { - id : 1, award : 2, - operator : 'length', value : 'eq 3', - reason : 'testing', resolution : 'hah', + id: 1, + award: 2, + operator: 'length', + value: 'eq 3', + reason: 'testing', + resolution: 'hah', } this.plugin.check_result_length(['3'], award, this.connection) // console.log(this.connection.results.store); @@ -577,9 +644,12 @@ describe('check_result_length', function () { it('eq pattern is not scored', function (done) { const award = { - id : 1, award : 2, - operator : 'length', value : 'eq 3', - reason : 'testing', resolution : 'hah', + id: 1, + award: 2, + operator: 'length', + value: 'eq 3', + reason: 'testing', + resolution: 'hah', } this.plugin.check_result_length(['4'], award, this.connection) // console.log(this.connection.results.store.karma); @@ -589,9 +659,12 @@ describe('check_result_length', function () { it('gt pattern is scored', function (done) { const award = { - id : 1, award : 2, - operator : 'length', value : 'gt 3', - reason : 'testing', resolution : 'hah', + id: 1, + award: 2, + operator: 'length', + value: 'gt 3', + reason: 'testing', + resolution: 'hah', } this.plugin.check_result_length(['5'], award, this.connection) // console.log(this.connection.results.store); @@ -602,9 +675,12 @@ describe('check_result_length', function () { it('gt pattern is not scored', function (done) { const award = { - id : 1, award : 2, - operator : 'length', value : 'gt 3', - reason : 'testing', resolution : 'hah', + id: 1, + award: 2, + operator: 'length', + value: 'gt 3', + reason: 'testing', + resolution: 'hah', } this.plugin.check_result_length(['3'], award, this.connection) // console.log(this.connection.results.store.karma); @@ -614,9 +690,12 @@ describe('check_result_length', function () { it('lt pattern is scored', function (done) { const award = { - id : 1, award : 2, - operator : 'length', value : 'lt 3', - reason : 'testing', resolution : 'hah', + id: 1, + award: 2, + operator: 'length', + value: 'lt 3', + reason: 'testing', + resolution: 'hah', } this.plugin.check_result_length(['2'], award, this.connection) // console.log(this.connection.results.store); @@ -627,9 +706,12 @@ describe('check_result_length', function () { it('lt pattern is not scored', function (done) { const award = { - id : 1, award : 2, - operator : 'length', value : 'lt 3', - reason : 'testing', resolution : 'hah', + id: 1, + award: 2, + operator: 'length', + value: 'lt 3', + reason: 'testing', + resolution: 'hah', } this.plugin.check_result_length(['3'], award, this.connection) // console.log(this.connection.results.store.karma); @@ -643,9 +725,12 @@ describe('check_result_exists', function () { it('exists pattern is scored', function (done) { const award = { - id : 1, award : 2, - operator : 'exists', value : 'any', - reason : 'testing', resolution : 'high five', + id: 1, + award: 2, + operator: 'exists', + value: 'any', + reason: 'testing', + resolution: 'high five', } this.plugin.check_result_exists(['3'], award, this.connection) // console.log(this.connection.results.store); @@ -656,9 +741,12 @@ describe('check_result_exists', function () { it('not exists pattern is not scored', function (done) { const award = { - id : 1, award : 3, - operator : 'exists', value : '', - reason : 'testing', resolution : 'misses', + id: 1, + award: 3, + operator: 'exists', + value: '', + reason: 'testing', + resolution: 'misses', } this.plugin.check_result_exists([], award, this.connection) // console.log(this.connection.results.store); @@ -676,9 +764,11 @@ describe('check_result', function () { 1: 'geoip | country | equals | CN | 2', } this.plugin.preparse_result_awards() - this.connection.results.add({name: 'geoip'}, {country: 'CN'}) - this.plugin.check_result(this.connection, - '{"plugin":"geoip","result":{"country":"CN"}}') + this.connection.results.add({ name: 'geoip' }, { country: 'CN' }) + this.plugin.check_result( + this.connection, + '{"plugin":"geoip","result":{"country":"CN"}}', + ) // console.log(this.connection.results.store); assert.equal(this.connection.results.store.karma.score, 2) assert.equal(this.connection.results.store.karma.awards[0], 1) @@ -690,9 +780,11 @@ describe('check_result', function () { 2: 'dnsbl | fail | equals | dnsbl.sorbs.net | -5', } this.plugin.preparse_result_awards() - this.connection.results.add({name: 'dnsbl'}, {fail: 'dnsbl.sorbs.net'}) - this.plugin.check_result(this.connection, - '{"plugin":"dnsbl","result":{"fail":"dnsbl.sorbs.net"}}') + this.connection.results.add({ name: 'dnsbl' }, { fail: 'dnsbl.sorbs.net' }) + this.plugin.check_result( + this.connection, + '{"plugin":"dnsbl","result":{"fail":"dnsbl.sorbs.net"}}', + ) // console.log(this.connection.results.store); assert.equal(this.connection.results.store.karma.score, -5) assert.equal(this.connection.results.store.karma.awards[0], 2) @@ -728,37 +820,49 @@ describe('tls', function () { beforeEach(_set_up) it('unconfigured TLS does nothing', function (done) { - this.connection.tls.enabled=true + this.connection.tls.enabled = true const mfrom = new Address('spamy@er7diogt.rrnsale.top') - this.connection.current_line="MAIL FROM:" - this.plugin.hook_mail(() => { - assert.equal(this.connection.results.store.karma, undefined) - done() - }, this.connection, [mfrom]) + this.connection.current_line = 'MAIL FROM:' + this.plugin.hook_mail( + () => { + assert.equal(this.connection.results.store.karma, undefined) + done() + }, + this.connection, + [mfrom], + ) }) it('TLS is scored', function (done) { this.plugin.cfg.tls = { set: 2, unset: -4 } - this.connection.tls.enabled=true + this.connection.tls.enabled = true const mfrom = new Address('spamy@er7diogt.rrnsale.top') - this.connection.current_line="MAIL FROM:" - this.plugin.hook_mail(() => { - // console.log(this.connection.results.store); - assert.equal(this.connection.results.store.karma.score, 2) - done() - }, this.connection, [mfrom]) + this.connection.current_line = 'MAIL FROM:' + this.plugin.hook_mail( + () => { + // console.log(this.connection.results.store); + assert.equal(this.connection.results.store.karma.score, 2) + done() + }, + this.connection, + [mfrom], + ) }) it('no TLS is scored', function (done) { this.plugin.cfg.tls = { set: 2, unset: -4 } - this.connection.tls.enabled=false + this.connection.tls.enabled = false const mfrom = new Address('spamy@er7diogt.rrnsale.top') - this.connection.current_line="MAIL FROM:" - this.plugin.hook_mail(() => { - // console.log(this.connection.results.store); - assert.equal(this.connection.results.store.karma.score, -4) - done() - }, this.connection, [mfrom]) + this.connection.current_line = 'MAIL FROM:' + this.plugin.hook_mail( + () => { + // console.log(this.connection.results.store); + assert.equal(this.connection.results.store.karma.score, -4) + done() + }, + this.connection, + [mfrom], + ) }) }) @@ -766,10 +870,10 @@ describe('skipping hooks', function () { beforeEach(_set_up) it('notes.disable_karma', function (done) { - function next (rc) { + function next(rc) { assert.equal(undefined, rc) } - function last (rc) { + function last(rc) { assert.equal(undefined, rc) done() } @@ -787,10 +891,10 @@ describe('skipping hooks', function () { }) it('private skip', function (done) { - function next (rc) { + function next(rc) { assert.equal(undefined, rc) } - function last (rc) { + function last(rc) { assert.equal(undefined, rc) done() }