Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

haraka-plugin-karma

Package Overview
Dependencies
Maintainers
3
Versions
26
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

haraka-plugin-karma - npm Package Compare versions

Comparing version 2.1.3 to 2.1.4

CHANGELOG.md

403

index.js

@@ -5,21 +5,34 @@ 'use strict'

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',
])

@@ -29,4 +42,4 @@

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')

@@ -40,9 +53,11 @@ this.register_hook('connect_init', 'results_init')

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()
},
)

@@ -79,3 +94,2 @@ plugin.merge_redis_ini()

exports.results_init = async function (next, connection) {
if (this.should_we_skip(connection)) {

@@ -88,3 +102,3 @@ connection.logdebug(this, 'skipping')

connection.logerror(this, 'this should never happen')
return next() // init once per connection
return next() // init once per connection
}

@@ -100,7 +114,6 @@

}
connection.results.add(this, { score:0, todo })
connection.results.add(this, { score: 0, todo })
} else {
connection.results.add(this, { score: 0 })
}
else {
connection.results.add(this, { score:0 })
}

@@ -112,3 +125,3 @@ if (!connection.server.notes.redis) {

if (!this.result_awards) return next() // not configured
if (!this.result_awards) return next() // not configured

@@ -138,6 +151,5 @@ if (connection.notes.redis) {

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

@@ -154,3 +166,2 @@

exports.check_result = function (connection, message) {
// connection.loginfo(this, message);

@@ -164,9 +175,10 @@ // {"plugin":"karma","result":{"fail":"spamassassin.hits"}}

}
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

@@ -182,3 +194,4 @@ const thisResult = m.result[r]

// 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'}

@@ -210,3 +223,2 @@ const thisResArr = this.result_as_array(thisResult)

exports.result_as_array = function (result) {
if (typeof result === 'string') return [result]

@@ -218,3 +230,3 @@ if (typeof result === 'number') return [result]

const array = []
Object.keys(result).forEach(tr => {
Object.keys(result).forEach((tr) => {
array.push(result[tr])

@@ -232,8 +244,7 @@ })

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) {

@@ -244,4 +255,4 @@ const tr = parseFloat(element)

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 })
}

@@ -251,3 +262,2 @@ }

exports.check_result_gt = function (thisResult, thisAward, conn) {
for (const element of thisResult) {

@@ -258,4 +268,4 @@ const tr = parseFloat(element)

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 })
}

@@ -265,8 +275,6 @@ }

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

@@ -279,4 +287,4 @@ }

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 })
}

@@ -292,4 +300,4 @@ }

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 })
}

@@ -299,3 +307,2 @@ }

exports.check_result_length = function (thisResult, thisAward, conn) {
for (const element of thisResult) {

@@ -321,4 +328,4 @@ const [operator, qty] = thisAward.value.split(/\s+/) // requires node 6+

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 })
}

@@ -328,3 +335,2 @@ }

exports.check_result_exists = function (thisResult, thisAward, conn) {
/* eslint-disable no-unused-vars */

@@ -343,4 +349,4 @@ for (const r of thisResult) {

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 })
}

@@ -350,3 +356,2 @@ }

exports.apply_tarpit = function (connection, hook, score, next) {
if (!this.cfg.tarpit) return next() // tarpit disabled in config

@@ -356,3 +361,3 @@

// 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()

@@ -376,3 +381,2 @@ // no delay for senders with good karma

exports.tarpit_delay = function (score, connection, hook, k) {
if (this.cfg.tarpit.delay && parseFloat(this.cfg.tarpit.delay)) {

@@ -383,7 +387,9 @@ connection.logdebug(this, 'static tarpit')

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)

@@ -407,3 +413,3 @@ }

// Reduce delay for good history
const history = ((k.good || 0) - (k.bad || 0))
const history = (k.good || 0) - (k.bad || 0)
if (history > 0) {

@@ -441,8 +447,8 @@ delay = delay - 2

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()

@@ -483,6 +489,6 @@ }

// 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]

@@ -502,3 +508,3 @@ // exceptions, whose 'DENY' should not be captured

next(constants.OK) // resume the connection
next(constants.OK) // resume the connection
}

@@ -561,3 +567,3 @@

