Spamassassin, utilisation de sa-learn

Vous avez une base en béton de règle spamassassin avec une éducation provenant de plusieurs dizaines de milliers de mails et spams et là, un bug de spamassassin ou un coup du sort, bref, la base est vérolée et perdue. Ça énerve (si!)
Vous auriez du utiliser la commande non documentée (en tout cas sans pub, je n’ai rien vu)
$ sa-learn --backup > regles.txt
puis
$ sa-learn --restore regles.txt
Ça permet même d’exporter des règles d’un serveur à un autre ce qui m’a permis de récupérer une base presque aussi complète que celle que j’avais…

Excellente astuce.
Juste: si tu peux m’expliquer comment on utilise sa-learn, ça fera DEUX tuto à toi que j’utiliserais.

En fait ça n’est pas très dur, si on bacle un peu ça donne:

Règles bayes, utilisation de sa-learn.

Spamassassin est muni d’un dispositif de règles de Bayes lui permettant de s’adapter aux messages au fur et à mesure. Pour la petite histoire, ces fameuses règles sont les suivantes:

proba(A sachant B).proba(B) = proba(A et B)
comme
proba(B sachant A).proba(A) = proba(B et A)

on en déduit

proba(A sachant B) = proba(B sachant A).proba(A)/proba(B)

Si A est «est un spam»
Si B est «contient le mot Viagra»

Proba(A) est la proportion de spams
Proba(B) est la proportion de message contenant Viagra
Proba(B sachant A) est la proportion de spams contenant Viagra

on en déduit la probabilité Proba(A sachant B) pour un message d’être un spam si il contient le mot Viagra.

Ça c’est la théorie. Dans la pratique, il faut choisir avec soin les mots (=tokens, Viagra est une bonne idée), c’est ce qui s’appelle je pense les tokens. Spamassassin s’occupe de ça. Les règles de Bayes s’éduquent, il faut connaître avec une certaine précision les différentes proportions. C’est pour cela que les règles de Bayes de spamassassin ne se déclenche qu’à partir d’un certains nombres de messages appris (200 spams et 200 hams par défaut je crois bien), cf /usr/share/perl5/Mail/SpamAssassin/conf.pm.

Sur un serveur de courrier, spamassassin appartient à mail et le répertoire .spamassassin est dans /var/mail. Il traite les fichiers de tout le monde.

Pour éduquer spamassassin, on peut le faire de façons variées:

Un répertoire toto contenant des spams (1 fichier par message)

$ sa-learn --spam toto

ou

$ cd toto $ sa-learn --spam --no-sync --file * $ sa-learn --sync
(le --no-sync permet d’aller plus vite (car l’apprentissage prend du temps) mais rend le -sync indispensable)

ou si on a un fichier format mbox

$ sa-learn --spam --mbox fichier_mbox_de_spams[/code]

Remplacer spam par ham pour apprendre les hams. Un bon apprentissage nécessite un bon millier de spams et un bons milleirs de messages un tant soit peu variés (on ne peut pas resservir sans arrêt le même).

