Port Spam filter to Rust - part 3
This commit is contained in:
@@ -1,49 +0,0 @@
|
||||
|
||||
if eval "(contains(subject_lc, 'delivery') &&
|
||||
(contains(subject_lc, 'failed') ||
|
||||
contains(subject_lc, 'report') ||
|
||||
contains(subject_lc, 'status') ||
|
||||
contains(subject_lc, 'warning'))) ||
|
||||
(contains(subject_lc, 'failure') &&
|
||||
(contains(subject_lc, 'delivery') ||
|
||||
contains(subject_lc, 'notice') ||
|
||||
contains(subject_lc, 'mail') )) ||
|
||||
(contains(subject_lc, 'delivered') &&
|
||||
(contains(subject_lc, 'couldn\\'t be') ||
|
||||
contains(subject_lc, 'could not be') ||
|
||||
contains(subject_lc, 'hasn\\'t been') ||
|
||||
contains(subject_lc, 'has not been'))) ||
|
||||
contains(subject_lc, 'returned mail') ||
|
||||
contains(subject_lc, 'undeliverable') ||
|
||||
contains(subject_lc, 'undelivered')" {
|
||||
# Subject contains words or phrases typical for DSN
|
||||
let "t.SUBJ_BOUNCE_WORDS" "1";
|
||||
}
|
||||
|
||||
if eval "is_empty(envelope.from)" {
|
||||
if eval "eq_ignore_case(header.content-type, 'multipart/report') &&
|
||||
( eq_ignore_case(header.content-type.attr.report-type, 'delivery-status') ||
|
||||
eq_ignore_case(header.content-type.attr.report-type, 'disposition-notification'))" {
|
||||
let "t.BOUNCE" "1";
|
||||
} else {
|
||||
let "from" "to_lowercase(header.from)";
|
||||
|
||||
if eval "contains(from, 'mdaemon') && !is_empty(header.X-MDDSN-Message)" {
|
||||
let "t.BOUNCE" "1";
|
||||
} elsif eval "contains(from, 'postmaster') || contains(from, 'mailer-daemon')" {
|
||||
if eval "t.SUBJ_BOUNCE_WORDS" {
|
||||
let "t.BOUNCE" "1";
|
||||
} else {
|
||||
foreverypart {
|
||||
if eval "(eq_ignore_case(header.content-type.type, 'message') ||
|
||||
eq_ignore_case(header.content-type.type, 'text')) &&
|
||||
(eq_ignore_case(header.content-type.subtype, 'rfc822-headers') ||
|
||||
eq_ignore_case(header.content-type.subtype, 'rfc822'))" {
|
||||
let "t.BOUNCE" "1";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
let "rcvd_raw" "header.received[*].raw";
|
||||
let "rcvd_count" "count(rcvd_raw)";
|
||||
|
||||
# Count received headers
|
||||
if eval "rcvd_count == 0" {
|
||||
let "t.RCVD_COUNT_ZERO" "1";
|
||||
} elsif eval "rcvd_count == 1" {
|
||||
let "t.RCVD_COUNT_ONE" "1";
|
||||
} elsif eval "rcvd_count == 2" {
|
||||
let "t.RCVD_COUNT_TWO" "1";
|
||||
} elsif eval "rcvd_count == 3" {
|
||||
let "t.RCVD_COUNT_THREE" "1";
|
||||
} elsif eval "rcvd_count <= 5" {
|
||||
let "t.RCVD_COUNT_FIVE" "1";
|
||||
} elsif eval "rcvd_count <= 7" {
|
||||
let "t.RCVD_COUNT_SEVEN" "1";
|
||||
} elsif eval "rcvd_count <= 12" {
|
||||
let "t.RCVD_COUNT_TWELVE" "1";
|
||||
}
|
||||
|
||||
# Received from an authenticated user
|
||||
if eval "!is_empty(env.authenticated_as)" {
|
||||
let "t.RCVD_VIA_SMTP_AUTH" "1";
|
||||
}
|
||||
|
||||
# Received headers have non-ASCII characters
|
||||
if eval "!is_ascii(rcvd_raw)" {
|
||||
let "t.RCVD_ILLEGAL_CHARS" "1";
|
||||
}
|
||||
|
||||
let "i" "0";
|
||||
let "tls_count" "0";
|
||||
let "rcvd_from_ip" "0";
|
||||
while "i < rcvd_count" {
|
||||
let "i" "i + 1";
|
||||
let "helo_domain" "received_part(i, 'from')";
|
||||
|
||||
# Check for a forged received trail
|
||||
if eval "!t.FORGED_RCVD_TRAIL" {
|
||||
let "iprev" "received_part(i, 'iprev')";
|
||||
|
||||
if eval "!is_empty(iprev) && !is_empty(helo_domain) && !eq_ignore_case(helo_domain, iprev)" {
|
||||
let "t.FORGED_RCVD_TRAIL" "1";
|
||||
}
|
||||
}
|
||||
|
||||
if eval "!t.PREVIOUSLY_DELIVERED" {
|
||||
let "for" "received_part(i, 'for')";
|
||||
# Recipient appears on Received trail
|
||||
if eval "!is_empty(for) && contains_ignore_case(recipients, for)" {
|
||||
let "t.PREVIOUSLY_DELIVERED" "1";
|
||||
}
|
||||
}
|
||||
|
||||
if eval "!t.RCVD_HELO_USER && eq_ignore_case(helo_domain, 'user')" {
|
||||
# Received: HELO contains 'user'
|
||||
let "t.RCVD_HELO_USER" "1";
|
||||
}
|
||||
|
||||
if eval "!is_empty(received_part(i, 'from.ip'))" {
|
||||
# Received from an IP address rather than a FQDN
|
||||
let "rcvd_from_ip" "rcvd_from_ip + 1";
|
||||
}
|
||||
|
||||
if eval "!is_empty(received_part(i, 'tls'))" {
|
||||
# Received with TLS
|
||||
let "tls_count" "tls_count + 1";
|
||||
}
|
||||
}
|
||||
|
||||
if eval "rcvd_from_ip >= 2 || (rcvd_from_ip == 1 && is_ip_addr(env.helo_domain))" {
|
||||
# Has two or more Received headers containing bare IP addresses
|
||||
let "t.RCVD_DOUBLE_IP_SPAM" "1";
|
||||
}
|
||||
|
||||
if eval "rcvd_count == 0" {
|
||||
# One received header in a message (currently zero but one header will be added later by the MTA)
|
||||
let "t.ONCE_RECEIVED" "1";
|
||||
|
||||
# Message has been directly delivered from MUA to local MX
|
||||
if eval "header.User-Agent.exists || header.X-Mailer.exists" {
|
||||
let "t.DIRECT_TO_MX" "1";
|
||||
}
|
||||
}
|
||||
|
||||
# Received with TLS checks
|
||||
if eval "rcvd_count > 0 && tls_count == rcvd_count && !is_empty(env.tls.version)" {
|
||||
let "t.RCVD_TLS_ALL" "1";
|
||||
} elsif eval "!is_empty(env.tls.version)" {
|
||||
let "t.RCVD_TLS_LAST" "1";
|
||||
} else {
|
||||
let "t.RCVD_NO_TLS_LAST" "1";
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
|
||||
let "raw_subject_lc" "to_lowercase(header.subject.raw)";
|
||||
let "is_ascii_subject" "is_ascii(subject_lc)";
|
||||
|
||||
if eval "len(subject_clean) >= 10 && count(tokenize(subject_clean, 'words')) > 1 && is_uppercase(subject_clean)" {
|
||||
# Subject contains mostly capital letters
|
||||
let "t.SUBJ_ALL_CAPS" "1";
|
||||
}
|
||||
|
||||
if eval "count_chars(subject_clean) > 200" {
|
||||
# Subject is very long
|
||||
let "t.LONG_SUBJ" "1";
|
||||
}
|
||||
|
||||
if eval "!is_empty(tokenize(subject_lc, 'uri_strict'))" {
|
||||
# Subject contains a URL
|
||||
let "t.URL_IN_SUBJECT" "1";
|
||||
}
|
||||
|
||||
if eval "!is_ascii(raw_subject_lc) && !env.param.smtputf8 && env.param.body != '8bitmime' && env.param.body != 'binarymime'" {
|
||||
# Subject needs encoding
|
||||
let "t.SUBJECT_NEEDS_ENCODING" "1";
|
||||
}
|
||||
|
||||
if eval "!header.Subject.exists" {
|
||||
# Missing subject header
|
||||
let "t.MISSING_SUBJECT" "1";
|
||||
} elsif eval "is_empty(trim(subject_lc))" {
|
||||
# Subject is empty
|
||||
let "t.EMPTY_SUBJECT" "1";
|
||||
}
|
||||
|
||||
if eval "is_ascii(subject_lc) && contains(raw_subject_lc, '=?') && contains(raw_subject_lc, '?=')" {
|
||||
if eval "contains(raw_subject_lc, '?q?')" {
|
||||
# Subject header is unnecessarily encoded in quoted-printable
|
||||
let "t.SUBJ_EXCESS_QP" "1";
|
||||
} elsif eval "contains(raw_subject_lc, '?b?')" {
|
||||
# Subject header is unnecessarily encoded in base64
|
||||
let "t.SUBJ_EXCESS_BASE64" "1";
|
||||
}
|
||||
}
|
||||
|
||||
if eval "starts_with(subject_lc, 're:') && is_empty(header.in-reply-to) && is_empty(header.references)" {
|
||||
# Fake reply
|
||||
let "t.FAKE_REPLY" "1";
|
||||
}
|
||||
|
||||
let "subject_lc_trim" "trim_end(subject_lc)";
|
||||
if eval "subject_lc != subject_lc_trim" {
|
||||
# Subject ends with space characters
|
||||
let "t.SUBJECT_ENDS_SPACES" "1";
|
||||
}
|
||||
|
||||
if eval "contains(subject_lc, '$') ||
|
||||
contains(subject_lc, '€') ||
|
||||
contains(subject_lc, '£') ||
|
||||
contains(subject_lc, '¥')" {
|
||||
# Subject contains currency symbols
|
||||
let "t.SUBJECT_HAS_CURRENCY" "1";
|
||||
}
|
||||
|
||||
if eval "ends_with(subject_lc_trim, '!')" {
|
||||
# Subject ends with an exclamation mark
|
||||
let "t.SUBJECT_ENDS_EXCLAIM" "1";
|
||||
} elsif eval "ends_with(subject_lc_trim, '?')" {
|
||||
# Subject ends with a question mark
|
||||
let "t.SUBJECT_ENDS_QUESTION" "1";
|
||||
}
|
||||
|
||||
if eval "contains(subject_lc_trim, '!')" {
|
||||
# Subject contains an exclamation mark
|
||||
let "t.SUBJECT_HAS_EXCLAIM" "1";
|
||||
}
|
||||
|
||||
if eval "contains(subject_lc_trim, '?')" {
|
||||
# Subject contains a question mark
|
||||
let "t.SUBJECT_HAS_QUESTION" "1";
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
if eval "(count(body_urls) == 1 || count(html_body_urls) == 1) && count(tokenize(text_body, 'words')) == 0" {
|
||||
let "t.URL_ONLY" "1";
|
||||
}
|
||||
|
||||
if eval "has_zwsp(urls)" {
|
||||
let "t.ZERO_WIDTH_SPACE_URL" "1";
|
||||
} elsif eval "has_obscured(urls)" {
|
||||
let "t.R_SUSPICIOUS_URL" "1";
|
||||
}
|
||||
|
||||
let "i" "count(urls)";
|
||||
while "i > 0" {
|
||||
let "i" "i - 1";
|
||||
let "url" "urls[i]";
|
||||
|
||||
# Skip non-URLs such as 'data:' and 'mailto:'
|
||||
if eval "!contains(url, '://')" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let "host" "uri_part(url, 'host')";
|
||||
|
||||
if eval "!is_empty(host)" {
|
||||
let "is_ip" "is_ip_addr(host)";
|
||||
let "host" "puny_decode(host)";
|
||||
let "host_lc" "to_lowercase(host)";
|
||||
let "host_sld" "domain_part(host_lc, 'sld')";
|
||||
|
||||
# Skip local and trusted domains
|
||||
if eval "is_local_domain(DOMAIN_DIRECTORY, host_sld) || key_exists('spam-allow', host_sld)" {
|
||||
continue;
|
||||
}
|
||||
|
||||
if eval "!is_ip &&
|
||||
(!t.REDIRECTOR_URL || !t.URL_REDIRECTOR_NESTED) &&
|
||||
key_exists('spam-redirect', host_sld)" {
|
||||
let "t.REDIRECTOR_URL" "1";
|
||||
let "redir_count" "1";
|
||||
|
||||
while "redir_count <= 5" {
|
||||
# Use a custom user-agent and a 3 second timeout
|
||||
let "url_redirect" "http_header(url, 'Location', 'Mozilla/5.0 (X11; Linux i686; rv:109.0) Gecko/20100101 Firefox/118.0', 3000)";
|
||||
if eval "!is_empty(url_redirect)" {
|
||||
let "url" "url_redirect";
|
||||
let "host" "uri_part(url, 'host')";
|
||||
let "is_ip" "is_ip_addr(host)";
|
||||
let "host" "puny_decode(host)";
|
||||
let "host_lc" "to_lowercase(host)";
|
||||
let "host_sld" "domain_part(host_lc, 'sld')";
|
||||
|
||||
if eval "!is_ip && key_exists('spam-redirect', host_sld)" {
|
||||
let "redir_count" "redir_count + 1";
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if eval "redir_count > 5" {
|
||||
let "t.URL_REDIRECTOR_NESTED" "1";
|
||||
}
|
||||
}
|
||||
|
||||
let "url_lc" "to_lowercase(url)";
|
||||
let "query" "uri_part(url_lc, 'path_query')";
|
||||
if eval "!is_ip" {
|
||||
if eval "!is_ascii(host)" {
|
||||
let "host_cured" "cure_text(host)";
|
||||
if eval "host_lc != host_cured && dns_exists(host_cured, 'ip')" {
|
||||
let "t.HOMOGRAPH_URL" "1";
|
||||
}
|
||||
|
||||
if eval "!is_single_script(host)" {
|
||||
let "t.MIXED_CHARSET_URL" "1";
|
||||
}
|
||||
} else {
|
||||
if eval "ends_with(host, 'googleusercontent.com') && starts_with(query, '/proxy/')" {
|
||||
let "t.HAS_GUC_PROXY_URI" "1";
|
||||
} elsif eval "ends_with(host, 'firebasestorage.googleapis.com')" {
|
||||
let "t.HAS_GOOGLE_FIREBASE_URL" "1";
|
||||
} elsif eval "starts_with(domain_part(host, 'sld'), 'google.') && contains(query, 'url?') " {
|
||||
let "t.HAS_GOOGLE_REDIR" "1";
|
||||
}
|
||||
}
|
||||
|
||||
if eval "(contains(host_lc, 'ipfs.') || contains(query, '/ipfs')) && contains(query, '/qm')" {
|
||||
# InterPlanetary File System (IPFS) gateway URL, likely malicious
|
||||
let "t.HAS_IPFS_GATEWAY_URL" "1";
|
||||
} elsif eval "ends_with(host_lc, '.onion')" {
|
||||
let "t.HAS_ONION_URI" "1";
|
||||
}
|
||||
} else {
|
||||
# URL is an ip address
|
||||
let "t.R_SUSPICIOUS_URL" "1";
|
||||
}
|
||||
|
||||
if eval "starts_with(query, '/wp-')" {
|
||||
# Contains WordPress URIs
|
||||
let "t.HAS_WP_URI" "1";
|
||||
if eval "starts_with(query, '/wp-content') | starts_with(query, '/wp-includes')" {
|
||||
# URL that is pointing to a compromised WordPress installation
|
||||
let "t.WP_COMPROMISED" "1";
|
||||
}
|
||||
}
|
||||
if eval "contains(query, '/../') && !contains(query, '/well-known') && !contains(query, '/well_known')" {
|
||||
# Message contains URI with a hidden path
|
||||
let "t.URI_HIDDEN_PATH" "1";
|
||||
}
|
||||
|
||||
# Phishing checks (refresh OpenPhish every 12 hours, PhishTank every 6 hours)
|
||||
if eval "key_exists_http('https://openphish.com/feed.txt', url, [43200, 'list'])" {
|
||||
let "t.PHISHED_OPENPHISH" "1";
|
||||
}
|
||||
if eval "key_exists_http('http://data.phishtank.com/data/online-valid.csv', url, [21600, 'csv', 1, ',', true])" {
|
||||
let "t.PHISHED_PHISHTANK" "1";
|
||||
}
|
||||
|
||||
} else {
|
||||
# URL could not be parsed
|
||||
let "t.R_SUSPICIOUS_URL" "1";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user