connection.results.add(this, {emit: true})
connection.results.add(this, { emit: true })
this.should_we_deny(next, connection, 'reset_transaction')

@@ -567,3 +573,2 @@ }

exports.hook_unrecognized_command = function (next, connection, params) {
if (this.should_we_skip(connection)) return next()

@@ -577,4 +582,4 @@

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})` })

@@ -590,3 +595,3 @@ return this.should_we_deny(next, connection, 'unrecognized_command')

const expire = (this.cfg.redis.expire_days || 60) * 86400 // to days
const dbkey = `karma|${connection.remote.ip}`
const dbkey = `karma|${connection.remote.ip}`

@@ -596,38 +601,41 @@ // redis plugin is emitting errors, no need to here

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 })
})
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,
}
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 })

@@ -639,3 +647,2 @@ next()

exports.hook_mail = function (next, connection, params) {
if (this.should_we_skip(connection)) return next()

@@ -647,5 +654,5 @@

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' })
}

@@ -656,6 +663,6 @@

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 })
}

@@ -668,3 +675,2 @@ }

exports.hook_rcpt = function (next, connection, params) {
if (this.should_we_skip(connection)) return next()

@@ -680,3 +686,3 @@

if (connection?.transaction?.mail_from?.user === rcpt.user) {
connection.results.add(this, {fail: 'env_user_match'})
connection.results.add(this, { fail: 'env_user_match' })
}

@@ -686,3 +692,3 @@

connection.results.add(this, {fail: 'rcpt_to'})
connection.results.add(this, { fail: 'rcpt_to' })

@@ -693,3 +699,2 @@ return this.should_we_deny(next, connection, 'rcpt')

exports.hook_rcpt_ok = function (next, connection, rcpt) {
if (this.should_we_skip(connection)) return next()

@@ -699,3 +704,3 @@

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' })
}

@@ -713,3 +718,3 @@

this.check_awards(connection) // update awards
this.check_awards(connection) // update awards

@@ -740,3 +745,3 @@ const results = connection.results.collate(this)

if (!k || k.score === undefined) {
connection.results.add(this, {err: 'karma results missing'})
connection.results.add(this, { err: 'karma results missing' })
return next()

@@ -747,3 +752,3 @@ }

this.check_awards(connection)
connection.results.add(this, {msg: 'no action', emit: true })
connection.results.add(this, { msg: 'no action', emit: true })
return next()

@@ -759,3 +764,3 @@ }

connection.results.add(this, {emit: true })
connection.results.add(this, { emit: true })
next()

@@ -765,3 +770,2 @@ }

exports.get_award_loc_from_note = function (connection, award) {
if (connection.transaction) {

@@ -781,3 +785,2 @@ const obj = this.assemble_note_obj(connection.transaction, award)

exports.get_award_loc_from_results = function (connection, loc_bits) {
let pi_name = loc_bits[1]

@@ -809,7 +812,9 @@ let notekey = loc_bits[2]

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)

@@ -819,3 +824,7 @@ }

// 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()

@@ -831,7 +840,9 @@ return this.get_award_loc_from_results(connection.transaction, loc_bits)

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

@@ -862,5 +873,7 @@ if (valbits[2].match(/^(equals|gt|lt|match)$/)) {

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)

@@ -870,3 +883,3 @@ delete karma.todo[key]

}
if (note !== wants) continue // didn't match
if (note !== wants) continue // didn't match
}

@@ -898,3 +911,5 @@

const operator = bits[3]
if (bits[4]) { wants = bits[4] }
if (bits[4]) {
wants = bits[4]
}
switch (operator) {

@@ -911,3 +926,6 @@ case 'gt':

default:
connection.logerror(this, `length operator "${operator}" not supported.`)
connection.logerror(
this,
`length operator "${operator}" not supported.`,
)
continue

@@ -917,5 +935,7 @@ }

}
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

@@ -935,3 +955,4 @@ if (!wants) continue

if (!award) return
if (isNaN(award)) { // garbage in config
if (isNaN(award)) {
// garbage in config
connection.logerror(this, `non-numeric award from: ${nl}:${award}`)

@@ -941,16 +962,21 @@ 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)

@@ -963,3 +989,3 @@ if (award > 0) connection.results.add(this, { pass: trimmed })

if (!this.cfg.spammy_tlds) return
if (mail_from.isNull()) return // null sender (bounce)
if (mail_from.isNull()) return // null sender (bounce)

@@ -972,4 +998,4 @@ const from_tld = mail_from.host.split('.').pop()

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' })
}

@@ -980,6 +1006,6 @@

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' })
}

@@ -1008,38 +1034,39 @@

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
.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.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,
}
if (asn_score) {
if (asn_score < -5) {
asn_results.fail = 'asn:history'
if (asn_score) {
if (asn_score < -5) {
asn_results.fail = 'asn:history'
} else if (asn_score > 5) {
asn_results.pass = '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 })

@@ -1051,4 +1078,5 @@ })

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)

@@ -1068,6 +1096,7 @@ .exec()

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()
}
{
"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"
},

@@ -26,14 +35,12 @@ "repository": {

"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"
}
}

@@ -21,3 +21,2 @@ [![Build Status][ci-img]][ci-url]

## How Karma Works

@@ -36,5 +35,4 @@

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

@@ -44,3 +42,2 @@

## <a name="awards"></a>AWARDS

@@ -56,3 +53,2 @@

## <a name="penalties"></a>Penalties

@@ -68,3 +64,3 @@

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.

@@ -106,9 +102,9 @@ The reward is purposefully small, to permit good senders in bad neighborhoods to still send.

* [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)

@@ -118,6 +114,5 @@ The data from these tests are helpful but the real power of karma is [scoring

### <a name="IP_Reputation"></a>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.

@@ -152,3 +147,3 @@

#### report\_as
#### report_as

@@ -160,3 +155,3 @@ 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:

to:
to:

@@ -170,3 +165,2 @@ NNN asn | pass | equals | asn_all_good | 2

### <a name="unrecognized"></a>Unrecognized SMTP verbs/commands

@@ -203,6 +197,4 @@

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

@@ -209,0 +201,0 @@ [geoip-url]: https://github.com/haraka/haraka-plugin-geoip

'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
const stub = fixtures.stub.stub
function _set_up (done) {
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']

@@ -25,3 +24,2 @@

describe('karma_init', function () {

@@ -88,3 +86,6 @@ beforeEach(function (done) {

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)

@@ -95,5 +96,8 @@ 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()

@@ -111,3 +115,3 @@ })

}
this.plugin.hook_deny(next, this.connection, ['','','',''])
this.plugin.hook_deny(next, this.connection, ['', '', '', ''])
})

@@ -120,3 +124,3 @@

}
this.plugin.hook_deny(next, this.connection, ['','','karma',''])
this.plugin.hook_deny(next, this.connection, ['', '', 'karma', ''])
})

@@ -130,3 +134,3 @@

this.plugin.deny_exclude_plugins = { access: true }
this.plugin.hook_deny(next, this.connection, ['','','access',''])
this.plugin.hook_deny(next, this.connection, ['', '', 'access', ''])
})

@@ -140,4 +144,10 @@

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',
])
})

@@ -151,3 +161,3 @@

this.plugin.deny_exclude_hooks = { queue: true }
this.plugin.hook_deny(next, this.connection, ['','','','','','queue'])
this.plugin.hook_deny(next, this.connection, ['', '', '', '', '', 'queue'])
})

@@ -160,3 +170,10 @@

}
this.plugin.hook_deny(next, this.connection, [constants.DENYSOFT,'','','','',''])
this.plugin.hook_deny(next, this.connection, [
constants.DENYSOFT,
'',
'',
'',
'',
'',
])
})

@@ -169,3 +186,3 @@ })

it('relaying=false', function (done) {
this.connection.relaying=false
this.connection.relaying = false
const r = this.plugin.get_award_location(this.connection, 'relaying')

@@ -177,3 +194,3 @@ assert.equal(false, r)

it('relaying=true', function (done) {
this.connection.relaying=true
this.connection.relaying = true
const r = this.plugin.get_award_location(this.connection, 'relaying')

@@ -225,3 +242,6 @@ assert.equal(true, r)

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);

@@ -234,3 +254,6 @@ assert.equal(undefined, r)

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])

@@ -244,8 +267,16 @@ done()

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()

@@ -255,5 +286,9 @@ })

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()

@@ -273,3 +308,3 @@ })

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)

@@ -281,6 +316,5 @@ assert.equal(undefined, r)

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' },
})

@@ -305,9 +339,11 @@ // test a non-matching criteria

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()

@@ -318,5 +354,5 @@ })

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)

@@ -449,3 +485,3 @@ assert.equal(undefined, r)

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 })

@@ -473,5 +509,8 @@ this.plugin.should_we_deny(next, this.connection, 'connect')

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',
}

@@ -486,5 +525,8 @@ this.plugin.check_result_equal(['clean'], award, this.connection)

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',
}

@@ -502,5 +544,8 @@ this.plugin.check_result_equal(['clean'], award, this.connection)

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',
}

@@ -520,5 +565,8 @@ this.plugin.check_result_gt([4], award, this.connection)

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',
}

@@ -534,5 +582,8 @@ this.plugin.check_result_lt([4], award, this.connection)

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',
}

@@ -551,5 +602,8 @@ this.plugin.check_result_lt([4], award, this.connection)

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',
}

@@ -565,5 +619,8 @@ this.plugin.check_result_match(['isphishing'], award, this.connection)

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',
}

@@ -578,7 +635,14 @@ this.plugin.check_result_match(['clean'], award, this.connection)

const award = {
id : 89, award : 2,
operator : 'match', value : 'google.com',
reason : 'testing', resolution : 'never',
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)
this.plugin.check_result_match(
['mail-yk0-f182.google.com'],
award,
this.connection,
)
// console.log(this.connection.results.store);

@@ -595,5 +659,8 @@ assert.equal(this.connection.results.store.karma.score, 2)

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',
}

@@ -609,5 +676,8 @@ this.plugin.check_result_length(['3'], award, this.connection)

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',
}

@@ -622,5 +692,8 @@ this.plugin.check_result_length(['4'], award, this.connection)

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',
}

@@ -636,5 +709,8 @@ this.plugin.check_result_length(['5'], award, this.connection)

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',
}

@@ -649,5 +725,8 @@ this.plugin.check_result_length(['3'], award, this.connection)

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',
}

@@ -663,5 +742,8 @@ this.plugin.check_result_length(['2'], award, this.connection)

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',
}

@@ -680,5 +762,8 @@ this.plugin.check_result_length(['3'], award, this.connection)

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',
}

@@ -694,5 +779,8 @@ this.plugin.check_result_exists(['3'], award, this.connection)

const award = {
id : 1, award : 3,
operator : 'exists', value : '',
reason : 'testing', resolution : 'misses',
id: 1,
award: 3,
operator: 'exists',
value: '',
reason: 'testing',
resolution: 'misses',
}

@@ -715,5 +803,7 @@ this.plugin.check_result_exists([], award, this.connection)

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);

@@ -730,5 +820,7 @@ assert.equal(this.connection.results.store.karma.score, 2)

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);

@@ -769,9 +861,13 @@ assert.equal(this.connection.results.store.karma.score, -5)

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:<foo@test.com>"
this.plugin.hook_mail(() => {
assert.equal(this.connection.results.store.karma, undefined)
done()
}, this.connection, [mfrom])
this.connection.current_line = 'MAIL FROM:<foo@test.com>'
this.plugin.hook_mail(
() => {
assert.equal(this.connection.results.store.karma, undefined)
done()
},
this.connection,
[mfrom],
)
})

@@ -781,10 +877,14 @@

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:<foo@test.com>"
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:<foo@test.com>'
this.plugin.hook_mail(
() => {
// console.log(this.connection.results.store);
assert.equal(this.connection.results.store.karma.score, 2)
done()
},
this.connection,
[mfrom],
)
})

@@ -794,10 +894,14 @@

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:<foo@test.com>"
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:<foo@test.com>'
this.plugin.hook_mail(
() => {
// console.log(this.connection.results.store);
assert.equal(this.connection.results.store.karma.score, -4)
done()
},
this.connection,
[mfrom],
)
})

@@ -810,6 +914,6 @@ })

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)

@@ -832,6 +936,6 @@ done()

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)

@@ -838,0 +942,0 @@ done()

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc