haraka-plugin-karma 2.1.0 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +2 -2
- package/Changes.md +13 -0
- package/config/karma.ini +77 -43
- package/index.js +33 -32
- package/package.json +4 -4
package/.github/workflows/ci.yml
CHANGED
|
@@ -21,7 +21,7 @@ jobs:
|
|
|
21
21
|
strategy:
|
|
22
22
|
matrix:
|
|
23
23
|
os: [ ubuntu-latest ]
|
|
24
|
-
node-version: [
|
|
24
|
+
node-version: [ 18, 20 ]
|
|
25
25
|
fail-fast: false
|
|
26
26
|
steps:
|
|
27
27
|
- uses: actions/checkout@v3
|
|
@@ -38,7 +38,7 @@ jobs:
|
|
|
38
38
|
strategy:
|
|
39
39
|
matrix:
|
|
40
40
|
os: [ windows-latest ]
|
|
41
|
-
node-version: [
|
|
41
|
+
node-version: [ 18, 20 ]
|
|
42
42
|
fail-fast: false
|
|
43
43
|
steps:
|
|
44
44
|
- uses: actions/checkout@v3
|
package/Changes.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
#### N.N.N - YYYY-MM-DD
|
|
3
3
|
|
|
4
4
|
|
|
5
|
+
### [2.1.2] - 2023-12-11
|
|
6
|
+
|
|
7
|
+
- config: update several plugin names
|
|
8
|
+
- style(es6): refer to plugin as 'this'
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### [2.1.1] - 2023-08-22
|
|
12
|
+
|
|
13
|
+
- fix: check_result unexpected return #50
|
|
14
|
+
|
|
15
|
+
|
|
5
16
|
### [2.1.0] - 2022-11-29
|
|
6
17
|
|
|
7
18
|
- fix: in disconnect, call redis_unsub after skip check
|
|
@@ -105,3 +116,5 @@
|
|
|
105
116
|
|
|
106
117
|
- use redis.merge_redis_ini()
|
|
107
118
|
[2.1.0]: https://github.com/haraka/haraka-plugin-karma/releases/tag/2.1.0
|
|
119
|
+
[2.1.1]: https://github.com/haraka/haraka-plugin-karma/releases/tag/2.1.1
|
|
120
|
+
[2.1.2]: https://github.com/haraka/haraka-plugin-karma/releases/tag/2.1.2
|
package/config/karma.ini
CHANGED
|
@@ -19,7 +19,7 @@ enable=true
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
[tarpit]
|
|
22
|
-
delay=
|
|
22
|
+
delay=1
|
|
23
23
|
|
|
24
24
|
; If you make the remote wait too long, they drop the connection.
|
|
25
25
|
; 'max' limits how long to make remotes wait between responses.
|
|
@@ -57,32 +57,49 @@ hooks=unrecognized_command,data,data_post,queue,queue_outbound
|
|
|
57
57
|
; karma captures and scores deny requests from other plugins, permitting finer
|
|
58
58
|
; control over connection handling. For plugins that should be able to reject
|
|
59
59
|
; the connection, add their name to the plugin list:
|
|
60
|
-
plugins=send_email, tls, access, helo.checks, data.headers, rspamd, spamassassin,
|
|
60
|
+
plugins=send_email, tls, access, helo.checks, data.headers, rspamd, spamassassin, clamd, attachment, limit
|
|
61
61
|
|
|
62
62
|
; hooks whose DENY rejections should be not be captured.
|
|
63
|
-
hooks=rcpt, queue
|
|
63
|
+
hooks=rcpt, queue, queue_outbound
|
|
64
64
|
|
|
65
65
|
|
|
66
66
|
[spammy_tlds]
|
|
67
67
|
; award negative karma to spammy TLDs
|
|
68
68
|
; caution, awarding karma > msg_negative_limit may blacklist that TLD
|
|
69
|
-
|
|
70
|
-
rocks=-3
|
|
71
|
-
ninja=-3
|
|
72
|
-
info=-2
|
|
69
|
+
bid=-3
|
|
73
70
|
biz=-2
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
bet=-4
|
|
72
|
+
bio=-4
|
|
73
|
+
buzz=-3
|
|
74
|
+
club=-3
|
|
75
|
+
company=-4
|
|
76
|
+
directory=-4
|
|
77
77
|
eu=-4
|
|
78
|
+
fun=-5
|
|
79
|
+
guru=-4
|
|
80
|
+
gury=-3
|
|
81
|
+
icu=-6
|
|
82
|
+
info=-4
|
|
78
83
|
link=-3
|
|
79
|
-
|
|
80
|
-
|
|
84
|
+
me=-1
|
|
85
|
+
monster=-5
|
|
86
|
+
mom=-5
|
|
87
|
+
na=-6
|
|
88
|
+
ninja=-3
|
|
89
|
+
one=-4
|
|
90
|
+
pics=-5
|
|
91
|
+
pw=-2
|
|
92
|
+
rocks=-3
|
|
81
93
|
ru=-2
|
|
82
|
-
|
|
94
|
+
sbs=-4
|
|
95
|
+
science=-6
|
|
83
96
|
stream=-3
|
|
84
|
-
|
|
97
|
+
studio=-5
|
|
98
|
+
top=-5
|
|
85
99
|
trade=-3
|
|
100
|
+
us=-4
|
|
101
|
+
work=-4
|
|
102
|
+
xyz=-6
|
|
86
103
|
|
|
87
104
|
|
|
88
105
|
[tls]
|
|
@@ -129,11 +146,11 @@ early_talker = -3
|
|
|
129
146
|
; towards spam and vise versa for hammy senders.
|
|
130
147
|
|
|
131
148
|
[asn_awards]
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
149
|
+
55286 = -6
|
|
150
|
+
33182 = -4
|
|
151
|
+
46717 = -4
|
|
152
|
+
13332 = -4
|
|
153
|
+
200002 = -4
|
|
137
154
|
|
|
138
155
|
|
|
139
156
|
; RESULT AWARDS
|
|
@@ -157,6 +174,7 @@ early_talker = -3
|
|
|
157
174
|
|
|
158
175
|
[result_awards]
|
|
159
176
|
;geoip.too_far = -1
|
|
177
|
+
|
|
160
178
|
001 = geoip | distance | gt | 4000 | -1 | Geographic distance is unusual for ham
|
|
161
179
|
002 = geoip | distance | gt | 8000 | -1 | Geographic distance is unusual for ham
|
|
162
180
|
|
|
@@ -176,17 +194,23 @@ early_talker = -3
|
|
|
176
194
|
012 = karma | fail | equals | rfc5321.MailFrom | -1 | RFC Ignorant MTA | Use a RFC compliant MTA
|
|
177
195
|
013 = karma | fail | equals | rfc5321.RcptTo | -1 | RFC Ignorant MTA | Use a RFC compliant MTA
|
|
178
196
|
|
|
197
|
+
016 = geoip | country | equals | RU | -3 | Sender from Russia
|
|
198
|
+
|
|
179
199
|
020 = asn | pass | equals | karma | 1 | ASN reputation is good
|
|
180
200
|
021 = asn | fail | equals | karma | -1 | ASN reputation is bad
|
|
181
201
|
022 = asn | pass | equals | asn_all_good | 2 | ASN reputation is ham-only
|
|
182
202
|
023 = asn | fail | equals | asn_all_bad | -2 | ASN reputation is spam-only
|
|
203
|
+
024 = asn | org | match | eonix | -4 | Spammy Hoster
|
|
183
204
|
|
|
184
|
-
;030 =
|
|
185
|
-
031 =
|
|
186
|
-
032 =
|
|
205
|
+
;030 = p0f | os_name | match | freebsd | 1 | FreeBSD
|
|
206
|
+
031 = p0f | os_name | match | windows | -1 | Windows OS, likely infected by malware | Don't use Windows as MTA
|
|
207
|
+
032 = p0f | os_flavor | equals | XP | -2 | Windows XP, likely infected by malware | Upgrade to a supported OS
|
|
187
208
|
|
|
188
209
|
; give back the point penalized for running windows
|
|
189
|
-
080 = fcrdns
|
|
210
|
+
; 080 = fcrdns | fcrdns | match | outlook.com | 1
|
|
211
|
+
; 081 = fcrdns | fcrdns@1 = 1 if length gt 0
|
|
212
|
+
; 082 = fcrdns | err@1 = -1 if length gt 0
|
|
213
|
+
; 083 = fcrdns | fail@1 = -1 if length gt 0
|
|
190
214
|
084 = fcrdns | fail | match | ptr_valid | -4 | FCrDNS has no valid PTR | Set up https://en.wikipedia.org/wiki/Forward-confirmed_reverse_DNS
|
|
191
215
|
085 = fcrdns | fail | match | valid_tld | -6 | FCrDNS has no valid TLD | Set up https://en.wikipedia.org/wiki/Forward-confirmed_reverse_DNS
|
|
192
216
|
086 = fcrdns | fail | equals | has_rdns | -6 | FCrDNS has no rDNS | Set up https://en.wikipedia.org/wiki/Forward-confirmed_reverse_DNS
|
|
@@ -212,6 +236,7 @@ early_talker = -3
|
|
|
212
236
|
117 = dnsbl | fail | equals | xbl.spamhaus.org | -6 | DNS Blacklist | Disinfect your host/network
|
|
213
237
|
118 = dnsbl | fail | equals | cbl.abuseat.org | -5 | DNS Blacklist | Disinfect your host/network
|
|
214
238
|
119 = dnsbl | fail | equals | dnsbl.justspam.org | -1 | DNS Blacklist | Disinfect your host/network
|
|
239
|
+
120 = dnsbl | fail | equals | dnsbl.sorbs.net | -2 | DNS Blacklist | Clean up DNSBL listing
|
|
215
240
|
|
|
216
241
|
130 = helo.checks | fail | match | valid_hostname | -1 | HELO host invalid | Use valid HELO hostname
|
|
217
242
|
131 = helo.checks | pass | match | forward_dns | 1 | HELO host has forward DNS
|
|
@@ -242,28 +267,30 @@ early_talker = -3
|
|
|
242
267
|
172 = rcpt_to.in_host_list | fail | gt | 0 | -1 | Invalid envelope recipient
|
|
243
268
|
;173 = rcpt_to.in_host_list | pass | gt | 0 | 1 | Valid Envelope recipient
|
|
244
269
|
|
|
245
|
-
181 =
|
|
246
|
-
182 =
|
|
247
|
-
183 =
|
|
248
|
-
184 =
|
|
249
|
-
185 =
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
270
|
+
181 = headers | fail | match | from_match | -1 | Envelope From does not match Message From:
|
|
271
|
+
182 = headers | pass | match | from_match | 1 | Envelope From matches Message From:
|
|
272
|
+
183 = headers | fail | equals | UA | -1 | Uncommon MUA
|
|
273
|
+
184 = headers | fail | match | direct-to-mx | -1 | Not relayed
|
|
274
|
+
185 = headers | fail | match | missing | -1 | Missing a required header
|
|
275
|
+
186 = headers | fail | match | from_phish | -6 | Phish attempt
|
|
276
|
+
|
|
277
|
+
190 = uribl | fail | equals | fresh15.spameatingmonkey.net | -2 | URI blacklist: fresh15.spameatingmonkey.net
|
|
278
|
+
191 = uribl | fail | equals | dbl.spamhaus.org | -2 | URI blacklist: dbl.spamhaus.org
|
|
279
|
+
192 = uribl | fail | equals | multi.uribl.com | -2 | URI blacklist: multi.uribl.com
|
|
280
|
+
193 = uribl | fail | equals | multi.surbl.org | -2 | URI blacklist: multi.surbl.org
|
|
281
|
+
194 = uribl | fail | match | rdns | -2 | URI Blacklist | Don't send spam
|
|
282
|
+
195 = uribl | fail | match | helo | -2 | URI Blacklist | Don't send spam
|
|
283
|
+
196 = uribl | fail | match | ehlo | -2 | URI Blacklist | Don't send spam
|
|
284
|
+
197 = uribl | fail | match | envfrom | -2 | URI Blacklist | Don't send spam
|
|
285
|
+
198 = uribl | fail | match | from | -2 | URI Blacklist | Don't send spam
|
|
286
|
+
199 = uribl | fail | match | replyto | -2 | URI Blacklist | Don't send spam
|
|
287
|
+
200 = uribl | fail | match | body | -2 | URI Blacklist | Don't send spam
|
|
288
|
+
201 = uribl | fail | match | msgid | -2 | URI Blacklist | Don't send spam
|
|
263
289
|
|
|
264
290
|
205 = bounce | fail | equals | single_recipient | -8 | Invalid bounce
|
|
265
291
|
206 = bounce | fail | equals | empty_return_path | -8 | Invalid bounce
|
|
266
292
|
207 = bounce | fail | equals | bad_rcpt | -8 | Invalid bounce
|
|
293
|
+
208 = bounce | isa | equals | yes | -2 | Bounces are bad
|
|
267
294
|
|
|
268
295
|
210 = clamd | fail | match | executable | -4 | Clam AntiVirus Executable
|
|
269
296
|
211 = clamd | fail | match | structured | -2 | Clam AntiVirus Structured
|
|
@@ -282,6 +309,12 @@ early_talker = -3
|
|
|
282
309
|
233 = rspamd | score | gt | 6 | -1 | rspamd moderate score
|
|
283
310
|
234 = rspamd | score | gt | 10 | -1 | rspamd high score
|
|
284
311
|
235 = rspamd | is_spam | equals | false | 1 | rspamd detected as ham
|
|
312
|
+
236 = rspamd | action | match | reject | -2 | rspamd suggested reject
|
|
313
|
+
|
|
314
|
+
237 = rspamd | symbols | match | DMARC_POLICY_ALLOW | 1 | DMARC policy allow
|
|
315
|
+
238 = rspamd | symbols | match | DMARC_POLICY_REJECT | -6 | DMARC policy reject
|
|
316
|
+
239 = rspamd | symbols | match | DMARC_POLICY_QUARANTINE | -4 | DMARC policy reject
|
|
317
|
+
240 = rspamd | symbols | match | DMARC_POLICY_SOFTFAIL | -2 | DMARC policy softfail
|
|
285
318
|
|
|
286
319
|
251 = spamassassin | hits | lt | 0 | 1 |
|
|
287
320
|
252 = spamassassin | hits | lt | -2 | 1 |
|
|
@@ -297,5 +330,6 @@ early_talker = -3
|
|
|
297
330
|
264 = spamassassin | hits | gt | 9 | -2 |
|
|
298
331
|
265 = spamassassin | hits | gt | 20 | -10 |
|
|
299
332
|
|
|
300
|
-
280 = known-senders | pass |
|
|
301
|
-
281 =
|
|
333
|
+
280 = known-senders | pass | equals | wks | 5 | Known Sender
|
|
334
|
+
281 = known-senders | pass | length | gt 0 | 5 | Known Sender
|
|
335
|
+
282 = limit | fail | length | gt 0 | -5 | Exceeding rate limits
|
package/index.js
CHANGED
|
@@ -155,18 +155,18 @@ exports.check_result = function (connection, message) {
|
|
|
155
155
|
if (!this.result_awards[m.plugin]) return // no awards for plugin
|
|
156
156
|
|
|
157
157
|
for (const r of Object.keys(m.result)) { // each result in mess
|
|
158
|
-
if (r === 'emit')
|
|
158
|
+
if (r === 'emit') continue // r: pass, fail, skip, err, ...
|
|
159
159
|
|
|
160
160
|
const pi_prop = this.result_awards[m.plugin][r]
|
|
161
|
-
if (!pi_prop)
|
|
161
|
+
if (!pi_prop) continue // no award for this plugin property
|
|
162
162
|
|
|
163
163
|
const thisResult = m.result[r]
|
|
164
164
|
// ignore empty arrays, objects, and strings
|
|
165
|
-
if (Array.isArray(thisResult) && thisResult.length === 0)
|
|
165
|
+
if (Array.isArray(thisResult) && thisResult.length === 0) continue
|
|
166
166
|
if (typeof thisResult === 'object' && !Object.keys(thisResult).length) {
|
|
167
|
-
|
|
167
|
+
continue
|
|
168
168
|
}
|
|
169
|
-
if (typeof thisResult === 'string' && !thisResult)
|
|
169
|
+
if (typeof thisResult === 'string' && !thisResult) continue // empty
|
|
170
170
|
|
|
171
171
|
// do any award conditions match this result?
|
|
172
172
|
for (let i=0; i < pi_prop.length; i++) { // each award...
|
|
@@ -518,6 +518,12 @@ exports.hook_queue = function (next, connection) {
|
|
|
518
518
|
this.should_we_deny(next, connection, 'queue')
|
|
519
519
|
}
|
|
520
520
|
|
|
521
|
+
exports.hook_queue_outbound = function (next, connection) {
|
|
522
|
+
if (this.should_we_skip(connection)) return next()
|
|
523
|
+
|
|
524
|
+
this.should_we_deny(next, connection, 'queue_outbound')
|
|
525
|
+
}
|
|
526
|
+
|
|
521
527
|
exports.hook_reset_transaction = function (next, connection) {
|
|
522
528
|
if (this.should_we_skip(connection)) return next()
|
|
523
529
|
|
|
@@ -544,15 +550,15 @@ exports.hook_unrecognized_command = function (next, connection, params) {
|
|
|
544
550
|
exports.ip_history_from_redis = function (next, connection) {
|
|
545
551
|
const plugin = this
|
|
546
552
|
|
|
547
|
-
if (
|
|
553
|
+
if (this.should_we_skip(connection)) return next()
|
|
548
554
|
|
|
549
|
-
const expire = (
|
|
555
|
+
const expire = (this.cfg.redis.expire_days || 60) * 86400 // to days
|
|
550
556
|
const dbkey = `karma|${connection.remote.ip}`
|
|
551
557
|
|
|
552
558
|
// redis plugin is emitting errors, no need to here
|
|
553
|
-
if (!
|
|
559
|
+
if (!this.db) return next()
|
|
554
560
|
|
|
555
|
-
|
|
561
|
+
this.db.hGetAll(dbkey).then(dbr => {
|
|
556
562
|
if (dbr === null) {
|
|
557
563
|
plugin.init_ip(dbkey, connection.remote.ip, expire)
|
|
558
564
|
return next()
|
|
@@ -591,7 +597,6 @@ exports.ip_history_from_redis = function (next, connection) {
|
|
|
591
597
|
connection.results.add(plugin, { err })
|
|
592
598
|
next()
|
|
593
599
|
})
|
|
594
|
-
|
|
595
600
|
}
|
|
596
601
|
|
|
597
602
|
exports.hook_mail = function (next, connection, params) {
|
|
@@ -672,42 +677,39 @@ exports.hook_data_post = function (next, connection) {
|
|
|
672
677
|
}
|
|
673
678
|
|
|
674
679
|
exports.increment = function (connection, key, val) {
|
|
675
|
-
|
|
676
|
-
if (!plugin.db) return
|
|
680
|
+
if (!this.db) return
|
|
677
681
|
|
|
678
|
-
|
|
682
|
+
this.db.hIncrBy(`karma|${connection.remote.ip}`, key, 1)
|
|
679
683
|
|
|
680
|
-
const asnkey =
|
|
681
|
-
if (asnkey)
|
|
684
|
+
const asnkey = this.get_asn_key(connection)
|
|
685
|
+
if (asnkey) this.db.hIncrBy(asnkey, key, 1)
|
|
682
686
|
}
|
|
683
687
|
|
|
684
688
|
exports.hook_disconnect = function (next, connection) {
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
if (plugin.should_we_skip(connection)) return next()
|
|
689
|
+
if (this.should_we_skip(connection)) return next()
|
|
688
690
|
|
|
689
|
-
|
|
691
|
+
this.redis_unsubscribe(connection)
|
|
690
692
|
|
|
691
693
|
const k = connection.results.get('karma')
|
|
692
694
|
if (!k || k.score === undefined) {
|
|
693
|
-
connection.results.add(
|
|
695
|
+
connection.results.add(this, {err: 'karma results missing'})
|
|
694
696
|
return next()
|
|
695
697
|
}
|
|
696
698
|
|
|
697
|
-
if (!
|
|
698
|
-
|
|
699
|
-
connection.results.add(
|
|
699
|
+
if (!this.cfg.thresholds) {
|
|
700
|
+
this.check_awards(connection)
|
|
701
|
+
connection.results.add(this, {msg: 'no action', emit: true })
|
|
700
702
|
return next()
|
|
701
703
|
}
|
|
702
704
|
|
|
703
|
-
if (k.score > (
|
|
704
|
-
|
|
705
|
+
if (k.score > (this.cfg.thresholds.positive || 3)) {
|
|
706
|
+
this.increment(connection, 'good', 1)
|
|
705
707
|
}
|
|
706
708
|
if (k.score < 0) {
|
|
707
|
-
|
|
709
|
+
this.increment(connection, 'bad', 1)
|
|
708
710
|
}
|
|
709
711
|
|
|
710
|
-
connection.results.add(
|
|
712
|
+
connection.results.add(this, {emit: true })
|
|
711
713
|
next()
|
|
712
714
|
}
|
|
713
715
|
|
|
@@ -739,11 +741,11 @@ exports.get_award_loc_from_results = function (connection, loc_bits) {
|
|
|
739
741
|
let obj
|
|
740
742
|
if (connection.transaction) obj = connection.transaction.results.get(pi_name)
|
|
741
743
|
|
|
742
|
-
// connection.logdebug(
|
|
744
|
+
// connection.logdebug(this, `no txn results: ${pi_name}`);
|
|
743
745
|
if (!obj) obj = connection.results.get(pi_name)
|
|
744
746
|
if (!obj) return
|
|
745
747
|
|
|
746
|
-
// connection.logdebug(
|
|
748
|
+
// connection.logdebug(this, `found results for ${pi_name}, ${notekey}`);
|
|
747
749
|
if (notekey) return obj[notekey]
|
|
748
750
|
return obj
|
|
749
751
|
}
|
|
@@ -997,9 +999,8 @@ exports.get_asn_key = function (connection) {
|
|
|
997
999
|
}
|
|
998
1000
|
|
|
999
1001
|
exports.init_asn = function (asnkey, expire) {
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
plugin.db.multi()
|
|
1002
|
+
if (!this.db) return
|
|
1003
|
+
this.db.multi()
|
|
1003
1004
|
.hmSet(asnkey, {'bad': 0, 'good': 0, 'connections': 1})
|
|
1004
1005
|
.expire(asnkey, expire * 2) // keep ASN longer
|
|
1005
1006
|
.exec()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "haraka-plugin-karma",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"description": "A heuristics scoring and reputation engine for SMTP connections",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -27,12 +27,12 @@
|
|
|
27
27
|
"haraka-constants": ">=1.0.2",
|
|
28
28
|
"haraka-utils": "*",
|
|
29
29
|
"haraka-plugin-redis": "2.0.5",
|
|
30
|
-
"redis": "4.
|
|
30
|
+
"redis": "4.6.11"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"eslint": "8",
|
|
33
|
+
"eslint": "8.55.0",
|
|
34
34
|
"eslint-plugin-haraka": "*",
|
|
35
35
|
"haraka-test-fixtures": "*",
|
|
36
|
-
"mocha": "
|
|
36
|
+
"mocha": "10.2.0"
|
|
37
37
|
}
|
|
38
38
|
}
|