#! /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' 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)