-
Notifications
You must be signed in to change notification settings - Fork 25
/
Copy pathExeFilter.py
1138 lines (973 loc) · 48.2 KB
/
ExeFilter.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/python
# -*- coding: iso-8859-1 -*-
"""
ExeFilter - main module / programme principal
ExeFilter is an open-source tool and framework to filter file formats in
e-mails, web pages or files. It detects many common file formats and can remove
active content (scripts, macros, etc) according to a configurable policy.
ExeFilter permet de filtrer des fichiers, courriels ou pages web, afin de
supprimer tout code exécutable et tout contenu potentiellement dangereux en
termes de sécurité informatique.
ExeFilter peut être employé soit comme script (lancé directement depuis la
ligne de commande), soit comme module (importé dans un autre script).
Lancé comme script, ExeFilter dépollue un ensemble de fichiers situés dans un
répertoire et place le résultat dans un répertoire destination.
La source et la destination peuvent être fournies en ligne de commande,
ou bien grâce à la fonction transfert() si ce module est importé.
Ce fichier fait partie du projet ExeFilter.
URL du projet: U{http://www.decalage.info/exefilter}
@organization: DGA/CELAR
@author: U{Philippe Lagadec<mailto:philippe.lagadec(a)laposte.net>}
@author: U{Arnaud Kerréneur<mailto:arnaud.kerreneur(a)dga.defense.gouv.fr>}
@author: U{Tanguy Vinceleux<mailto:tanguy.vinceleux(a)dga.defense.gouv.fr>}
@contact: U{Philippe Lagadec<mailto:philippe.lagadec(a)laposte.net>}
@copyright: DGA/CELAR 2004-2008
@copyright: NATO/NC3A 2008-2011 (modifications PL apres ExeFilter v1.1.0)
@license: CeCILL (open-source compatible GPL)
cf. code source ou fichier LICENCE.txt joint
@version: 1.18
@status: beta
"""
#==============================================================================
__docformat__ = 'epytext en'
#__author__ = "Philippe Lagadec, Tanguy Vinceleux, Arnaud Kerréneur (DGA/CELAR)"
__date__ = "2011-08-25"
__version__ = "1.18"
#------------------------------------------------------------------------------
# LICENCE pour le projet ExeFilter:
# Copyright DGA/CELAR 2004-2008
# Copyright NATO/NC3A 2008-2010 (modifications PL apres ExeFilter v1.1.0)
# Auteurs:
# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net
# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr
# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr
#
# Ce logiciel est régi par la licence CeCILL soumise au droit français et
# respectant les principes de diffusion des logiciels libres. Vous pouvez
# utiliser, modifier et/ou redistribuer ce programme sous les conditions
# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA
# sur le site "http://www.cecill.info". Une copie de cette licence est jointe
# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html.
#
# En contrepartie de l'accessibilité au code source et des droits de copie,
# de modification et de redistribution accordés par cette licence, il n'est
# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons,
# seule une responsabilité restreinte pèse sur l'auteur du programme, le
# titulaire des droits patrimoniaux et les concédants successifs.
#
# A cet égard l'attention de l'utilisateur est attirée sur les risques
# associés au chargement, à l'utilisation, à la modification et/ou au
# développement et à la reproduction du logiciel par l'utilisateur étant
# donné sa spécificité de logiciel libre, qui peut le rendre complexe à
# manipuler et qui le réserve donc à des développeurs et des professionnels
# avertis possédant des connaissances informatiques approfondies. Les
# utilisateurs sont donc invités à charger et tester l'adéquation du
# logiciel à leurs besoins dans des conditions permettant d'assurer la
# sécurité de leurs systèmes et ou de leurs données et, plus généralement,
# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité.
#
# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez
# pris connaissance de la licence CeCILL, et que vous en avez accepté les
# termes.
#------------------------------------------------------------------------------
# HISTORIQUE:
# 2004-10-24 v0.01 PL: - 1ère version (Sas de dépollution)
# 2004-2006 PL,AK,TV: - Nombreuses évolutions
# 2007-06-20 v1.01 PL: - Licence CeCILL, transformation en ExeFilter
# 2007-07-24 PL: - Appel à get_username pour ameliorer la portabilité
# 2007-09-13 PL: - Amélioration portabilité constantes, imports
# 2007-10-08 PL: - Ajout option -e pour exporter la politique en HTML
# - Journal syslog desactive par defaut
# 2007-10-22 PL: - Ajout options antivirus
# 2008-02-29 PL: - correction banniere et rapport avec XF_VERSION/DATE
# 2008-03-23 v1.02 PL: - ajout gettext dans programme principal pour traduction
# - ajout _() a chaque constante chaine pour traduction
# - ajout parametre pour activer l'archivage (non par défaut)
# - code archivage deplace de transfert vers init_archivage
# 2009-10-05 v1.03 PL: - set default encoding to Latin-1 to avoid unicode errors
# 2009-10-22 v1.04 PL: - archiving disabled by default
# 2009-11-02 v1.05 PL: - init_gettext always called, even when imported
# - updated parameters for gettext translation
# 2009-11-11 v1.06 PL: - added new CLI option -o for a single output file
# - added translation for CLI options and summary
# - added parameters to set exit code according to results
# 2010-02-04 v1.07 PL: - removed commun.sous_rep_temp to avoid race conditions
# - avoid exceptions when username or locale cannot be
# determined
# 2010-02-07 v1.08 PL: - added batch mode option to disable HTML report display
# - removed path module import
# 2010-02-09 v1.09 PL: - workaround when username cannot be determined
# 2010-02-23 v1.10 PL: - removed plx import
# 2010-04-20 v1.11 PL: - added new option -f to force filename extension
# 2010-05-02 v1.12 PL: - added display_html_report to display HTML report
# 2010-09-24 v1.13 PL: - translated log folder names to English
# - no log file by default
# 2010-12-03 v1.14 PL: - new command line option -l to enable log file
# 2011-02-18 v1.15 PL: - now uses the system temp dir by default
# 2011-04-17 v1.16 PL: - scan-only mode when no destination is specified
# 2011-04-30 v1.17 PL: - new functions to scan and clean files or dirs
# 2011-08-25 v1.18 PL: - added initial support for multithreading
# - added scan_string and clean_string functions
#------------------------------------------------------------------------------
# TODO:
# + improve scan_string and clean_string (see TODOs)
# + improve logfile support in scan/clean functions
# + add option to set log file names or "auto" for automatic name
# + add option to set log file level
# + handle assert errors with -o option
# - fix init_gettext() when application is compiled with py2exe
# + finir traduction gettext
# + traduire codes parametres en anglais (pour avoir une config homogene)
# + decouper transfert() en plusieurs fonctions pour une utilisation plus generique
#------------------------------------------------------------------------------
#=== GETTEXT =================================================================
def init_gettext():
"""
gettext initialization, in order to translate necessary strings at runtime.
This MUST be done before any import or usage of _() around strings.
"""
# Gettext pour adapter certaines chaines de caracteres a la langue du
# systeme (traduction en anglais ou francais)
# => DOIT etre fait avant toute constante chaine _("...") et tout import
import gettext, locale, os.path
# repertoire "locale": normalement un sous-repertoire du script principal
#locale_dir = os.path.join(plx.get_main_dir(), "locale")
# locale dir is a subfolder of this script's folder:
#TODO: fix this when compiled with py2exe...
locale_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'locale')
#print 'locale_dir =', locale_dir
# initialisation de la locale (cf. doc Python): est-ce toujours necessaire ?
locale.setlocale(locale.LC_ALL, '')
# langue du systeme: 2 premieres lettres de la locale/lang
try:
lang = locale.getdefaultlocale()[0][:2]
except:
# workaround if locale cannot be determined
lang = 'en'
#print 'lang =', lang
if lang != 'fr':
# ordre prefere des langues: 1) systeme, 2) english, 3) francais
languages=[lang, 'en', 'fr']
# On charge la langue correspondante depuis le repertoire locale:
try:
gt = gettext.translation("ExeFilter", locale_dir, languages)
# puis on installe la fonction de traduction _():
gt.install(unicode=True)
except:
# en cas d'erreur, on doit quand meme definir _() comme une fonction
# "builtin" qui renvoie la chaine inchangee:
__builtins__._ = lambda text: text
else:
# si la langue est 'fr', on n'utilise aucune traduction:
gettext.NullTranslations().install()
#__builtins__._ = lambda text: text
# always init gettext first
init_gettext()
#=== IMPORTS ==================================================================
# modules standards Python:
import os, sys, time, socket, optparse, tempfile, os.path
import threading
# hack to change default encoding to Latin-1 instead of ASCII:
reload(sys)
sys.setdefaultencoding( "latin-1" )
# modules spécifiques à Windows:
if sys.platform == 'win32':
try:
import win32api , win32security
except:
raise ImportError, "the pywin32 module is not installed: "\
"see http://sourceforge.net/projects/pywin32"
# modules d'ExeFilter:
from commun import *
import Politique
import Rapport
import Journal
import Conteneur_Repertoire
import Conteneur_Fichier
import Parametres
# numero de version global (provenant de __init__.py)
from __init__ import __version__ as XF_VERSION
from __init__ import __date__ as XF_DATE
#TODO: a supprimer
import commun # (nécessaire pour certaines variables globales)
import Conteneur # pour importer la variable Conteneur.RACINE_TEMP
#=== CONSTANTES ===============================================================
REP_RAPPORT = os.path.join("log", "reports")+os.sep # log\reports\
REP_LOG = os.path.join("log", "logs")+os.sep # log\logs\
#REP_TEMP = "temp" + os.sep # temp\
REP_ARCHIVE = "archive" + os.sep # archive\
TAILLE_TEMP = 10000 # taille max répertoire temp, en Mo
TAILLE_ARCHIVE = 10000 # taille max archive, en Mo
# default exit codes for result:
EXITCODE_CLEAN = 0 # clean content
EXITCODE_BLOCKED = 1 # blocked content
EXITCODE_CLEANED = 2 # cleaned content
EXITCODE_ERROR = 3 # error during analysis
#=== VARIABLES GLOBALES =======================================================
#TODO: a supprimer pour permettre plusieurs transferts simultanés
# (or use threading.local)
nom_rapport = None
nom_journal_secu = None
nom_journal_debug = None
transfert_termine = False
# full path of the security logfile:
path_logfile = None
# full path of the debug logfile:
path_debugfile = None
# ExeFilter parameters, with default values:
parametres = {}
#--- REPERTOIRES ---
Parametres.Parametre("rep_rapports", str, nom=_(u"Répertoire des fichiers rapports"),
description=_(u"Répertoire où sont stockés tous les fichiers rapports"),
valeur_defaut = REP_RAPPORT).ajouter(parametres)
Parametres.Parametre("rep_journaux", str, nom=_(u"Répertoire des fichiers journaux"),
description=_(u"Répertoire où sont stockés tous les fichiers journaux"),
valeur_defaut = REP_LOG).ajouter(parametres)
Parametres.Parametre("rep_temp", str, nom=_(u"Répertoire des fichiers temporaires"),
description=_(u"Répertoire où sont stockés tous les fichiers temporaires"),
valeur_defaut = 'auto').ajouter(parametres)
Parametres.Parametre("rep_archives", str, nom=_(u"Répertoire des fichiers archivés"),
description=_(u"Répertoire où sont archivés tous les fichiers transférés"),
valeur_defaut = REP_ARCHIVE).ajouter(parametres)
Parametres.Parametre("taille_temp", int, nom=_(u"Taille maximale du répertoire temporaire (en octets)"),
description=_(u"Taille maximale du répertoire où sont stockés tous les fichiers temporaires"),
valeur_defaut = TAILLE_TEMP*1000000).ajouter(parametres)
Parametres.Parametre("taille_archives", int, nom=_(u"Taille maximale des archives (en octets)"),
description=_(u"Taille maximale du répertoire où sont archivés tous les fichiers transférés"),
valeur_defaut = TAILLE_ARCHIVE*1000000).ajouter(parametres)
#--- JOURNAUX ---
Parametres.Parametre("journal_securite", bool, nom=_(u"Ecrire un journal sécurité dans un fichier"),
description=_(u"Le journal sécurité décrit synthétiquement les évènements concernant la sécurité des transferts."),
valeur_defaut = False).ajouter(parametres)
Parametres.Parametre("journal_syslog", bool, nom=_(u"Envoyer un journal sécurité par syslog"),
description=_("Le journal sécurité décrit synthétiquement les évènements "
"concernant la sécurité des transferts. Syslog permet de centraliser ces "
"journaux par le réseau sur un serveur"),
valeur_defaut = False).ajouter(parametres)
Parametres.Parametre("journal_debug", bool, nom=_("Ecrire un journal technique de débogage"),
description=_("Le journal technique contient les évènements détaillés des "
"transferts, pour un débogage en cas de problème."),
valeur_defaut = False).ajouter(parametres)
Parametres.Parametre("serveur_syslog", str, nom=_("Serveur syslog (nom ou adresse IP)"),
description=_("Nom ou adresse IP du serveur syslog qui centralise les journaux sécurité."),
valeur_defaut = "localhost").ajouter(parametres)
Parametres.Parametre("port_syslog", int, nom=_("Port syslog (numéro de port UDP)"),
description=_("Numéro de port UDP du serveur syslog: 514 pour un serveur syslog standard."),
valeur_defaut = 514).ajouter(parametres)
#--- AUTRES PARAMETRES ---
Parametres.Parametre("archive_after", bool, nom=_(u"Archiver tous les fichiers apres filtrage"),
description=_(u"Pour archiver une copie de chaque fichier filtré dans un répertoire d'archivage."),
valeur_defaut = False).ajouter(parametres)
Parametres.Parametre("exitcode_clean", int, nom='Exit code when overall result is clean',
description='Exit code (errorlevel) returned by ExeFilter when all analyzed '
'files are clean. (only works with -o option for now)',
valeur_defaut = EXITCODE_CLEAN).ajouter(parametres)
Parametres.Parametre("exitcode_cleaned", int, nom='Exit code when overall result is cleaned',
description='Exit code (errorlevel) returned by ExeFilter when at least some analyzed '
'files have been cleaned. (only works with -o option for now)',
valeur_defaut = EXITCODE_CLEANED).ajouter(parametres)
Parametres.Parametre("exitcode_blocked", int, nom='Exit code when overall result is blocked',
description='Exit code (errorlevel) returned by ExeFilter when all analyzed '
'files are blocked. (only works with -o option for now)',
valeur_defaut = EXITCODE_BLOCKED).ajouter(parametres)
Parametres.Parametre("exitcode_error", int, nom='Exit code when overall result is error',
description='Exit code (errorlevel) returned by ExeFilter when an error '
'occurred during the analysis. (only works with -o option for now)',
valeur_defaut = EXITCODE_ERROR).ajouter(parametres)
#--- ANTIVIRUS ---
# ClamAV (clamd):
Parametres.Parametre("antivirus_clamd", bool, nom=_("Utiliser l'antivirus ClamAV (serveur clamd)"),
description=_("Utiliser la version serveur de l'antivirus ClamAV (clamd) "
"pour analyser les fichiers acceptes. Clamd doit tourner en "
"tant que service sur la machine locale."),
valeur_defaut = False).ajouter(parametres)
Parametres.Parametre("clamd_serveur", str, nom=_("Adresse IP ou nom du serveur antivirus clamd"),
description=_("En general le serveur clamd tourne sur la meme machine: localhost."),
valeur_defaut = 'localhost').ajouter(parametres)
Parametres.Parametre("clamd_port", int, nom=_("Port du serveur antivirus clamd"),
description=_("En general le serveur clamd tourne sur le port 3310."),
valeur_defaut = 3310).ajouter(parametres)
# F-Prot 6.x (fpscan):
Parametres.Parametre("antivirus_fpscan", bool, nom=_("Utiliser l'antivirus F-Prot 6 (fpscan)"),
description=_("Utiliser la version ligne de commande de l'antivirus F-Prot 6 "
"(fpscan) pour analyser les fichiers acceptes. Attention cela "
"peut degrader significativement les performances."),
valeur_defaut = False).ajouter(parametres)
if sys.platform == 'win32': fpscan_defaut = "c:\\Program Files\\FRISK Software\\F-PROT Antivirus for Windows\\fpscan.exe"
else: fpscan_defaut = "fpscan" # on suppose qu'il est dans le PATH
Parametres.Parametre("fpscan_executable", str, nom=_("Exécutable de l'antivirus F-Prot 6 (fpscan)"),
description=_("Emplacement du fichier fpscan de l'antivirus F-Prot 6"),
valeur_defaut = fpscan_defaut).ajouter(parametres)
# F-Prot 3.x (fpcmd, obsolete):
Parametres.Parametre("antivirus_fpcmd", bool, nom=_("Utiliser l'antivirus F-Prot 3 (fpcmd)"),
description=_("Utiliser la version ligne de commande de l'antivirus F-Prot 3 "
"(fpcmd) pour analyser les fichiers acceptes. Attention cela "
"peut degrader significativement les performances."),
valeur_defaut = False).ajouter(parametres)
#TODO: verifier le chemin par defaut + améliorer portabilité
if sys.platform == 'win32': fpcmd_defaut = "c:\\Program Files\\FRISK Software\\F-PROT Antivirus for Windows\\fpcmd.exe"
else: fpcmd_defaut = "fpcmd" # on suppose que fpcmd est dans le PATH
Parametres.Parametre("fpcmd_executable", str, nom=_("Exécutable de l'antivirus F-Prot 3 (fpcmd)"),
description=_("Emplacement du fichier fpcmd de l'antivirus F-Prot 3"),
valeur_defaut = fpcmd_defaut).ajouter(parametres)
#=== FONCTIONS ================================================================
#------------------------------------------------------------------------------
# GET_JOURNAL
#-------------------
def get_journal() :
"""
Retourne le chemin du fichier journal securite.
Si le fichier journal n'est pas encore créé, renvoie None
@return: le chemin du fichier journal
@rtype: str
"""
return path_logfile
## global nom_journal_secu
## if nom_journal_secu == None:
## #TODO: renvoyer une valeur par defaut ?
## return None
## else:
## rep_journaux = path(p.parametres['rep_journaux'].valeur)
## return (rep_journaux / nom_journal_secu).abspath()
#------------------------------------------------------------------------------
# GET_JOURNAL_DEBUG
#-------------------
def get_journal_debug() :
"""
Retourne le chemin du fichier journal de débogage.
Si le fichier journal n'est pas encore créé, renvoie None
@return: le chemin du fichier journal
@rtype: str
"""
return path_debugfile
global nom_journal_debug
if nom_journal_debug == None:
#TODO: renvoyer une valeur par defaut ?
return None
else:
rep_journaux = path(p.parametres['rep_journaux'].valeur)
return (rep_journaux / nom_journal_debug).abspath()
#------------------------------------------------------------------------------
# GET_RAPPORT
#-------------------
def get_rapport() :
"""
Retourne le chemin d'accès au fichier contenant le rapport au format html
Si le fichier du rapport n'est pas encore créé, renvoie None
@return: chemin d'accès au fichier contenant le rapport au format html
@rtype: str
"""
if nom_rapport == None:
chemin_rapport = None
else:
#chemin_rapport = REP_RAPPORT + nom_rapport
chemin_rapport = path(p.parametres['rep_rapports'].valeur) / nom_rapport
chemin_rapport = chemin_rapport.abspath()
return chemin_rapport
def display_html_report():
"""
Display HTML report in the web browser
"""
if get_rapport():
plx.display_html_file(os.path.abspath(get_rapport()+'.html'))
#------------------------------------------------------------------------------
# CANCEL_TRANSFERT
#-------------------
def cancel_transfert ():
"""
Annule le transfert en cours
"""
commun.continuer_transfert = False
#------------------------------------------------------------------------------
# GET_NB_FICHIERS
#-------------------
def get_nb_fichiers ():
"""
Retourne le nombre de fichiers à analyser ou None si le moteur
n'a pas encore commencé le transfert
@return: le nombre de fichiers à analyser
@rtype: int
"""
if commun.transfert_commence == True : return commun.nb_fichiers
else : return None
#------------------------------------------------------------------------------
# GET_COMPTEUR_AVANCEMENT
#-------------------------
def get_compteur_avancement ():
"""
Retourne le nombre fichiers déjà analyser ou None si le moteur
n'a pas encore commencé le transfert
@return: le nombre de fichiers déjà analysés
@rtype: int
"""
if commun.transfert_commence == True : return commun.compteur_avancement
else : return None
def init_archivage(politique, taille_src):
"""
Preparation du repertoire d'archivage avant transfert.
@param politique: objet Politique employe pour le transfert
@param taille_src: taille totale des fichiers source a nettoyer, en octets
"""
# si le repertoire d'archivage n'existe pas, on le cree:
chem_arc = path(politique.parametres['rep_archives'].valeur)
try:
os.makedirs(chem_arc)
except:
pass
# calcul de la taille actuelle du répertoire archivage
taille_arc = 0
for f in chem_arc.walkfiles():
taille_arc += f.size
# test si la taille des fichiers source est supérieure à celle du rép d'archivage
# si c'est le cas, on génère une exception
if taille_src > politique.parametres['taille_archives'].valeur:
msg = _(u"La taille des fichiers source est superieure a la taille du repertoire d'archivage.")
Journal.error(msg)
raise RuntimeError, msg
# boucle pour effacer les sous-rép les plus anciens dans le cas où il n'y a pas assez
# d'espace disque dans le rép archivage pour copier les fichiers source
while politique.parametres['taille_archives'].valeur < taille_arc + taille_src:
date_archive = 0
taille_rep_archive = 0
# boucle pour déterminer quel est le sous-rép archive le plus ancien
for rep in chem_arc.dirs():
if date_archive == 0:
# on récupère la date du 1er sous-rép archive lu
date_archive = os.path.getmtime(rep)
rep_archive = rep
# si la date du sous-rép archive lu est inférieure à date_archive,
# ce sous-rép devient le plus ancien
elif date_archive > os.path.getmtime(rep):
date_archive = os.path.getmtime(rep)
rep_archive = rep
if os.path.exists(rep_archive):
# calcul de la taille du sous-rép archive le plus ancien
for rep in rep_archive.walkfiles():
taille_rep_archive += rep.size
# on met à jour la taille du rép temp principal
taille_arc = taille_arc - taille_rep_archive
# on efface le sous-rép temp le plus ancien
rep_archive.rmtree()
else:
# s'il n'y a plus de sous-rép archive à effacer, on génère une exception
msg = _(u"repertoire d'archivage deja vide => taille source trop grande")
Journal.error(msg)
raise RuntimeError, msg
#------------------------------------------------------------------------------
# TRANSFERT
#-------------------
# lock for multithreading
lock_transfert = threading.Lock()
def transfert(liste_source, destination, type_transfert="entree", handle=None,
pol=None, dest_is_a_file=False, force_extension=None,
logfile=None):
"""
Main function to scan or clean files and directories.
(thread safe: uses a lock to make sure only one thread runs it)
Lance le transfert et l'analyse des répertoires et/ou fichiers source
@param liste_source: la liste des sources à transférer
@type liste_source: list
@param destination: destination directory, or None/'' for scan-only mode
@type destination: str
@param type_transfert: le type de transfert pour charger la politique de filtre
@type type_transfert: str
@param handle: le handle de connexion a utiliser pour exécuter le filtre
@type handle: pyHandle
#@param taille_temp: taille maximale du répertoire temporaire, en octets. Taille par défaut 10Go (DVD double couche)
#@type taille_temp: int
@param dest_is_a_file: False if destination is a dir (default),
True if it's a filename.
@type dest_is_a_file: bool
@param force_extension: if set, force filename extension to a specific value
(used to control which filters are applied)
Note: force_extension may be "" or must start with a dot
@type force_extension: str, unicode
"""
# run the function with a lock to make sure only one thread can run it at a
# time:
with lock_transfert:
exitcode = _transfert_not_threadsafe(liste_source, destination,
type_transfert, handle, pol, dest_is_a_file, force_extension,
logfile)
return exitcode
def _transfert_not_threadsafe(liste_source, destination, type_transfert="entree", handle=None,
pol=None, dest_is_a_file=False, force_extension=None,
logfile=None):
"""
Main function to scan or clean files and directories.
Not thread safe: should not be called directly, use transfert() instead.
"""
global nom_journal_secu
global path_logfile
global nom_journal_debug
global path_debugfile
global nom_rapport
global p
# obsolete, to be removed?
if sys.platform == 'win32':
if handle != None :
win32security.ImpersonateLoggedOnUser(handle)
taille_src = 0
# on récupère le nom de l'utilisateur qui lance ExeFilter, avec nom de
# domaine (ou de machine) sous Windows:
try:
username = get_username()
username_withdomain = get_username(with_domain=True)
except:
# workaround if user name cannot be determined
username = username_withdomain = 'unknown'
# création du tronc commun pour les noms des journaux et des rapports:
nom_machine = socket.gethostname()
date = time.strftime("%Y-%m-%d", time.localtime())
heure = time.strftime("%Hh%Mm%Ss", time.localtime())
nom_commun = date + "_" + nom_machine + "_" + username + "_" + heure
# on transmet à transfert :
# soit un objet Politique déjà configuré,
# soit un nom de fichier de config directement,
# soit une liste de fichiers de config,
# soit un mot-clé " entree " ou " sortie " décrivant le type de transfert,
# afin de conserver la compatibilité avec l'IHM actuelle. Dans ce cas le fichier de config filtres.cfg doit être analysé correctement.
if pol != None:
if isinstance(pol, Politique.Politique):
p = pol
elif isinstance (pol, [file, str, unicode, list]):
p = Politique.Politique(pol)
# obsolete, to be removed?
elif type_transfert in ("entree", "sortie"):
# vérifier si le fichier existe
# si le fichier existe alors on le charge
# sinon politque par défaut
# p = Politique.Politique("politique_%s.cfg" % type_transfert)
p = Politique.Politique()
else:
# politique par défaut
p = Politique.Politique()
# store policy in a global variable: to be removed for multithreading
commun.politique = p
# nom des fichiers log = nom de la machine + date et heure du transfert
if logfile == 'auto':
print 'logfile=auto'
# generate log filename automatically (one per session):
nom_journal_secu = nom_commun + ".log"
# set full path in logs folder:
logs_folder = path(p.parametres['rep_journaux'].valeur)
# make sure the logs folder exists (if logging to file is enabled):
if (not os.path.exists(logs_folder)) and p.parametres['journal_securite'].valeur:
logs_folder.makedirs()
path_logfile = (logs_folder / nom_journal_secu).abspath()
elif logfile:
print 'logfile=%s' % logfile
# use provided log filename:
nom_journal_secu = logfile
# set full path directly:
path_logfile = path(nom_journal_secu).abspath()
else:
nom_journal_secu = None
path_logfile = None
# debug logfile: only handled via policy, auto name
nom_journal_debug = "debug_" + nom_commun + ".log"
# set full path in logs folder:
logs_folder = path(p.parametres['rep_journaux'].valeur)
# make sure the logs folder exists (if logging to file is enabled):
if (not os.path.exists(logs_folder)) and p.parametres['journal_debug'].valeur:
logs_folder.makedirs()
path_debugfile = (logs_folder / nom_journal_debug).abspath()
# création du journal d'évènements:
Journal.init_journal(p, journal_secu = path_logfile, journal_debug = path_debugfile)
# création des sous-répertoires temp et archivage:
commun.sous_rep_archive = "transfert_" + nom_commun
Journal.important(_(u"ExeFilter v%s lancé par utilisateur %s sur la machine %s") %
(XF_VERSION, username_withdomain, nom_machine))
# on ajoute la politique dans le journal:
p.journaliser()
Journal.info2(_(u"Début de l'analyse"))
Rapport.liste_resultats = []
# liste des répertoires et/ou fichiers source
liste_conteneurs_source = []
# initialisation des variables globales
commun.nb_fichiers = commun.compteur_avancement = 0
commun.continuer_transfert = True
commun.transfert_commence = False
# if destination is a single file, check if source is one file:
if dest_is_a_file:
assert(len(liste_source)==1)
assert(os.path.isfile(liste_source[0]))
# if destination is empty (None or ''), set mode to scan-only instead of clean:
if destination:
commun.clean_mode = True
Journal.debug('Analysis mode: clean')
else:
commun.clean_mode = False
Journal.debug('Analysis mode: scan only')
Journal.debug('clean_mode=%s' % commun.clean_mode)
# boucle pour lire chaque répertoire et/ou fichier contenu dans la liste
for source in liste_source :
# on vérifie le type de source: répertoire ou fichier ?
if os.path.isdir(source):
#rep_source = Conteneur_Repertoire.Conteneur_Repertoire (source, destination)
# si source est G:/tutu/tata, rep_relatif_source = tata
(head, tail) = os.path.split(source)
rep_relatif_source = tail
rep_source = Conteneur_Repertoire.Conteneur_Repertoire (source,
destination, rep_relatif_source, politique=p)
# calcul de la taille du répertoire source
taille_src += rep_source.compter_taille_rep()
else:
#rep_source = Conteneur_Fichier.Conteneur_Fichier (source, destination)
rep_relatif_source = ""
rep_source = Conteneur_Fichier.Conteneur_Fichier (source,
destination, rep_relatif_source, politique=p,
dest_is_a_file=dest_is_a_file, force_extension=force_extension)
# calcul de la taille du fichier source
taille_src += os.stat(source).st_size
# on ajoute les conteneurs source à la liste
liste_conteneurs_source.append(rep_source)
# on incrémente le compteur nombre de fichiers total
commun.nb_fichiers += rep_source.compter_nb_fichiers()
# test si la taille des fichiers source est supérieure à celle du rép temp
# si c'est le cas, on génère une exception
#if taille_src > taille_temp:
if taille_src > p.parametres['taille_temp'].valeur:
msg = "La taille des fichiers source est superieure a la taille du repertoire temporaire."
Journal.error(msg)
raise RuntimeError, msg
# initialisation de l'archivage:
if parametres['archive_after'].valeur:
init_archivage(p, taille_src)
commun.transfert_commence = True
# boucle d'analyse de chaque conteneur source contenu dans la liste
for conteneur_source in liste_conteneurs_source:
Journal.info2(u"Analyse de contenu de %s ..." % conteneur_source)
# ici il faudrait un try pour gérer toutes les exceptions, journaliser
# et nettoyer s'il y a des erreurs.
# ou alors lancer un thread...
# test de l'interruption de transfert par l'utilisateur
if commun.continuer_transfert == True:
# s'il n'y a pas d'interruption, on lance le nettoyage
liste_resultat = conteneur_source.nettoyer(p)
# s'il y a eu une interruption pendant nettoyer(), on s'arrête
if commun.continuer_transfert == False:
break
# génération du rapport
nom_rapport = "rapport_" + nom_commun
Journal.info2(u"Génération du rapport: %s ..." % nom_rapport)
chemin_rapport = p.parametres['rep_rapports'].valeur + nom_rapport
resume = Rapport.generer_rapport(chemin_rapport,
', '.join(liste_source), destination ,
XF_VERSION, XF_DATE, commun.continuer_transfert)
Journal.info2(u"Fin de l'analyse")
# log du résumé de la dépollution
Journal.important(_(u'Résumé : %d fichiers analysés ; '
u'%d fichiers acceptés ; %d fichiers nettoyés ; %d fichiers refusés ; %d erreurs')
% (resume[0], resume[1], resume[2], resume[3], resume[4]))
if commun.continuer_transfert == False:
Journal.warning(u"TRANSFERT ANNULE par l'utilisateur")
# return exit code according to results:
#TODO: use results from containers instead of this quick hack:
clean = resume[1]
cleaned = resume[2]
blocked = resume[3]
errors = resume[4]
if errors:
exitcode = p.parametres['exitcode_error'].valeur
elif (clean+cleaned>0) and (cleaned+blocked>0):
exitcode = p.parametres['exitcode_cleaned'].valeur
elif (clean>0) and (cleaned+blocked == 0):
exitcode = p.parametres['exitcode_clean'].valeur
elif (blocked>0) and (clean+cleaned == 0):
exitcode = p.parametres['exitcode_blocked'].valeur
else:
raise ValueError, 'Summary values look wrong...'
Journal.debug('Exit code: %d' % exitcode)
Journal.fermer_journal()
return exitcode
#return
def clean_dir(source_dir, dest_dir, policy=None, logfile=None):
"""
Clean all files of a directory (source_dir) according to the specified
policy, store the result in dest_dir.
Optionally write log to logfile.
Return an exit code according to the overall result (see doc).
"""
# TODO: force logfile option in policy if logfile is set
exitcode = transfert([source_dir], dest_dir, pol=policy, logfile=logfile)
return exitcode
def scan_dir(source_dir, policy=None, logfile=None):
"""
Scan all files of a directory (source_dir) according to the specified
policy.
Optionally write log to logfile.
Return an exit code according to the overall result (see doc).
"""
exitcode = transfert([source_dir], None, pol=policy, logfile=logfile)
return exitcode
def clean_file(source_file, dest_file, policy=None, logfile=None):
"""
Clean a single file (source_file) according to the specified policy,
store the result in dest_file.
Optionally write log to logfile.
Return an exit code according to the overall result (see doc).
"""
exitcode = transfert([source_file], dest_file, pol=policy, logfile=logfile,
dest_is_a_file=True)
return exitcode
def clean_file_to_dir(source_file, dest_dir, policy=None, logfile=None):
"""
Clean a single file (source_file) according to the specified policy,
store the result in a file with the same name in a destination directory
(dest_dir).
Optionally write log to logfile.
Return an exit code according to the overall result (see doc).
"""
exitcode = transfert([source_file], dest_dir, pol=policy, logfile=logfile)
return exitcode
def scan_file(source_file, policy=None, logfile=None):
"""
Scan a single file (source_file) according to the specified policy.
Optionally write log to logfile.
Return an exit code according to the overall result (see doc).
"""
exitcode = transfert([source_file], None, pol=policy, logfile=logfile)
return exitcode
# mapping from content-types to extensions corresponding to ExeFilter filters:
# (sources http://en.wikipedia.org/wiki/Internet_media_type,
# http://www.iana.org/assignments/media-types/index.html
# and http://www.w3schools.com/media/media_mimeref.asp)
# Note: we could have used mimetypes.guess_extension, but it does not work very
# well, for example for 'text/plain' it returns '.ksh'...
CT_to_ext = {
'application/msword': '.doc',
'application/pdf': '.pdf',
'application/rtf': '.rtf',
'application/soap+xml': '.xml',
'application/vnd.ms-excel': '.xls',
'application/vnd.ms-powerpoint': '.ppt',
#TODO: add also the macro enabled versions from IANA, just in case
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
'application/vnd.openxmlformats-officedocument.presentationml.presentation': '.pptx',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
'application/xhtml+xml': '.html',
'application/xml': '.xml',
'application/zip': '.zip',
'audio/mpeg': '.mp3',
'audio/vnd.wave': '.wav',
'image/gif': '.gif',
'image/jpeg': '.jpg',
'image/png': '.png',
'image/x-ms-bmp': '.bmp',
'message/rfc822': '.eml',
'text/html': '.html',
'text/plain': '.txt',
'text/xml': '.xml',
'video/x-msvideo': '.avi',
}
def scan_string(data, filename=None, content_type=None, policy=None, logfile=None):
"""
Scan a string according to the specified policy. The filename and/or the
content-type may be provided (at least one of them is required).
It will be used to select the right filter(s).
If only the content-type is provided, a default file extension will be used.
If both filename and content-type are given, content-type is ignored.
The string will be written to a temporary file for analysis.
NOTE: this is a quick and dirty solution which will be improved in future
versions
Optionally write log to logfile.
Return an exit code according to the overall result (see doc).
"""
if filename is not None:
# only keep the filename, in case it contains a path with directories:
# (to avoid directory traversal and other issues)
fname = os.path.basename(filename)
#ext = os.path.splitext(filename)[1]
elif content_type is not None:
if content_type not in CT_to_ext:
#TODO: here we should return a "blocked" result rather than an exception
raise ValueError, 'Content-type not allowed: "%s"' % content_type
ext = CT_to_ext[content_type]
# use a temporary filename = temp.ext
fname = 'temp'+ext
else:
raise RuntimeError, 'Either filename or content-type is required'
# create a new temporary dir to store the file with its original filename
tempdir = tempfilemgr.newTempDir()
fpath = os.path.join(tempdir, fname)
#f, fname = tempfilemgr.newTempFile(suffix=ext)
f = open(fpath, 'wb')
f.write(data)
f.close()
exitcode = transfert([fpath], None, pol=policy, logfile=logfile)
# remove temp file and dir:
os.remove(fpath)
os.rmdir(tempdir)
return exitcode
def clean_string(data, filename=None, content_type=None, policy=None, logfile=None):
"""
Clean a string according to the specified policy. The filename and/or the
content-type may be provided (at least one of them is required).
It will be used to select the right filter(s).
If only the content-type is provided, a default file extension will be used.
If both filename and content-type are given, content-type is ignored.
The string will be written to a temporary file for analysis.
NOTE: this is a quick and dirty solution which will be improved in future
versions
Optionally write log to logfile.
Return a tuple (exitcode, cleaned_data):
- exit code according to the overall result (see doc).
"""
if filename is not None:
# only keep the filename, in case it contains a path with directories:
# (to avoid directory traversal and other issues)
fname = os.path.basename(filename)
#ext = os.path.splitext(filename)[1]
elif content_type is not None:
if content_type not in CT_to_ext:
#TODO: here we should return a "blocked" result rather than an exception
raise ValueError, 'Content-type not allowed: "%s"' % content_type
ext = CT_to_ext[content_type]
# use a temporary filename = temp.ext
fname = 'temp'+ext
else:
raise RuntimeError, 'Either filename or content-type is required'
# create a new temporary dir to store the file with its original filename
tempdir = tempfilemgr.newTempDir()
fpath = os.path.join(tempdir, fname)
f = open(fpath, 'wb')
f.write(data)
f.close()
# temp file for the cleaned destination:
f, fdest = tempfilemgr.newTempFile()
f.close()
exitcode = transfert([fpath], fdest, pol=policy, logfile=logfile,
dest_is_a_file=True)
cleaned_data = open(fdest, 'rb').read()
# remove temp files and dir:
os.remove(fdest)
os.remove(fpath)
os.rmdir(tempdir)
return (exitcode, cleaned_data)
#==============================================================================
# PROGRAMME PRINCIPAL
#=====================
# ne sert que si on appelle le module ExeFilter.py directement, sans passer par
# le module go.py qui lance la méthode transfert du module ExeFilter.py dans un
# thread.
if __name__ == '__main__':
# si compilation py2exe, il faut fixer ici le codec par défaut, car il n'y