[quote]Spamassassin apprend également[/quote] au fur et à mesure (on le voit dans les entêtes:
[quote]X-Spam-Status: No, score=-2.6 required=5.0 tests=BAYES_00 autolearn=ham
(apris en ham)[/quote]
ou

[quote]X-Spam-Status: No, score=2.6 required=5.0 tests=BAYES_00,HTML_MESSAGE,
        MIME_HTML_ONLY,NO_REAL_NAME,SUBJ_ILLEGAL_CHARS autolearn=no 
(non utilisé)[/quote]

ou
[quote]X-Spam-Status: Yes, score=32.8 required=5.0 tests=BAYES_99,DATE_IN_PAST_12_24,
        HELO_DYNAMIC_IPADDR,RCVD_IN_SORBS_DUL,UNPARSEABLE_RELAY,        URIBL_AB_SURBL,URIBL_JP_SURBL,URIBL_OB_SURBL,URIBL_SBL,URIBL_SC_SURBL,
        URIBL_WS_SURBL autolearn=spam version=3.1.7-deb
(appris en spam)
[/quote]

[b]Le programme suivant[/b]

[code]# Exim filter
if
     $h_X-Spam-Status contains "Yes,"
then
    logfile /var/log/spam.log
    logwrite "SPAM: $tod_log $message_id processed venant de $sender_address"
    save /var/tmp/Spams/
    finish
endif[/code]

mis dans le fichier .forward d'un utilisateur permet sur un serveur Exim qu'un spam reçu pour la personne considéré soit déplacé dans /var/tmp/Spams avec une trace dans le log. Cela permet éventuellement de retrouver un faux positif (rare).

[b]Il est plus fréquent[/b] d'avoir des faux négatifs, i.e des spams passant le filtre. Il fautéduquer spamassassin en lui apprenant ce message comme spam. On peut mettre en pla
ce un bête système pour qu'un spam redirigé vers une adresse [spam@monserveur.fr](mailto:spam@monserveur.fr) soit appris en tant que spam. Pour cela

1) Rajout de
spam: "| /usr/local/bin/traitespam"

dans /etc/aliases

2) Le fichier /usr/local/bin/traitespam contient
[code]#!/bin/sh
NOM=S`/bin/date +"%s"`
/bin/cat > /tmp/$NOM
/bin/chmod 777 /tmp/$NOM
/bin/date >> /tmp/learnspam.log
HOME=/var/mail/ /usr/local/bin/mangespam /tmp/$NOM >> /tmp/learnspam.log
/bin/rm /tmp/$NOM
  1. Le fichier mangespam est un programme Caml appartenant à mail en suid, il contient

#open "sys";; #open "unix";; setuid 8; setgid 8; system_command ("/usr/bin/sa-learn --spam --file "^sys__command_line.(1));;

Enfin on peut signaler automatiquement les spams à signalspam chaque jour par le script suivant:

[code]#!/usr/bin/python2.4

-- coding: iso-8859-15 --

Pierre Beyssac – 15/5/2007

import base64
import cookielib
import os
import sys
import urllib
import urllib2

Compte à configurer

username = 'logindesignalspam’
password = ‘motdepasse’

class signalspam_old:
""“Submit a spam using standard web forms.”""
connecturl = 'https://www.signal-spam.fr/connexion.php
signalurl = ‘https://www.signal-spam.fr/signaler.php

def init(self):
policy = cookielib.DefaultCookiePolicy(
rfc2965=True, strict_ns_domain=cookielib.DefaultCookiePolicy.DomainStrict)
cj = cookielib.MozillaCookieJar()
cj.set_policy(policy)
self.opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj))
def _postexpect(self, url, data, expect):
""“Post form at given url, expect some substring in reply.”""
urlenc_data = urllib.urlencode(data)
z = self.opener.open(url, urlenc_data)
page = z.read()
z.close()
return page.find(expect) >= 0

def authenticate(self, username, password):
""“Authenticate to the service.
A session cookie will be set as a side effect.”""
data = {
‘nomutilisateur’: username,
‘motdepasse’: password,
‘connexion’: ‘Connexion’
}
return self._postexpect(self.connecturl, data, ‘Espace membre’)

def submit(self, msg):
""“Submit a message.”""
data = { ‘message’: msg, ‘envoi’: ‘Envoyer’ }
if self._postexpect(self.signalurl, data, ‘Succès’):
return 202
return 999

class signalspam:
""“Submit a spam using the undocumented REST API.”""
resturl = 'https://www.signal-spam.fr/api/signaler
host = 'www.signal-spam.fr
realm = ‘signal-spam’

def authenticate(self, username, password):
""“Authenticate to the service.”""
self.auth_handler = urllib2.HTTPBasicAuthHandler()
self.opener = urllib2.build_opener(self.auth_handler)
self.auth_handler.add_password(self.realm, self.host, username, password)
return True

