diff --git a/surveillance/fichiers_copyrightes.py b/surveillance/fichiers_copyrightes.py new file mode 100755 index 00000000..37e4986b --- /dev/null +++ b/surveillance/fichiers_copyrightes.py @@ -0,0 +1,322 @@ +#! /usr/bin/env python +# -*- coding:utf-8 -*- + +""" +Script de détection de fichiers potentiellement copyrightés dans les +répertoires des adhérents. Les tests se basent sur le nom et la taille +des fichiers. + +Copyright (C) Nicolas Bruot +Licence : GPL v2 +""" + +import sys +import os +import re +import time +import shutil + +# Variables générales + +sendto = 'bureau@lists.crans.org tvincent@crans.org' +analysed_path = '/home-adh/' + +old_path = '/usr/scripts/var/copyrighted-content.old' +new_path = '/usr/scripts/var/copyrighted-content.new' +diffs_path = '/usr/scripts/var/copyrighted-content.diffs' +alert_path = '/usr/scripts/var/copyrighted-content.alert' + +# ====================================================================== +# Variables pour le calcul du score. +# ================================== + +# Score minimum pour être enregistré dans le fichier des différences. +min_score = 10 + +# Score à partir duquel une notification est envoyée immédiatement par +# email (sans attendre l'exécution avec l'option --report). +notif_score = 40 + +# Taille en dessous de laquelle aucun test n'est effectué sur le +# fichier (augmenter pour améliorer la rapidité du script). +pass_size = 10e3 + +# Taille en dessous de laquelle seuls les filtres regexps_light sont +# testés. +light_size = 40e3 + +# Caractères non alphanumériques ("_" inclus) +blank = '[^a-zA-Z0-9]' +lblank = '([^a-zA-Z0-9]|^)' # blanc ou début de chaine +rblank = '([^a-zA-Z0-9]|$)' # blanc ou fin de chaine + +# regexps, regexps_light : motifs contribuant au score du fichier. +# Pour qu'un motif contribue au score, il doit être matché et, si le +# champ min_size existe, respecter la condition de taille minimum du +# fichier. +# Si un ou plusieurs motifs avec un champ crit_size sont matchés, +# size_score est ajouté au score si la taille du fichier dépasse la +# valeur minimale des crit_size matchés. +# Seuls les motifs de regexp_light sont testés pour les fichiers d'une +# taille inférieure à light_size. Pour ce dictionnaire, il ne peut pas +# y avoir de champs min_size ou crit_size. +size_score = 10 +regexps_light = {'\.torrent$': {'score':40}, + '\.srt$': {'score':40}, + 'key.?gen': {'score':40}} +regexps = {'\.avi$': {'score':10, 'min_size':1e6, 'crit_size':1e8}, + '\.mkv$': {'score':10, 'min_size':1e6, 'crit_size':1e8}, + '\.mov$': {'score':10, 'min_size':1e6, 'crit_size':1e8}, + '\.mpeg$': {'score':10, 'min_size':1e6, 'crit_size':1e8}, + '\.mp4$': {'score':10, 'min_size':1e6, 'crit_size':1e8}, + '\.mpg$': {'score':10, 'min_size':1e6, 'crit_size':1e8}, + '\.ogm$': {'score':10, 'min_size':1e5, 'crit_size':1e8}, + '\.wmv$': {'score':10, 'min_size':1e5, 'crit_size':1e8}, + '\.flv$': {'score':10, 'min_size':1e6, 'crit_size':1e8}, + '\.mp3$': {'score':10, 'min_size':1e5}, + '\.m4a$': {'score':10, 'min_size':1e5}, + '\.zip$': {'score':5, 'min_size':1e5, 'crit_size':1e8}, + '\.gz$': {'score':3, 'min_size':1e5, 'crit_size':1e8}, + '\.rar$': {'score':5, 'min_size':1e5, 'crit_size':1e8}, + '\.dmg$': {'score':10, 'min_size':1e5, 'crit_size':1e8}, + '\.pdf$': {'score':5, 'min_size':1e5, 'crit_size':2e7}, + '\.ogg$': {'score':10, 'min_size':1e5}, + '\.iso$': {'score':15, 'min_size':1e5, 'crit_size':1e8}, + + # Episodes (S01E01, Episode01...) + 's\d{1,2}e\d': {'score':20, 'min_size':1e8}, + lblank + '\d{1,2}x\d{1,2}' + rblank: {'score':20, 'min_size':1e8}, + 'episode.?\d': {'score':20}, + lblank + 's\d{1,2}' + rblank: {'score':20, 'min_size':1e7}, + lblank + 'e\d{1,2}' + rblank: {'score':20, 'min_size':1e7}, + lblank + 'vol.{0,2}\d{1,2}' + rblank: {'score':20}, + + # Crochets [] + '\[(?!pot).*\]': {'score':5}, + + # "01 Toto.mp3"... + '^\d{1,2}' + blank + '.*\.(mp3|ogg|wmv)$': {'score':10}, + + # Autres mots-clés + blank + 'fr' + rblank: {'score':10}, + blank + 'eng' + rblank: {'score':10}, + + blank + 'vo' + rblank: {'score':10, 'min_size':1e7}, + blank + 'vost' + rblank: {'score':30}, + blank + 'vostfr' + rblank: {'score':30}, + blank + 'vosten' + rblank: {'score':30}, + + blank * 3: {'score':3}, + + lblank + 'dvd.?(rip|)' + rblank: {'score':20}, + + lblank + 'cd' + rblank: {'score':10, 'min_size':1e8}, + lblank + 'xvid' + rblank: {'score':20}, + lblank + 'hdtv' + rblank: {'score':20}, + lblank + 'hq' + rblank: {'score':20}, + lblank + 'vtv' + rblank: {'score':20}, + lblank + 'tvrip' + rblank: {'score':20}, + lblank + 'dvtv' + rblank: {'score':20}, + lblank + 'bt' + rblank: {'score':20}, + lblank + 'fqm' + rblank: {'score':20}, + lblank + 'web.?rip' + rblank: {'score':20}, + lblank + 'natzox' + rblank: {'score':10}, + lblank + 'sfm' + rblank: {'score':20}, + lblank + 'lbp' + rblank: {'score':20}, + lblank + 'hd' + rblank: {'score':10}, + lblank + 'hdp' + rblank: {'score':10}, + lblank + 'itdk' + rblank: {'score':20}, + lblank + 'ust' + rblank: {'score':20}, + lblank + 'fxg' + rblank: {'score':20}, + + lblank + 'part.?\d': {'score':10, 'min_size':1e7}} + +# Si aucune expression n'a été matchée, un fichier d'une taille +# supérieure à suspect_size se verra attribuer un score de +# suspect_size_score. +suspect_size = 200e6 +suspect_size_score = 30 + +# ====================================================================== + +# Pour les gros fichiers, on teste l'ensemble des expressions +# régulières : +regexps_full = dict(regexps_light, **regexps) + +# Compilation des expressions régulières : +for key in regexps_light.keys(): + regexps_light[re.compile(key, re.I).findall] = regexps_light[key] + del regexps_light[key] + +for key in regexps_full.keys(): + regexps_full[re.compile(key, re.I).findall] = regexps_full[key] + del regexps_full[key] + + +def human_readable(num): + """Convertit le nombre en entrée (taille d'un fichier) en une + chaine "human-readable". Exemple: 12000 => 12K""" + + for x in ['', 'K', 'M', 'G', 'T']: + if num < 1000.: + return '%3.0f%s' % (num, x) + num /= 1000. + + +def get_score(file, size): + """Calcule le score d'un fichier d'après des expressions + régulières et des conditions de taille.""" + + score = 0 + + if size < light_size: + # On ne fait que les tests simples. + + for key in regexps_light: + m = key(file) # liste d'expressions matchées + param = regexps_light[key] + + score += len(m)*param['score'] + + return score + + else: + # On effectue les tests complexes. + + crit_size = 1e99 + # On parcourt les expressions régulières. + for key in regexps_full: + m = key(file) # liste d'expressions matchées + param = regexps_full[key] + + # Tests avec min_size. + if not param.has_key('min_size') or size >= param['min_size']: + score += len(m)*param['score'] + + # Mise à jour de crit_size. + if param.has_key('crit_size'): + crit_size = min(crit_size, param['crit_size']) + + # Si le fichier est gros par rapport à une taille définie par les + # expressions matchées, on augmente le score. + if size >= crit_size: + score += size_score + + # Si le score est nul, les fichiers gros sont quand même suspects. + if score == 0 and size >= suspect_size: + score = suspect_size_score + + return score + + +def analyse(): + """Liste les fichiers dans le dossier à analyser et ajoute les + nouveaux fichiers à copyrighted.diffs. Envoie une notification si + des scores trop élevés sont trouvés.""" + + if os.path.isfile(new_path): + # Rotation des fichiers + shutil.move(new_path, old_path) + # Lecture de old_files + old_files = open(old_path).read().split('\n') + else: + old_files = [] + # Ouverture des autres fichiers + new_file = open(new_path, 'w') + diffs_file = open(diffs_path, 'a') + alert_file = open(alert_path, 'w') + os.chmod(new_path, 0600) + os.chmod(diffs_path, 0600) + os.chmod(alert_path, 0600) + + # Listage des fichiers + for root, sub_folders, files in os.walk(analysed_path): + + for file in files: + filename = os.path.join(root, file) + try: + filesize = os.path.getsize(filename) + except OSError: # le fichier a pu être supprimé entre temps + filesize = 0 + + if filesize < pass_size: + # On va pas tester les fichiers trop petits. + continue + + score = get_score(file, filesize) + + if score >= min_score: + # On inscrit le fichier. + new_file.write(filename + '\n') + # Si c'est un nouveau fichier, on l'ajoute au + # fichier diffs. + if not filename in old_files: + date = time.strftime("%Y-%m-%d %H:%M", + time.localtime()) + line = (str(score) + '\t' + date + + '\t' + human_readable(filesize) + + '\t' + filename + '\n') + + diffs_file.write(line) + + # Pour des scores vraiment élevés on va alerter + # par email immédiatement. + if score >= notif_score: + alert_file.write(line) + + # On ne parcourt pas les dossiers "Mail". + if len(root.split('/')) == 3 and 'Mail' in sub_folders: + sub_folders.remove('Mail') + + new_file.close() + diffs_file.close() + alert_file.close() + + # Envoi d'une notification le cas échéant. + if os.path.getsize(alert_path) > 0: + sujet = "Alerte de fichiers potentiellement copyrightes" + os.system('sort -n -r ' + alert_path + ' | mail -s "' + + sujet + '" ' + sendto) + os.remove(alert_path) + + +def report(): + """Envoie un email récapitulatif du fichier de + copyrighted-content.diffs et purge le fichier.""" + + # Envoi de l'email contenant diffs_path + if os.path.isfile(diffs_path): + sujet = "Rapport des fichiers potentiellement copyrightes" + os.system('sort -n -r ' + diffs_path + ' | mail -s "' + + sujet + '" ' + sendto) + os.remove(diffs_path) + + +if __name__ == '__main__': + + help = """Détection de fichiers potentiellement copyrightés. +usage: fichiers_copyrightes.py [OPTION] + +OPTION doit être l'une des suivantes : + + --analyse + Ajoute dans le fichier copyrighted-content.diffs les nouveaux + fichiers du répertoire exploré attribués de leur score. Si des + fichiers de score élevé sont trouvés, un email de notification est + envoyé. + + --report + Envoie un email récapitulatif du fichier copyrighted-content.diffs + et purge le fichier.""" + + # Gestion des options + if len(sys.argv) != 2: + print help + sys.exit(0) + elif sys.argv[1] == '--analyse': + analyse() + elif sys.argv[1] == '--report': + report() + else: + print help + sys.exit(0)