def submit(self, msg):
""“Submit a message.”""
# Using base64 in the API is a weird idea, since it’s probably
# not mandated and uses up 25% more space.
urlenc_data = urllib.urlencode({ ‘message’: base64.b64encode(msg) })
code = 202
try:
z = self.opener.open(self.resturl, urlenc_data)
except urllib2.HTTPError, e:
code = e.code
return code

if password == None:
if len(sys.argv) != 2:
print >>sys.stderr, "Missing password"
sys.exit(1)
password = sys.argv[1]

sp = signalspam()

print >>sys.stderr, “Connexion en cours…”,
sys.stderr.flush()

if not sp.authenticate(username, password):
print >>sys.stderr, "Erreur de connexion"
sys.exit(1)
print >>sys.stderr, “ok”

print >>sys.stderr, "Lecture sur stdin…"
mailtext = '\n\n’
lines = 0
for l in sys.stdin:
mailtext += l
lines += 1

msglist = mailtext.split(’\nFrom:’)[1:]
l = len(msglist)

print >>sys.stderr, “Fin de lecture, %d ligne(s), %d message(s)” % (lines, l)
if lines < 10 or l < 1:
print >>sys.stderr, "Trop court !"
sys.exit(1)

#sys.exit(0)

n = 1
for msg in msglist:
code = sp.submit('From ’ + msg)
if code == 202:
print >>sys.stderr, “Envoi ok message %d/%d” % (n, l)
else:
print >>sys.stderr, “Erreur %d à l’envoi du message %d/%d” % (code, n, l)
n += 1

[/code]

Un script /usr/local/bin/virespam

#!/bin/sh /usr/local/bin/tourne /var/tmp/SPAM if [ -f /var/tmp/SPAM.7.gz ] ; then rm /var/tmp/SPAM.7.gz fi cd /var/tmp/Spams ls | xargs -n 1 /usr/local/bin/getspam > /var/tmp/SPAM ls /var/tmp/Spams/* | xargs rm /usr/local/bin/signalspam.py

avec /usr/local/bin/tourne étant

#!/bin/sh if [ -f $1.1.gz ] ; then j=`ls $1*gz | grep -c $1` for i in `seq $[$j] 1`; do mv $1.$i.gz $1.$[$i+1].gz; done fi if [ -f $1.0 ] ; then mv $1.0 $1.1 gzip $1.1 fi cat $1 > $1.0 echo -n > $1

permet de classifier une fois par jour les spams et de les envoyer à signalspam. Il suffit de mettre
0 4 * * * mail /usr/local/bin/virespam > /dev/null 2> /dev/null

dans la crontab.

La base de spamassassin est précieuse, j’effectue chaque jour la commandeai mis dans la crontab les commandes suivantes permettant de virer les éléments obsolètes de la b
ase (qui a tendance à devenir obèse) et la remet d’aplomb.

33 0 * * * mail /usr/bin/sa-learn --sync > /dev/null 2> /dev/null
30 1 * * * mail /usr/bin/sa-learn --force-expire > /dev/null 2> /dev/null

Par ailleurs, la commande

52 18 * * * mail /usr/local/bin/sauvebayes
avec /usr/local/bin/sauvebayes contenant

#!/bin/sh
/bin/date +'/usr/bin/sa-learn --backup | gzip > /var/mail/backup-%w.txt.gz' | /bin/bash > /tmp/logbayes 2>&1

permet de sauvegarder un exemplaire de la base de données. En fait un par semaine, je les écrème à la main au fur et à mesure. Cela permet de faire un
$ gunzip /var/mail/backup-14.txt.gz
$ sa-learn --restore /var/mail/backup-14.txt

en cas de cata (on s’aperçoit TRÈS vite quand les règles bayes ne fonctionnent plus).