From 7bafff3699942898e9a7ebdea0b46c3d2e4c6ee3 Mon Sep 17 00:00:00 2001 From: Dan Puttick Date: Fri, 16 Dec 2016 13:55:15 -0500 Subject: [PATCH 01/22] Fixed several small filecheck.py bugs for python3 compat --- bin/filecheck.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bin/filecheck.py b/bin/filecheck.py index 1d3873c..14fe1a4 100644 --- a/bin/filecheck.py +++ b/bin/filecheck.py @@ -241,7 +241,7 @@ class KittenGroomerFileCheck(KittenGroomerBase): def inode(self): ''' Usually empty file. No reason (?) to copy it on the dest key''' if self.cur_file.is_symlink(): - self.cur_file.log_string += 'Symlink to {}'.format(self.log_details['symlink']) + self.cur_file.log_string += 'Symlink to {}'.format(self.cur_file.log_details['symlink']) else: self.cur_file.log_string += 'Inode file' @@ -390,19 +390,19 @@ class KittenGroomerFileCheck(KittenGroomerBase): xmlDoc = PDFiD(self.cur_file.src_path) oPDFiD = cPDFiD(xmlDoc, True) # TODO: other keywords? - if oPDFiD.encrypt > 0: + if oPDFiD.encrypt.count > 0: self.cur_file.add_log_details('encrypted', True) self.cur_file.make_dangerous() - if oPDFiD.js > 0 or oPDFiD.javascript > 0: + if oPDFiD.js.count > 0 or oPDFiD.javascript.count > 0: self.cur_file.add_log_details('javascript', True) self.cur_file.make_dangerous() - if oPDFiD.aa > 0 or oPDFiD.openaction > 0: + if oPDFiD.aa.count > 0 or oPDFiD.openaction.count > 0: self.cur_file.add_log_details('openaction', True) self.cur_file.make_dangerous() - if oPDFiD.richmedia > 0: + if oPDFiD.richmedia.count > 0: self.cur_file.add_log_details('flash', True) self.cur_file.make_dangerous() - if oPDFiD.launch > 0: + if oPDFiD.launch.count > 0: self.cur_file.add_log_details('launch', True) self.cur_file.make_dangerous() @@ -459,7 +459,7 @@ class KittenGroomerFileCheck(KittenGroomerBase): # Exifreader truncates data. if len(printable) > 25 and printable.endswith(", ... ]"): value = tags[tag].values - if isinstance(value, basestring): + if isinstance(value, str): printable = value else: printable = str(value) From cb4ae5deec5e45704758851a055243f6e5b830ec Mon Sep 17 00:00:00 2001 From: Dan Puttick Date: Fri, 16 Dec 2016 14:09:13 -0500 Subject: [PATCH 02/22] Changing travis and tests to run filecheck in Python 3 --- .travis.yml | 6 ++---- tests/test_examples.py | 6 +----- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index fc6e793..3978062 100644 --- a/.travis.yml +++ b/.travis.yml @@ -47,11 +47,9 @@ install: - unzip pdfid_v0_2_1.zip - pip install -U pip - pip install lxml exifread pillow + - pip install olefile + - pip install git+https://github.com/decalage2/oletools.git - pip install git+https://github.com/Rafiot/officedissector.git - - | - if [[ "$TRAVIS_PYTHON_VERSION" == 2* ]]; then - pip install -U oletools olefile - fi # Module dependencies - pip install -r dev-requirements.txt - pip install coveralls codecov diff --git a/tests/test_examples.py b/tests/test_examples.py index 79d9efb..38d6784 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -9,9 +9,7 @@ import pytest from bin.specific import KittenGroomerSpec from bin.pier9 import KittenGroomerPier9 from bin.generic import KittenGroomer - -if sys.version_info.major == 2: - from bin.filecheck import KittenGroomerFileCheck +from bin.filecheck import KittenGroomerFileCheck skip = pytest.mark.skip @@ -64,14 +62,12 @@ def test_generic_2(src_complex, dst): dump_logs(spec) -@py2_only def test_filecheck(src_complex, dst): spec = KittenGroomerFileCheck(src_complex, dst, debug=True) spec.processdir() dump_logs(spec) -@py2_only def test_filecheck_2(src_simple, dst): spec = KittenGroomerFileCheck(src_simple, dst, debug=True) spec.processdir() From 09973488eb9d322072eee67e85b3c7159db6a279 Mon Sep 17 00:00:00 2001 From: Dan Puttick Date: Wed, 14 Dec 2016 16:32:58 -0500 Subject: [PATCH 03/22] Added small comment to helpers.py --- kittengroomer/helpers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/kittengroomer/helpers.py b/kittengroomer/helpers.py index 990f899..9604c49 100644 --- a/kittengroomer/helpers.py +++ b/kittengroomer/helpers.py @@ -55,6 +55,7 @@ class FileBase(object): else: try: mt = magic.from_file(self.src_path, mime=True) + # magic will always return something, even if it's just 'data' except UnicodeEncodeError as e: # FIXME: The encoding of the file is broken (possibly UTF-16) mt = '' From cef4223c53602eda2deee00e93069a246322bcf8 Mon Sep 17 00:00:00 2001 From: Dan Puttick Date: Fri, 16 Dec 2016 17:36:21 -0500 Subject: [PATCH 04/22] Deleting old tests, minor test reorganization. --- setup.py | 7 +- tests/oldtests.py | 95 -------------------- tests/{test_examples.py => test_binaries.py} | 0 tests/test_integration.py | 25 ------ 4 files changed, 6 insertions(+), 121 deletions(-) delete mode 100755 tests/oldtests.py rename tests/{test_examples.py => test_binaries.py} (100%) delete mode 100644 tests/test_integration.py diff --git a/setup.py b/setup.py index d30afac..e327b7f 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,12 @@ setup( url='https://github.com/CIRCL/CIRCLean', description='Standalone CIRCLean/KittenGroomer code.', packages=['kittengroomer'], - scripts=['bin/generic.py', 'bin/pier9.py', 'bin/specific.py', 'bin/filecheck.py'], + scripts=[ + 'bin/generic.py', + 'bin/pier9.py', + 'bin/specific.py', + 'bin/filecheck.py' + ], include_package_data=True, package_data={'data': ['PDFA_def.ps', 'srgb.icc']}, test_suite="tests", diff --git a/tests/oldtests.py b/tests/oldtests.py deleted file mode 100755 index 888d3de..0000000 --- a/tests/oldtests.py +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -import unittest -import os -import sys - -if __name__ == '__main__': - sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), os.pardir)) - -from bin.specific import KittenGroomerSpec -from bin.pier9 import KittenGroomerPier9 -from bin.generic import KittenGroomer - -if sys.version_info.major == 2: - from bin.filecheck import KittenGroomerFileCheck - -from kittengroomer import FileBase - - -class TestBasic(unittest.TestCase): - - def setUp(self): - self.maxDiff = None - self.curpath = os.getcwd() - - def dump_logs(self, kg): - print(open(kg.log_processing, 'rb').read()) - if kg.debug: - if os.path.exists(kg.log_debug_err): - print(open(kg.log_debug_err, 'rb').read()) - if os.path.exists(kg.log_debug_out): - print(open(kg.log_debug_out, 'rb').read()) - - def test_specific_valid(self): - src = os.path.join(self.curpath, 'tests/src2') - dst = os.path.join(self.curpath, 'tests/dst') - spec = KittenGroomerSpec(src, dst, debug=True) - spec.processdir() - self.dump_logs(spec) - - def test_specific_invalid(self): - src = os.path.join(self.curpath, 'tests/src') - dst = os.path.join(self.curpath, 'tests/dst') - spec = KittenGroomerSpec(src, dst, debug=True) - spec.processdir() - self.dump_logs(spec) - - def test_pier9(self): - src = os.path.join(self.curpath, 'tests/src') - dst = os.path.join(self.curpath, 'tests/dst') - spec = KittenGroomerPier9(src, dst, debug=True) - spec.processdir() - self.dump_logs(spec) - - def test_generic(self): - src = os.path.join(self.curpath, 'tests/src2') - dst = os.path.join(self.curpath, 'tests/dst') - spec = KittenGroomer(src, dst, debug=True) - spec.processdir() - self.dump_logs(spec) - - def test_generic_2(self): - src = os.path.join(self.curpath, 'tests/src') - dst = os.path.join(self.curpath, 'tests/dst') - spec = KittenGroomer(src, dst, debug=True) - spec.processdir() - self.dump_logs(spec) - - def test_filecheck(self): - if sys.version_info.major >= 3: - return - src = os.path.join(self.curpath, 'tests/src') - dst = os.path.join(self.curpath, 'tests/dst') - spec = KittenGroomerFileCheck(src, dst, debug=True) - spec.processdir() - self.dump_logs(spec) - - def test_filecheck_2(self): - if sys.version_info.major >= 3: - return - src = os.path.join(self.curpath, 'tests/src2') - dst = os.path.join(self.curpath, 'tests/dst') - spec = KittenGroomerFileCheck(src, dst, debug=True) - spec.processdir() - self.dump_logs(spec) - - def test_help_file(self): - f = FileBase('tests/src/blah.conf', 'tests/dst/blah.conf') - f.make_unknown() - f.make_binary() - f.make_unknown() - f.make_dangerous() - f.make_binary() - f.make_dangerous() diff --git a/tests/test_examples.py b/tests/test_binaries.py similarity index 100% rename from tests/test_examples.py rename to tests/test_binaries.py diff --git a/tests/test_integration.py b/tests/test_integration.py deleted file mode 100644 index 72adafb..0000000 --- a/tests/test_integration.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -import os - -import kittengroomer as kg -import bin.specific as specific - -PATH = os.getcwd() + '/tests/' - - -def test_base(): - assert kg.FileBase - assert kg.KittenGroomerBase - assert kg.main - - -def test_help_file(): - f = kg.FileBase('tests/src_complex/blah.conf', 'tests/dst/blah.conf') - f.make_unknown() - f.make_binary() - f.make_unknown() - f.make_dangerous() - f.make_binary() - f.make_dangerous() From ecb4f5671013423902cd2ecf8afde56d1f99129d Mon Sep 17 00:00:00 2001 From: Dan Puttick Date: Fri, 16 Dec 2016 17:02:34 -0500 Subject: [PATCH 05/22] Small fixes to bin/README.md --- bin/README.md | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/bin/README.md b/bin/README.md index f37894e..28edb2e 100644 --- a/bin/README.md +++ b/bin/README.md @@ -1,13 +1,12 @@ -Example scripts -=============== +Examples +======== -These are a series of example scripts designed to demonstrate PyCIRCLean's capabilities. Feel free to -adapt or modify any of them to suit your requirements. In order to use any of these scripts, you will need to -install the PyCIRCLean dependencies (preferably in a virtualenv): +These are several sanitizers that demonstrate PyCIRCLean's capabilities. Feel free to +adapt or modify any of them to suit your requirements. In order to use any of these scripts, +you will first need to install the PyCIRCLean dependencies (preferably in a virtualenv): ``` - pip install git+https://github.com/ahupp/python-magic.git # we cannot use the PyPi package for now due to a bug - python setup.py install # from the root of the repository + pip install . ``` Requirements per script @@ -16,23 +15,22 @@ Requirements per script filecheck.py ------------ -*WARNING*: Only works with Python 2.7 (oletools and olefile aren't ported to Python3 for now) - Requirements by type of document: * Microsoft office: oletools, olefile * OOXML: officedissector * PDF: pdfid * Archives: p7zip-full, p7zip-rar +* Metadata: exifread +* Images: pillow ``` sudo apt-get install p7zip-full p7zip-rar libxml2-dev libxslt1-dev - pip install lxml officedissector git+https://github.com/ahupp/python-magic.git oletools olefile + pip install lxml oletools olefile pillow exifread pip install git+https://github.com/Rafiot/officedissector.git # pdfid is not a package, installing manually wget https://didierstevens.com/files/software/pdfid_v0_2_1.zip unzip pdfid_v0_2_1.zip - python setup.py -q install ``` generic.py From 173a844b692aedab09473a1f2713fd99a3377ce9 Mon Sep 17 00:00:00 2001 From: Dan Puttick Date: Fri, 16 Dec 2016 17:18:53 -0500 Subject: [PATCH 06/22] Some reorganization of filecheck.py, adding docstrings --- bin/filecheck.py | 201 ++++++++++++++++++++++++----------------------- 1 file changed, 103 insertions(+), 98 deletions(-) diff --git a/bin/filecheck.py b/bin/filecheck.py index 14fe1a4..e9798b3 100644 --- a/bin/filecheck.py +++ b/bin/filecheck.py @@ -1,7 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- import os -import sys import mimetypes import shlex import subprocess @@ -21,8 +20,7 @@ from pdfid import PDFiD, cPDFiD from kittengroomer import FileBase, KittenGroomerBase, main -SEVENZ = '/usr/bin/7z' -PY3 = sys.version_info.major == 3 +SEVENZ_PATH = '/usr/bin/7z' # Prepare application/ @@ -41,7 +39,7 @@ mimes_data = ['octet-stream'] mimes_exif = ['image/jpeg', 'image/tiff'] mimes_png = ['image/png'] -# Mime types we can pull metadata from +# Mimetypes we can pull metadata from mimes_metadata = ['image/jpeg', 'image/tiff', 'image/png'] # Aliases @@ -62,7 +60,7 @@ propertype = {'.gz': 'application/gzip'} # Commonly used malicious extensions # Sources: http://www.howtogeek.com/137270/50-file-extensions-that-are-potentially-dangerous-on-windows/ # https://github.com/wiregit/wirecode/blob/master/components/core-settings/src/main/java/org/limewire/core/settings/FilterSettings.java -mal_ext = ( +MAL_EXTS = ( # Applications ".exe", ".pif", ".application", ".gadget", ".msi", ".msp", ".com", ".scr", ".hta", ".cpl", ".msc", ".jar", @@ -86,55 +84,58 @@ mal_ext = ( class File(FileBase): def __init__(self, src_path, dst_path): - ''' Init file object, set the mimetype ''' super(File, self).__init__(src_path, dst_path) - self.is_recursive = False - if not self.has_mimetype(): - # No mimetype, should not happen. - self.make_dangerous() - - if not self.has_extension(): - self.make_dangerous() - - if self.extension in mal_ext: - self.log_details.update({'malicious_extension': self.extension}) - self.make_dangerous() - + self._check_dangerous() if self.is_dangerous(): return self.log_details.update({'maintype': self.main_type, 'subtype': self.sub_type, 'extension': self.extension}) + self._check_extension() + self._check_mime() - # Check correlation known extension => actual mime type + def _check_dangerous(self): + if not self.has_mimetype(): + # No mimetype, should not happen. + self.make_dangerous() + if not self.has_extension(): + self.make_dangerous() + if self.extension in MAL_EXTS: + self.log_details.update({'malicious_extension': self.extension}) + self.make_dangerous() + + def _check_extension(self): + """Guesses the file's mimetype based on its extension. If the file's + mimetype (as determined by libmagic) is contained in the mimetype + module's list of valid mimetypes and the expected mimetype based on its + extension differs from the mimetype determined by libmagic, then it + marks the file as dangerous.""" if propertype.get(self.extension) is not None: expected_mimetype = propertype.get(self.extension) else: expected_mimetype, encoding = mimetypes.guess_type(self.src_path, strict=False) if aliases.get(expected_mimetype) is not None: expected_mimetype = aliases.get(expected_mimetype) - is_known_extension = self.extension in mimetypes.types_map.keys() if is_known_extension and expected_mimetype != self.mimetype: self.log_details.update({'expected_mimetype': expected_mimetype}) self.make_dangerous() - # check correlation actual mime type => known extensions + def _check_mime(self): + """Takes the mimetype (as determined by libmagic) and determines + whether the list of extensions that are normally associated with + that extension contains the file's actual extension.""" if aliases.get(self.mimetype) is not None: mimetype = aliases.get(self.mimetype) else: mimetype = self.mimetype - expected_extensions = mimetypes.guess_all_extensions(mimetype, strict=False) if expected_extensions: if len(self.extension) > 0 and self.extension not in expected_extensions: self.log_details.update({'expected_extensions': expected_extensions}) self.make_dangerous() - else: - # there are no known extensions associated to this mimetype. - pass def has_metadata(self): if self.mimetype in mimes_metadata: @@ -144,18 +145,14 @@ class File(FileBase): class KittenGroomerFileCheck(KittenGroomerBase): - def __init__(self, root_src=None, root_dst=None, max_recursive=2, debug=False): - ''' - Initialize the basics of the conversion process - ''' + def __init__(self, root_src=None, root_dst=None, max_recursive_depth=2, debug=False): if root_src is None: root_src = os.path.join(os.sep, 'media', 'src') if root_dst is None: root_dst = os.path.join(os.sep, 'media', 'dst') super(KittenGroomerFileCheck, self).__init__(root_src, root_dst, debug) - - self.recursive = 0 - self.max_recursive = max_recursive + self.recursive_archive_depth = 0 + self.max_recursive_depth = max_recursive_depth subtypes_apps = [ (mimes_office, self._winoffice), @@ -189,21 +186,17 @@ class KittenGroomerFileCheck(KittenGroomerBase): 'inode': self.inode, } - # ##### Helpers ##### + # ##### Helper functions ##### def _init_subtypes_application(self, subtypes_application): - ''' - Create the Dict to pick the right function based on the sub mime type - ''' - to_return = {} - for list_subtypes, fct in subtypes_application: + """Creates a dictionary with the right method based on the sub mime type.""" + subtype_dict = {} + for list_subtypes, func in subtypes_application: for st in list_subtypes: - to_return[st] = fct - return to_return + subtype_dict[st] = func + return subtype_dict def _print_log(self): - ''' - Print the logs related to the current file being processed - ''' + """Print the logs related to the current file being processed.""" tmp_log = self.log_name.fields(**self.cur_file.log_details) if self.cur_file.is_dangerous(): tmp_log.warning(self.cur_file.log_string) @@ -212,13 +205,13 @@ class KittenGroomerFileCheck(KittenGroomerBase): else: tmp_log.debug(self.cur_file.log_string) - def _run_process(self, command_line, timeout=0, background=False): - '''Run subprocess, wait until it finishes''' + def _run_process(self, command_string, timeout=0, background=False): + """Run command_string in a subprocess, wait until it finishes.""" if timeout != 0: deadline = time.time() + timeout else: deadline = None - args = shlex.split(command_line) + args = shlex.split(command_string) with open(self.log_debug_err, 'ab') as stderr, open(self.log_debug_out, 'ab') as stdout: p = subprocess.Popen(args, stdout=stdout, stderr=stderr) if background: @@ -236,42 +229,42 @@ class KittenGroomerFileCheck(KittenGroomerBase): return True ####################### - - # ##### Discarded mime types, reason in the comments ###### + # ##### Discarded mimetypes, reason in the docstring ###### def inode(self): - ''' Usually empty file. No reason (?) to copy it on the dest key''' + """Empty file or symlink.""" if self.cur_file.is_symlink(): self.cur_file.log_string += 'Symlink to {}'.format(self.cur_file.log_details['symlink']) else: self.cur_file.log_string += 'Inode file' def unknown(self): - ''' This main type is unknown, that should not happen ''' + """Main type should never be unknown.""" self.cur_file.log_string += 'Unknown file' def example(self): - '''Used in examples, should never be returned by libmagic''' + """Used in examples, should never be returned by libmagic.""" self.cur_file.log_string += 'Example file' def multipart(self): - '''Used in web apps, should never be returned by libmagic''' + """Used in web apps, should never be returned by libmagic""" self.cur_file.log_string += 'Multipart file' - # ##### Threated as malicious, no reason to have it on a USB key ###### + # ##### Treated as malicious, no reason to have it on a USB key ###### def message(self): - '''Way to process message file''' + """Process a message file.""" self.cur_file.log_string += 'Message file' self.cur_file.make_dangerous() self._safe_copy() def model(self): - '''Way to process model file''' + """Process a model file.""" self.cur_file.log_string += 'Model file' self.cur_file.make_dangerous() self._safe_copy() - # ##### Converted ###### + # ##### Files that will be converted ###### def text(self): + """Process an rtf, ooxml, or plaintext file.""" for r in mimes_rtf: if r in self.cur_file.sub_type: self.cur_file.log_string += 'Rich Text file' @@ -289,7 +282,7 @@ class KittenGroomerFileCheck(KittenGroomerBase): self._safe_copy() def application(self): - ''' Everything can be there, using the subtype to decide ''' + """Processes an application specific file according to its subtype.""" for subtype, fct in self.subtypes_application.items(): if subtype in self.cur_file.sub_type: fct() @@ -299,12 +292,13 @@ class KittenGroomerFileCheck(KittenGroomerBase): self._unknown_app() def _executables(self): - '''Way to process executable file''' + """Processes an executable file.""" self.cur_file.add_log_details('processing_type', 'executable') self.cur_file.make_dangerous() self._safe_copy() def _winoffice(self): + """Processes a winoffice file using olefile/oletools.""" self.cur_file.add_log_details('processing_type', 'WinOffice') # Try as if it is a valid document oid = oletools.oleid.OleID(self.cur_file.src_path) @@ -343,6 +337,7 @@ class KittenGroomerFileCheck(KittenGroomerBase): self._safe_copy() def _ooxml(self): + """Processes an ooxml file.""" self.cur_file.add_log_details('processing_type', 'ooxml') try: doc = officedissector.doc.Document(self.cur_file.src_path) @@ -369,6 +364,7 @@ class KittenGroomerFileCheck(KittenGroomerBase): self._safe_copy() def _libreoffice(self): + """Processes a libreoffice file.""" self.cur_file.add_log_details('processing_type', 'libreoffice') # As long as there ar no way to do a sanity check on the files => dangerous try: @@ -385,7 +381,7 @@ class KittenGroomerFileCheck(KittenGroomerBase): self._safe_copy() def _pdf(self): - '''Way to process PDF file''' + """Processes a PDF file.""" self.cur_file.add_log_details('processing_type', 'pdf') xmlDoc = PDFiD(self.cur_file.src_path) oPDFiD = cPDFiD(xmlDoc, True) @@ -407,33 +403,47 @@ class KittenGroomerFileCheck(KittenGroomerBase): self.cur_file.make_dangerous() def _archive(self): - '''Way to process Archive''' + """Processes an archive using 7zip. The archive is extracted to a + temporary directory and self.processdir is called on that directory. + The recursive archive depth is increased to protect against archive + bombs.""" self.cur_file.add_log_details('processing_type', 'archive') self.cur_file.is_recursive = True self.cur_file.log_string += 'Archive extracted, processing content.' tmpdir = self.cur_file.dst_path + '_temp' self._safe_mkdir(tmpdir) - extract_command = '{} -p1 x "{}" -o"{}" -bd -aoa'.format(SEVENZ, self.cur_file.src_path, tmpdir) + extract_command = '{} -p1 x "{}" -o"{}" -bd -aoa'.format(SEVENZ_PATH, self.cur_file.src_path, tmpdir) self._run_process(extract_command) - self.recursive += 1 + self.recursive_archive_depth += 1 self.tree(tmpdir) self.processdir(tmpdir, self.cur_file.dst_path) - self.recursive -= 1 + self.recursive_archive_depth -= 1 self._safe_rmtree(tmpdir) + def _handle_archivebomb(self, src_dir): + self.cur_file.make_dangerous() + self.cur_file.add_log_details('Archive Bomb', True) + self.log_name.warning('ARCHIVE BOMB.') + self.log_name.warning('The content of the archive contains recursively other archives.') + self.log_name.warning('This is a bad sign so the archive is not extracted to the destination key.') + self._safe_rmtree(src_dir) + if src_dir.endswith('_temp'): + bomb_path = src_dir[:-len('_temp')] + self._safe_remove(bomb_path) + def _unknown_app(self): - '''Way to process an unknown file''' + """Processes an unknown file.""" self.cur_file.make_unknown() self._safe_copy() def _binary_app(self): - '''Way to process an unknown binary file''' + """Processses an unknown binary file.""" self.cur_file.make_binary() self._safe_copy() ####################### # Metadata extractors - def _metadata_exif(self, metadataFile): + def _metadata_exif(self, metadata_file): img = open(self.cur_file.src_path, 'rb') tags = None @@ -463,7 +473,7 @@ class KittenGroomerFileCheck(KittenGroomerBase): printable = value else: printable = str(value) - metadataFile.write("Key: {}\tValue: {}\n".format(tag, printable)) + metadata_file.write("Key: {}\tValue: {}\n".format(tag, printable)) self.cur_file.add_log_details('metadata', 'exif') img.close() return True @@ -487,22 +497,36 @@ class KittenGroomerFileCheck(KittenGroomerBase): return False def extract_metadata(self): - metadataFile = self._safe_metadata_split(".metadata.txt") - success = self.metadata_processing_options.get(self.cur_file.mimetype)(metadataFile) - metadataFile.close() + metadata_file = self._safe_metadata_split(".metadata.txt") + success = self.metadata_processing_options.get(self.cur_file.mimetype)(metadata_file) + metadata_file.close() if not success: # FIXME Delete empty metadata file pass ####################### - # ##### Not converted, checking the mime type ###### + # ##### Media - audio and video aren't converted ###### def audio(self): - '''Way to process an audio file''' + """Processes an audio file.""" self.cur_file.log_string += 'Audio file' self._media_processing() + def video(self): + """Processes a video.""" + self.cur_file.log_string += 'Video file' + self._media_processing() + + def _media_processing(self): + """Generic way to process all media files.""" + self.cur_file.add_log_details('processing_type', 'media') + self._safe_copy() + def image(self): - '''Way to process an image''' + """Processes an image. + + Extracts metadata if metadata is present. Creates a temporary + directory, opens the using PIL.Image, saves it to the temporary + directory, and copies it to the destination.""" if self.cur_file.has_metadata(): self.extract_metadata() @@ -534,40 +558,20 @@ class KittenGroomerFileCheck(KittenGroomerBase): self.cur_file.log_string += 'Image file' self.cur_file.add_log_details('processing_type', 'image') - def video(self): - '''Way to process a video''' - self.cur_file.log_string += 'Video file' - self._media_processing() - - def _media_processing(self): - '''Generic way to process all the media files''' - self.cur_file.add_log_details('processing_type', 'media') - self._safe_copy() - ####################### def processdir(self, src_dir=None, dst_dir=None): - ''' - Main function doing the processing - ''' + """Main function coordinating file processing.""" if src_dir is None: src_dir = self.src_root_dir if dst_dir is None: dst_dir = self.dst_root_dir - if self.recursive > 0: + if self.recursive_archive_depth > 0: self._print_log() - if self.recursive >= self.max_recursive: - self.cur_file.make_dangerous() - self.cur_file.add_log_details('Archive Bomb', True) - self.log_name.warning('ARCHIVE BOMB.') - self.log_name.warning('The content of the archive contains recursively other archives.') - self.log_name.warning('This is a bad sign so the archive is not extracted to the destination key.') - self._safe_rmtree(src_dir) - if src_dir.endswith('_temp'): - archbomb_path = src_dir[:-len('_temp')] - self._safe_remove(archbomb_path) + if self.recursive_archive_depth >= self.max_recursive_depth: + self._handle_archivebomb(src_dir) for srcpath in self._list_all_files(src_dir): self.cur_file = File(srcpath, srcpath.replace(src_dir, dst_dir)) @@ -581,5 +585,6 @@ class KittenGroomerFileCheck(KittenGroomerBase): if not self.cur_file.is_recursive: self._print_log() + if __name__ == '__main__': - main(KittenGroomerFileCheck, 'Generic version of the KittenGroomer. Convert and rename files.') + main(KittenGroomerFileCheck, 'File sanitizer used in CIRCLean. Renames potentially dangerous files.') From 0badab0b2015faac95566b70395c8f776dbb25e6 Mon Sep 17 00:00:00 2001 From: Dan Puttick Date: Tue, 20 Dec 2016 15:16:58 -0500 Subject: [PATCH 07/22] Small setup.py change --- setup.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index e327b7f..f20da6a 100644 --- a/setup.py +++ b/setup.py @@ -12,10 +12,10 @@ setup( description='Standalone CIRCLean/KittenGroomer code.', packages=['kittengroomer'], scripts=[ - 'bin/generic.py', - 'bin/pier9.py', - 'bin/specific.py', - 'bin/filecheck.py' + 'bin/generic.py', + 'bin/pier9.py', + 'bin/specific.py', + 'bin/filecheck.py' ], include_package_data=True, package_data={'data': ['PDFA_def.ps', 'srgb.icc']}, From 35501b69af86a841f76ced9c8b456d092018eecd Mon Sep 17 00:00:00 2001 From: Dan Puttick Date: Wed, 21 Dec 2016 20:41:46 -0500 Subject: [PATCH 08/22] Create test_filecheck.py --- tests/test_filecheck.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 tests/test_filecheck.py diff --git a/tests/test_filecheck.py b/tests/test_filecheck.py new file mode 100644 index 0000000..a4dc481 --- /dev/null +++ b/tests/test_filecheck.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import pytest + +from bin.filecheck import KittenGroomerFileCheck, File, main + + +class TestFileHandling: + pass + + # We're going to give KittenGroomer a bunch of files, and it's going to process them + # Maybe we want to make a function that processdir delegates to? Or is it just the File Object that's responsible? + # Ideally we should be able to pass a path to a function and have it do stuff? And then we can test that function? + # So we have a function that takes a path and returns...log info? That makes sense actually. Or some sort of meta data + # The function could maybe be called processfile From 70a73dc29203481e44741e6ebd3b2e6c8d0ccf86 Mon Sep 17 00:00:00 2001 From: Dan Puttick Date: Wed, 21 Dec 2016 21:24:29 -0500 Subject: [PATCH 09/22] Move process_file code into its own method --- bin/filecheck.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/bin/filecheck.py b/bin/filecheck.py index e9798b3..610de21 100644 --- a/bin/filecheck.py +++ b/bin/filecheck.py @@ -560,6 +560,19 @@ class KittenGroomerFileCheck(KittenGroomerBase): ####################### + def process_file(self, srcpath, dstpath, relative_path): + self.cur_file = File(srcpath, dstpath) + self.log_name.info('Processing {} ({}/{})', + relative_path, + self.cur_file.main_type, + self.cur_file.sub_type) + if not self.cur_file.is_dangerous(): + self.mime_processing_options.get(self.cur_file.main_type, self.unknown)() + else: + self._safe_copy() + if not self.cur_file.is_recursive: + self._print_log() + def processdir(self, src_dir=None, dst_dir=None): """Main function coordinating file processing.""" if src_dir is None: @@ -574,16 +587,10 @@ class KittenGroomerFileCheck(KittenGroomerBase): self._handle_archivebomb(src_dir) for srcpath in self._list_all_files(src_dir): - self.cur_file = File(srcpath, srcpath.replace(src_dir, dst_dir)) - - self.log_name.info('Processing {} ({}/{})', srcpath.replace(src_dir + '/', ''), - self.cur_file.main_type, self.cur_file.sub_type) - if not self.cur_file.is_dangerous(): - self.mime_processing_options.get(self.cur_file.main_type, self.unknown)() - else: - self._safe_copy() - if not self.cur_file.is_recursive: - self._print_log() + dstpath = srcpath.replace(src_dir, dst_dir) + relative_path = srcpath.replace(src_dir + '/', '') + # which path do we want in the log? + self.process_file(srcpath, dstpath, relative_path) if __name__ == '__main__': From 3dad4faa6140d82853c9026ca4eecc8e12ec7a7b Mon Sep 17 00:00:00 2001 From: Dan Puttick Date: Sat, 24 Dec 2016 16:46:22 -0500 Subject: [PATCH 10/22] Reorganize tests making them easier to run - The tests now automatically run depending on whether you have the dependencies installed, instead of failing and throwing exceptions. - CONTRIBUTING.md has more information on how to run the tests. - When the tests run, they will save their logs to /test_logs instead of printing them so you can read them later. - Change names of source file directories to make them more descriptive --- .travis.yml | 10 +-- CONTRIBUTING.md | 12 ++- bin/README.md | 18 +++- bin/filecheck.py | 1 + tests/logging.py | 22 +++++ tests/src_complex/42.zip | Bin 42838 -> 0 bytes tests/src_complex/blah.zip | Bin 344 -> 0 bytes tests/{src_complex => src_invalid}/blah.conf | 0 .../{src_complex => src_invalid}/blah.tar.bz2 | Bin tests/{src_complex => src_invalid}/blah.txt | 0 tests/{src_complex => src_invalid}/foobar.dat | 0 .../geneve_1564.pdf | Bin .../geneve_1564_wrong_mime.conf | Bin .../{src_complex => src_invalid}/message.msg | 0 tests/{src_complex => src_invalid}/ntree.wrl | 0 tests/{src_simple => src_valid}/blah.conf | 0 tests/test_binaries.py | 84 ------------------ tests/test_filecheck.py | 46 ++++++++-- tests/test_generic.py | 50 +++++++++++ tests/test_helpers.py | 6 +- tests/test_logs/.keepdir | 0 tests/test_specific_and_pier9.py | 53 +++++++++++ tox.ini | 2 +- 23 files changed, 201 insertions(+), 103 deletions(-) create mode 100644 tests/logging.py delete mode 100644 tests/src_complex/42.zip delete mode 100644 tests/src_complex/blah.zip rename tests/{src_complex => src_invalid}/blah.conf (100%) rename tests/{src_complex => src_invalid}/blah.tar.bz2 (100%) rename tests/{src_complex => src_invalid}/blah.txt (100%) rename tests/{src_complex => src_invalid}/foobar.dat (100%) rename tests/{src_complex => src_invalid}/geneve_1564.pdf (100%) rename tests/{src_complex => src_invalid}/geneve_1564_wrong_mime.conf (100%) rename tests/{src_complex => src_invalid}/message.msg (100%) rename tests/{src_complex => src_invalid}/ntree.wrl (100%) rename tests/{src_simple => src_valid}/blah.conf (100%) delete mode 100644 tests/test_binaries.py create mode 100644 tests/test_generic.py create mode 100644 tests/test_logs/.keepdir create mode 100644 tests/test_specific_and_pier9.py diff --git a/.travis.yml b/.travis.yml index 3978062..8866a29 100644 --- a/.travis.yml +++ b/.travis.yml @@ -61,7 +61,7 @@ install: - pushd theZoo/malwares/Binaries - python unpackall.py - popd - - mv theZoo/malwares/Binaries/out tests/src_complex/ + - mv theZoo/malwares/Binaries/out tests/src_invalid/ # Path traversal - git clone https://github.com/jwilk/path-traversal-samples - pushd path-traversal-samples @@ -72,12 +72,12 @@ install: - make - popd - popd - - mv path-traversal-samples/zip/*.zip tests/src_complex/ - - mv path-traversal-samples/rar/*.rar tests/src_complex/ + - mv path-traversal-samples/zip/*.zip tests/src_invalid/ + - mv path-traversal-samples/rar/*.rar tests/src_invalid/ # Office docs - git clone https://github.com/eea/odfpy.git - - mv odfpy/tests/examples/* tests/src_complex/ - - pushd tests/src_complex/ + - mv odfpy/tests/examples/* tests/src_invalid/ + - pushd tests/src_invalid/ - wget https://bitbucket.org/decalage/olefileio_pl/raw/3073963b640935134ed0da34906fea8e506460be/Tests/images/test-ole-file.doc - wget --no-check-certificate https://www.officedissector.com/corpus/fraunhoferlibrary.zip - unzip -o fraunhoferlibrary.zip diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c27f429..e514393 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,5 +29,13 @@ or if you have an example you'd like to contribute. Running the tests ================= -* Running the tests is easy. First, make sure you've installed the project and testing dependencies. -Then, run `python -m pytest` or just `pytest` in the top level or /tests directory. +* Running the tests is fairly straightforward. +* First, make sure you've installed the project and testing dependencies. +* Then, run `python -m pytest` or just `pytest` in the top level directory of the module. +* Each integration test that runs will generate a timestamped copy of the log for that run +in the tests/testlogs directory. +* If you'd like to get information about code coverage, run the tests using +`pytest --cov=kittengroomer`. +* You can test with multiple versions of Python if you have them installed +by running `pip install tox` and then `tox`. Make sure you modify "envlist" +in tox.ini for the Python versions you plan to use. diff --git a/bin/README.md b/bin/README.md index 28edb2e..b910162 100644 --- a/bin/README.md +++ b/bin/README.md @@ -15,6 +15,10 @@ Requirements per script filecheck.py ------------ +This is the script used by the [CIRCLean](https://github.com/CIRCL/Circlean) +USB key sanitizer. It is designed to handle a range of file types, and will +mark them as dangerous if they meet certain criteria. + Requirements by type of document: * Microsoft office: oletools, olefile * OOXML: officedissector @@ -23,12 +27,14 @@ Requirements by type of document: * Metadata: exifread * Images: pillow +Note: pdfid is a not installable with pip. It must be downloaded and installed +manually in the directory where filecheck will be run. ``` sudo apt-get install p7zip-full p7zip-rar libxml2-dev libxslt1-dev pip install lxml oletools olefile pillow exifread pip install git+https://github.com/Rafiot/officedissector.git - # pdfid is not a package, installing manually + # installing pdfid manually wget https://didierstevens.com/files/software/pdfid_v0_2_1.zip unzip pdfid_v0_2_1.zip ``` @@ -36,6 +42,9 @@ Requirements by type of document: generic.py ---------- +This is a script used by an older version of CIRCLean. It has more dependencies +than filecheck.py and they are more complicated to install. + Requirements by type of document: * Office and all text files: unoconv, libreoffice * PDF: ghostscript, pdf2htmlEX @@ -60,9 +69,16 @@ Requirements by type of document: pier9.py -------- +This script has a list of file formats for various brands of industrial +manufacturing equipment, such as 3d printers, CNC machines, etc. It only +copies files that match these file formats. + No external dependencies required. specific.py ----------- +As the name suggests, this script copies only specific file formats according +to the configuration provided by the user. + No external dependencies required. diff --git a/bin/filecheck.py b/bin/filecheck.py index 610de21..e9022bb 100644 --- a/bin/filecheck.py +++ b/bin/filecheck.py @@ -197,6 +197,7 @@ class KittenGroomerFileCheck(KittenGroomerBase): def _print_log(self): """Print the logs related to the current file being processed.""" + # TODO: change name to _write_log tmp_log = self.log_name.fields(**self.cur_file.log_details) if self.cur_file.is_dangerous(): tmp_log.warning(self.cur_file.log_string) diff --git a/tests/logging.py b/tests/logging.py new file mode 100644 index 0000000..e625137 --- /dev/null +++ b/tests/logging.py @@ -0,0 +1,22 @@ +import os + + +def save_logs(groomer, test_description): + divider = ('=' * 10 + '{}' + '=' * 10 + '\n') + test_log_path = 'tests/test_logs/{}.log'.format(test_description) + with open(test_log_path, 'w+') as test_log: + test_log.write(divider.format('TEST LOG')) + with open(groomer.log_processing, 'r') as logfile: + log = logfile.read() + test_log.write(log) + if groomer.debug: + if os.path.exists(groomer.log_debug_err): + test_log.write(divider.format('ERR LOG')) + with open(groomer.log_debug_err, 'r') as debug_err: + err = debug_err.read() + test_log.write(err) + if os.path.exists(groomer.log_debug_out): + test_log.write(divider.format('OUT LOG')) + with open(groomer.log_debug_out, 'r') as debug_out: + out = debug_out.read() + test_log.write(out) diff --git a/tests/src_complex/42.zip b/tests/src_complex/42.zip deleted file mode 100644 index e7681537ced5f7abcc7dfb999da856951911649d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42838 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 I_n*~&0V&+h761SM diff --git a/tests/src_complex/blah.zip b/tests/src_complex/blah.zip deleted file mode 100644 index 3e809f43758641b17be4cfd469a7f60f09025340..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 344 zcmWIWW@h1H0D+Q*D0kr(r_}|3Y!K#TkYPy5NzBko&d*B=4dG;9KJB0vc3_TvSZM_} z10%}|W(Ec@5t5NvtN=ub3MHw2kn0&3fp|+Jh=t^6R*0j~930?{Fb>m+$i@Z3i~~CuXd1}D7^bnZfvjZ$!nr_t IDTu=W0EsR|>Hq)$ diff --git a/tests/src_complex/blah.conf b/tests/src_invalid/blah.conf similarity index 100% rename from tests/src_complex/blah.conf rename to tests/src_invalid/blah.conf diff --git a/tests/src_complex/blah.tar.bz2 b/tests/src_invalid/blah.tar.bz2 similarity index 100% rename from tests/src_complex/blah.tar.bz2 rename to tests/src_invalid/blah.tar.bz2 diff --git a/tests/src_complex/blah.txt b/tests/src_invalid/blah.txt similarity index 100% rename from tests/src_complex/blah.txt rename to tests/src_invalid/blah.txt diff --git a/tests/src_complex/foobar.dat b/tests/src_invalid/foobar.dat similarity index 100% rename from tests/src_complex/foobar.dat rename to tests/src_invalid/foobar.dat diff --git a/tests/src_complex/geneve_1564.pdf b/tests/src_invalid/geneve_1564.pdf similarity index 100% rename from tests/src_complex/geneve_1564.pdf rename to tests/src_invalid/geneve_1564.pdf diff --git a/tests/src_complex/geneve_1564_wrong_mime.conf b/tests/src_invalid/geneve_1564_wrong_mime.conf similarity index 100% rename from tests/src_complex/geneve_1564_wrong_mime.conf rename to tests/src_invalid/geneve_1564_wrong_mime.conf diff --git a/tests/src_complex/message.msg b/tests/src_invalid/message.msg similarity index 100% rename from tests/src_complex/message.msg rename to tests/src_invalid/message.msg diff --git a/tests/src_complex/ntree.wrl b/tests/src_invalid/ntree.wrl similarity index 100% rename from tests/src_complex/ntree.wrl rename to tests/src_invalid/ntree.wrl diff --git a/tests/src_simple/blah.conf b/tests/src_valid/blah.conf similarity index 100% rename from tests/src_simple/blah.conf rename to tests/src_valid/blah.conf diff --git a/tests/test_binaries.py b/tests/test_binaries.py deleted file mode 100644 index 38d6784..0000000 --- a/tests/test_binaries.py +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -import os -import sys - -import pytest - -from bin.specific import KittenGroomerSpec -from bin.pier9 import KittenGroomerPier9 -from bin.generic import KittenGroomer -from bin.filecheck import KittenGroomerFileCheck - - -skip = pytest.mark.skip -py2_only = pytest.mark.skipif(sys.version_info.major == 3, - reason="filecheck.py only runs on python 2") - - -@pytest.fixture -def src_simple(): - return os.path.join(os.getcwd(), 'tests/src_simple') - - -@pytest.fixture -def src_complex(): - return os.path.join(os.getcwd(), 'tests/src_complex') - - -@pytest.fixture -def dst(): - return os.path.join(os.getcwd(), 'tests/dst') - - -def test_specific_valid(src_simple, dst): - spec = KittenGroomerSpec(src_simple, dst, debug=True) - spec.processdir() - dump_logs(spec) - - -def test_specific_invalid(src_complex, dst): - spec = KittenGroomerSpec(src_complex, dst, debug=True) - spec.processdir() - dump_logs(spec) - - -def test_pier9(src_complex, dst): - spec = KittenGroomerPier9(src_complex, dst, debug=True) - spec.processdir() - dump_logs(spec) - - -def test_generic(src_simple, dst): - spec = KittenGroomer(src_simple, dst, debug=True) - spec.processdir() - dump_logs(spec) - - -def test_generic_2(src_complex, dst): - spec = KittenGroomer(src_complex, dst, debug=True) - spec.processdir() - dump_logs(spec) - - -def test_filecheck(src_complex, dst): - spec = KittenGroomerFileCheck(src_complex, dst, debug=True) - spec.processdir() - dump_logs(spec) - - -def test_filecheck_2(src_simple, dst): - spec = KittenGroomerFileCheck(src_simple, dst, debug=True) - spec.processdir() - dump_logs(spec) - -## Helper functions - -def dump_logs(spec): - print(open(spec.log_processing, 'rb').read()) - if spec.debug: - if os.path.exists(spec.log_debug_err): - print(open(spec.log_debug_err, 'rb').read()) - if os.path.exists(spec.log_debug_out): - print(open(spec.log_debug_out, 'rb').read()) diff --git a/tests/test_filecheck.py b/tests/test_filecheck.py index a4dc481..ac7cf42 100644 --- a/tests/test_filecheck.py +++ b/tests/test_filecheck.py @@ -1,16 +1,48 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +import os + import pytest -from bin.filecheck import KittenGroomerFileCheck, File, main +from tests.logging import save_logs +try: + from bin.filecheck import KittenGroomerFileCheck, File, main + NODEPS = False +except ImportError: + NODEPS = True + +skipif_nodeps = pytest.mark.skipif(NODEPS, + reason="Dependencies aren't installed") + + +@skipif_nodeps +class TestIntegration: + + @pytest.fixture + def src_valid(self): + return os.path.join(os.getcwd(), 'tests/src_valid') + + @pytest.fixture + def src_invalid(self): + return os.path.join(os.getcwd(), 'tests/src_invalid') + + @pytest.fixture + def dst(self): + return os.path.join(os.getcwd(), 'tests/dst') + + def test_filecheck(self, src_invalid, dst): + groomer = KittenGroomerFileCheck(src_invalid, dst, debug=True) + groomer.processdir() + test_description = "filecheck_invalid" + save_logs(groomer, test_description) + + def test_filecheck_2(self, src_valid, dst): + groomer = KittenGroomerFileCheck(src_valid, dst, debug=True) + groomer.processdir() + test_description = "filecheck_valid" + save_logs(groomer, test_description) class TestFileHandling: pass - - # We're going to give KittenGroomer a bunch of files, and it's going to process them - # Maybe we want to make a function that processdir delegates to? Or is it just the File Object that's responsible? - # Ideally we should be able to pass a path to a function and have it do stuff? And then we can test that function? - # So we have a function that takes a path and returns...log info? That makes sense actually. Or some sort of meta data - # The function could maybe be called processfile diff --git a/tests/test_generic.py b/tests/test_generic.py new file mode 100644 index 0000000..a17fb5e --- /dev/null +++ b/tests/test_generic.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import os + +import pytest + +from bin.generic import KittenGroomer, File, main +from tests.logging import save_logs + +skipif_nodeps = pytest.mark.skipif(os.path.exists('/usr/bin/unoconv') is False, + reason="Dependencies aren't installed") + + +@skipif_nodeps +class TestIntegration: + + @pytest.fixture + def src_valid(self): + return os.path.join(os.getcwd(), 'tests/src_valid') + + @pytest.fixture + def src_invalid(self): + return os.path.join(os.getcwd(), 'tests/src_invalid') + + @pytest.fixture + def dst(self): + return os.path.join(os.getcwd(), 'tests/dst') + + def test_generic(self, src_valid, dst): + groomer = KittenGroomer(src_valid, dst, debug=True) + groomer.processdir() + test_description = 'generic_valid' + save_logs(groomer, test_description) + + def test_generic_2(self, src_invalid, dst): + groomer = KittenGroomer(src_invalid, dst, debug=True) + groomer.processdir() + test_description = 'generic_invalid' + save_logs(groomer, test_description) + + +class TestFileHandling: + pass + + # We're going to give KittenGroomer a bunch of files, and it's going to process them + # Maybe we want to make a function that processdir delegates to? Or is it just the File Object that's responsible? + # Ideally we should be able to pass a path to a function and have it do stuff? And then we can test that function? + # So we have a function that takes a path and returns...log info? That makes sense actually. Or some sort of meta data + # The function could maybe be called processfile diff --git a/tests/test_helpers.py b/tests/test_helpers.py index a14510a..f59c862 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -21,7 +21,7 @@ class TestFileBase: @fixture def source_file(self): - return 'tests/src_simple/blah.conf' + return 'tests/src_valid/blah.conf' @fixture def dest_file(self): @@ -84,7 +84,7 @@ class TestFileBase: # We should probably catch everytime that happens and tell the user explicitly happened (and maybe put it in the log) def test_create(self): - file = FileBase('tests/src_simple/blah.conf', '/tests/dst/blah.conf') + file = FileBase('tests/src_valid/blah.conf', '/tests/dst/blah.conf') def test_create_broken(self, tmpdir): with pytest.raises(TypeError): @@ -221,7 +221,7 @@ class TestKittenGroomerBase: @fixture def source_directory(self): - return 'tests/src_complex' + return 'tests/src_invalid' @fixture def dest_directory(self): diff --git a/tests/test_logs/.keepdir b/tests/test_logs/.keepdir new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_specific_and_pier9.py b/tests/test_specific_and_pier9.py new file mode 100644 index 0000000..d411aa8 --- /dev/null +++ b/tests/test_specific_and_pier9.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import os + +import pytest + +from bin.specific import KittenGroomerSpec +from bin.pier9 import KittenGroomerPier9 +from tests.logging import save_logs + + +@pytest.fixture +def src_valid(): + return os.path.join(os.getcwd(), 'tests/src_valid') + + +@pytest.fixture +def src_invalid(): + return os.path.join(os.getcwd(), 'tests/src_invalid') + + +@pytest.fixture +def dst(): + return os.path.join(os.getcwd(), 'tests/dst') + + +def test_specific_valid(src_valid, dst): + groomer = KittenGroomerSpec(src_valid, dst, debug=True) + groomer.processdir() + test_description = 'specific_valid' + save_logs(groomer, test_description) + + +def test_specific_invalid(src_invalid, dst): + groomer = KittenGroomerSpec(src_invalid, dst, debug=True) + groomer.processdir() + test_description = 'specific_invalid' + save_logs(groomer, test_description) + + +def test_pier9_valid(src_invalid, dst): + groomer = KittenGroomerPier9(src_invalid, dst, debug=True) + groomer.processdir() + test_description = 'pier9_valid' + save_logs(groomer, test_description) + + +def test_pier9_invalid(src_invalid, dst): + groomer = KittenGroomerPier9(src_invalid, dst, debug=True) + groomer.processdir() + test_description = 'pier9_invalid' + save_logs(groomer, test_description) diff --git a/tox.ini b/tox.ini index da1ed31..40b9ad6 100644 --- a/tox.ini +++ b/tox.ini @@ -2,4 +2,4 @@ envlist=py27,py35 [testenv] deps=-rdev-requirements.txt -commands= pytest tests/test_helpers.py --cov=kittengroomer +commands= pytest --cov=kittengroomer --cov=bin From 43c01e0f056ec5c3dbc45fab005fcdf70442f996 Mon Sep 17 00:00:00 2001 From: Dan Puttick Date: Wed, 21 Dec 2016 21:45:20 -0500 Subject: [PATCH 11/22] Add example autorun.inf file to tests/src_invalid --- tests/src_invalid/autorun.inf | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 tests/src_invalid/autorun.inf diff --git a/tests/src_invalid/autorun.inf b/tests/src_invalid/autorun.inf new file mode 100644 index 0000000..895e1a4 --- /dev/null +++ b/tests/src_invalid/autorun.inf @@ -0,0 +1,4 @@ +[autorun] +open=setup.exe +icon=setup.ico +label=My install CD From d4bfe794be9fbf4a62acf407b9822d8c864abe52 Mon Sep 17 00:00:00 2001 From: Dan Puttick Date: Thu, 22 Dec 2016 10:12:13 -0500 Subject: [PATCH 12/22] In FileBase, self.extension is now always lowercase --- kittengroomer/helpers.py | 6 +++++- tests/test_helpers.py | 7 +++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/kittengroomer/helpers.py b/kittengroomer/helpers.py index 9604c49..10dd6a9 100644 --- a/kittengroomer/helpers.py +++ b/kittengroomer/helpers.py @@ -45,9 +45,13 @@ class FileBase(object): self.dst_path = dst_path self.log_details = {'filepath': self.src_path} self.log_string = '' - _, self.extension = os.path.splitext(self.src_path) + self._determine_extension() self._determine_mimetype() + def _determine_extension(self): + _, ext = os.path.splitext(self.src_path) + self.extension = ext.lower() + def _determine_mimetype(self): if os.path.islink(self.src_path): # magic will throw an IOError on a broken symlink diff --git a/tests/test_helpers.py b/tests/test_helpers.py index f59c862..0d0c338 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -113,6 +113,13 @@ class TestFileBase: # assert file.log_details == copied_log # this fails for now, we need to make log_details undeletable # we should probably check for more extensions here + def test_extension_uppercase(self, tmpdir): + file_path = tmpdir.join('TEST.TXT') + file_path.write('testing') + file_path = file_path.strpath + file = FileBase(file_path, file_path) + assert file.extension == '.txt' + def test_mimetypes(self, generic_conf_file): assert generic_conf_file.has_mimetype() assert generic_conf_file.mimetype == 'text/plain' From f7ab393eb6f51a4419dd167ffeb710cc6defc582 Mon Sep 17 00:00:00 2001 From: Dan Puttick Date: Wed, 21 Dec 2016 18:04:59 -0500 Subject: [PATCH 13/22] Refactor FileBase helper methods --- kittengroomer/helpers.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/kittengroomer/helpers.py b/kittengroomer/helpers.py index 10dd6a9..72ac1c1 100644 --- a/kittengroomer/helpers.py +++ b/kittengroomer/helpers.py @@ -81,7 +81,6 @@ class FileBase(object): Returns False + updates log if self.main_type or self.sub_type are not set. """ - if not self.main_type or not self.sub_type: self.log_details.update({'broken_mime': True}) return False @@ -93,16 +92,22 @@ class FileBase(object): Returns False + updates self.log_details if self.extension is not set. """ - if not self.extension: + if self.extension == '': self.log_details.update({'no_extension': True}) return False return True def is_dangerous(self): """Returns True if self.log_details contains 'dangerous'.""" - if self.log_details.get('dangerous'): - return True - return False + return ('dangerous' in self.log_details) + + def is_unknown(self): + """Returns True if self.log_details contains 'unknown'.""" + return ('unknown' in self.log_details) + + def is_binary(self): + """returns True if self.log_details contains 'binary'.""" + return ('binary' in self.log_details) def is_symlink(self): """Returns True and updates log if file is a symlink.""" @@ -120,10 +125,9 @@ class FileBase(object): Marks a file as dangerous. Prepends and appends DANGEROUS to the destination file name - to avoid double-click of death. + to help prevent double-click of death. """ if self.is_dangerous(): - # Already marked as dangerous, do nothing return self.log_details['dangerous'] = True path, filename = os.path.split(self.dst_path) @@ -131,8 +135,7 @@ class FileBase(object): def make_unknown(self): """Marks a file as an unknown type and prepends UNKNOWN to filename.""" - if self.is_dangerous() or self.log_details.get('binary'): - # Already marked as dangerous or binary, do nothing + if self.is_dangerous() or self.is_binary(): return self.log_details['unknown'] = True path, filename = os.path.split(self.dst_path) @@ -141,7 +144,6 @@ class FileBase(object): def make_binary(self): """Marks a file as a binary and appends .bin to filename.""" if self.is_dangerous(): - # Already marked as dangerous, do nothing return self.log_details['binary'] = True path, filename = os.path.split(self.dst_path) @@ -265,9 +267,10 @@ class KittenGroomerBase(object): def _safe_metadata_split(self, ext): """Create a separate file to hold this file's metadata.""" + # TODO: fix logic in this method dst = self.cur_file.dst_path try: - if os.path.exists(self.cur_file.src_path + ext): # should we check dst_path as well? + if os.path.exists(self.cur_file.src_path + ext): # should we check dst_path as well? raise KittenGroomerError("Cannot create split metadata file for \"" + self.cur_file.dst_path + "\", type '" + ext + "': File exists.") From 21cc175867d1eeb57f4ad30df3b5ea9e647c7452 Mon Sep 17 00:00:00 2001 From: Dan Puttick Date: Wed, 18 Jan 2017 16:56:16 -0500 Subject: [PATCH 14/22] Move non-filecheck.py binaries into examples directory Tests for these scripts also removed from /tests and from .travis.yml Two .zip archives accidentally deleted from /tests/src_invalid, re-added them and changed .gitignore to prevent the problem --- .gitignore | 10 ++--- .travis.yml | 20 +-------- bin/README.md | 69 ++++--------------------------- examples/README.md | 56 +++++++++++++++++++++++++ {bin => examples}/generic.py | 0 {bin => examples}/pier9.py | 0 {bin => examples}/specific.py | 0 setup.py | 3 -- tests/src_invalid/42.zip | Bin 0 -> 42838 bytes tests/src_invalid/blah.zip | Bin 0 -> 344 bytes tests/test_generic.py | 50 ---------------------- tests/test_specific_and_pier9.py | 53 ------------------------ 12 files changed, 72 insertions(+), 189 deletions(-) create mode 100644 examples/README.md rename {bin => examples}/generic.py (100%) rename {bin => examples}/pier9.py (100%) rename {bin => examples}/specific.py (100%) create mode 100644 tests/src_invalid/42.zip create mode 100644 tests/src_invalid/blah.zip delete mode 100644 tests/test_generic.py delete mode 100644 tests/test_specific_and_pier9.py diff --git a/.gitignore b/.gitignore index 95e49f2..ecf6be3 100644 --- a/.gitignore +++ b/.gitignore @@ -67,8 +67,8 @@ target/ *.vrb # Project specific -/tests/dst/* -!/tests/logs/ -!/tests/.keepdir - - +tests/dst/* +tests/test_logs/* +!tests/**/.keepdir +!tests/src_invalid/* +!tests/src_valid/* diff --git a/.travis.yml b/.travis.yml index 8866a29..8e3dfb7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,8 +17,6 @@ addons: packages: # General dependencies - p7zip-full - # generic.py dependencies - - ghostscript # Testing dependencies - mercurial @@ -26,21 +24,7 @@ install: # General dependencies - sudo add-apt-repository "deb http://archive.ubuntu.com/ubuntu/ trusty multiverse" && sudo add-apt-repository "deb http://archive.ubuntu.com/ubuntu/ trusty-updates multiverse" - sudo apt-get update -qq - - sudo apt-get install -y p7zip-rar - # generic.py: pdf2htmlEX + dependencies - - sudo add-apt-repository ppa:fontforge/fontforge --yes - # to get a working 0.26 poppler - - sudo add-apt-repository ppa:delayargentina/delayx --yes - - sudo apt-get update -qq - - sudo apt-get install -y libpoppler-dev libpoppler-private-dev libspiro-dev libcairo-dev libpango1.0-dev libfreetype6-dev libltdl-dev libfontforge-dev python-imaging python-pip firefox xvfb - - git clone https://github.com/coolwanglu/pdf2htmlEX.git - - pushd pdf2htmlEX - - cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr -DENABLE_SVG=ON . - - make - - sudo make install - - popd - # generic.py: Other dependencies - - sudo apt-get install -y libreoffice libreoffice-script-provider-python unoconv + - sudo apt-get install -y p7zip-rar python-pip # filecheck.py dependencies - sudo apt-get install libxml2-dev libxslt1-dev - wget https://didierstevens.com/files/software/pdfid_v0_2_1.zip @@ -82,7 +66,7 @@ install: - wget --no-check-certificate https://www.officedissector.com/corpus/fraunhoferlibrary.zip - unzip -o fraunhoferlibrary.zip - rm fraunhoferlibrary.zip - - 7z x 42.zip -p42 + - 7z x -p42 42.zip - wget http://www.sample-videos.com/audio/mp3/india-national-anthem.mp3 - wget http://www.sample-videos.com/video/mp4/720/big_buck_bunny_720p_1mb.mp4 - wget http://thewalter.net/stef/software/rtfx/sample.rtf diff --git a/bin/README.md b/bin/README.md index b910162..f509f34 100644 --- a/bin/README.md +++ b/bin/README.md @@ -1,25 +1,18 @@ -Examples -======== - -These are several sanitizers that demonstrate PyCIRCLean's capabilities. Feel free to -adapt or modify any of them to suit your requirements. In order to use any of these scripts, -you will first need to install the PyCIRCLean dependencies (preferably in a virtualenv): - -``` - pip install . -``` - -Requirements per script -======================= - filecheck.py ------------- +============ This is the script used by the [CIRCLean](https://github.com/CIRCL/Circlean) USB key sanitizer. It is designed to handle a range of file types, and will mark them as dangerous if they meet certain criteria. -Requirements by type of document: +Before installing the filecheck.py depenencies, make sure to install the PyCIRCLean +dependencies: + +``` + pip install . +``` + +Dependencies by type of document: * Microsoft office: oletools, olefile * OOXML: officedissector * PDF: pdfid @@ -38,47 +31,3 @@ manually in the directory where filecheck will be run. wget https://didierstevens.com/files/software/pdfid_v0_2_1.zip unzip pdfid_v0_2_1.zip ``` - -generic.py ----------- - -This is a script used by an older version of CIRCLean. It has more dependencies -than filecheck.py and they are more complicated to install. - -Requirements by type of document: -* Office and all text files: unoconv, libreoffice -* PDF: ghostscript, pdf2htmlEX - -``` - # required for pdf2htmlEX - sudo add-apt-repository ppa:fontforge/fontforge --yes - sudo add-apt-repository ppa:coolwanglu/pdf2htmlex --yes - sudo apt-get update -qq - sudo apt-get install -qq libpoppler-dev libpoppler-private-dev libspiro-dev libcairo-dev libpango1.0-dev libfreetype6-dev libltdl-dev libfontforge-dev python-imaging python-pip firefox xvfb - # install pdf2htmlEX - git clone https://github.com/coolwanglu/pdf2htmlEX.git - pushd pdf2htmlEX - cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr -DENABLE_SVG=ON . - make - sudo make install - popd - # Installing the rest - sudo apt-get install ghostscript p7zip-full p7zip-rar libreoffice unoconv -``` - -pier9.py --------- - -This script has a list of file formats for various brands of industrial -manufacturing equipment, such as 3d printers, CNC machines, etc. It only -copies files that match these file formats. - -No external dependencies required. - -specific.py ------------ - -As the name suggests, this script copies only specific file formats according -to the configuration provided by the user. - -No external dependencies required. diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..933e1d9 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,56 @@ +Examples +======== + +These are several sanitizers that demonstrate PyCIRCLean's capabilities. Feel free to +adapt or modify any of them to suit your requirements. In order to use any of these scripts, +you will first need to install the PyCIRCLean dependencies (preferably in a virtualenv): + +``` + pip install . +``` + +Requirements per script +======================= + +generic.py +---------- + +This is a script that was used by an older version of CIRCLean. + +Requirements by type of document: +* Office and all text files: unoconv, libreoffice +* PDF: ghostscript, pdf2htmlEX + +``` + # required for pdf2htmlEX + sudo add-apt-repository ppa:fontforge/fontforge --yes + sudo add-apt-repository ppa:coolwanglu/pdf2htmlex --yes + sudo apt-get update -qq + sudo apt-get install -qq libpoppler-dev libpoppler-private-dev libspiro-dev libcairo-dev libpango1.0-dev libfreetype6-dev libltdl-dev libfontforge-dev python-imaging python-pip firefox xvfb + # install pdf2htmlEX + git clone https://github.com/coolwanglu/pdf2htmlEX.git + pushd pdf2htmlEX + cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr -DENABLE_SVG=ON . + make + sudo make install + popd + # Installing the rest + sudo apt-get install ghostscript p7zip-full p7zip-rar libreoffice unoconv +``` + +pier9.py +-------- + +This script contains a list of file formats for various brands of industrial +manufacturing equipment, such as 3d printers, CNC machines, etc. It only +copies files that match these file formats. + +No external dependencies required. + +specific.py +----------- + +As the name suggests, this script copies only specific file formats according +to the configuration provided by the user. + +No external dependencies required. diff --git a/bin/generic.py b/examples/generic.py similarity index 100% rename from bin/generic.py rename to examples/generic.py diff --git a/bin/pier9.py b/examples/pier9.py similarity index 100% rename from bin/pier9.py rename to examples/pier9.py diff --git a/bin/specific.py b/examples/specific.py similarity index 100% rename from bin/specific.py rename to examples/specific.py diff --git a/setup.py b/setup.py index f20da6a..7f84998 100644 --- a/setup.py +++ b/setup.py @@ -12,9 +12,6 @@ setup( description='Standalone CIRCLean/KittenGroomer code.', packages=['kittengroomer'], scripts=[ - 'bin/generic.py', - 'bin/pier9.py', - 'bin/specific.py', 'bin/filecheck.py' ], include_package_data=True, diff --git a/tests/src_invalid/42.zip b/tests/src_invalid/42.zip new file mode 100644 index 0000000000000000000000000000000000000000..e7681537ced5f7abcc7dfb999da856951911649d GIT binary patch literal 42838 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 I_n*~&0V&+h761SM literal 0 HcmV?d00001 diff --git a/tests/src_invalid/blah.zip b/tests/src_invalid/blah.zip new file mode 100644 index 0000000000000000000000000000000000000000..3e809f43758641b17be4cfd469a7f60f09025340 GIT binary patch literal 344 zcmWIWW@h1H0D+Q*D0kr(r_}|3Y!K#TkYPy5NzBko&d*B=4dG;9KJB0vc3_TvSZM_} z10%}|W(Ec@5t5NvtN=ub3MHw2kn0&3fp|+Jh=t^6R*0j~930?{Fb>m+$i@Z3i~~CuXd1}D7^bnZfvjZ$!nr_t IDTu=W0EsR|>Hq)$ literal 0 HcmV?d00001 diff --git a/tests/test_generic.py b/tests/test_generic.py deleted file mode 100644 index a17fb5e..0000000 --- a/tests/test_generic.py +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -import os - -import pytest - -from bin.generic import KittenGroomer, File, main -from tests.logging import save_logs - -skipif_nodeps = pytest.mark.skipif(os.path.exists('/usr/bin/unoconv') is False, - reason="Dependencies aren't installed") - - -@skipif_nodeps -class TestIntegration: - - @pytest.fixture - def src_valid(self): - return os.path.join(os.getcwd(), 'tests/src_valid') - - @pytest.fixture - def src_invalid(self): - return os.path.join(os.getcwd(), 'tests/src_invalid') - - @pytest.fixture - def dst(self): - return os.path.join(os.getcwd(), 'tests/dst') - - def test_generic(self, src_valid, dst): - groomer = KittenGroomer(src_valid, dst, debug=True) - groomer.processdir() - test_description = 'generic_valid' - save_logs(groomer, test_description) - - def test_generic_2(self, src_invalid, dst): - groomer = KittenGroomer(src_invalid, dst, debug=True) - groomer.processdir() - test_description = 'generic_invalid' - save_logs(groomer, test_description) - - -class TestFileHandling: - pass - - # We're going to give KittenGroomer a bunch of files, and it's going to process them - # Maybe we want to make a function that processdir delegates to? Or is it just the File Object that's responsible? - # Ideally we should be able to pass a path to a function and have it do stuff? And then we can test that function? - # So we have a function that takes a path and returns...log info? That makes sense actually. Or some sort of meta data - # The function could maybe be called processfile diff --git a/tests/test_specific_and_pier9.py b/tests/test_specific_and_pier9.py deleted file mode 100644 index d411aa8..0000000 --- a/tests/test_specific_and_pier9.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -import os - -import pytest - -from bin.specific import KittenGroomerSpec -from bin.pier9 import KittenGroomerPier9 -from tests.logging import save_logs - - -@pytest.fixture -def src_valid(): - return os.path.join(os.getcwd(), 'tests/src_valid') - - -@pytest.fixture -def src_invalid(): - return os.path.join(os.getcwd(), 'tests/src_invalid') - - -@pytest.fixture -def dst(): - return os.path.join(os.getcwd(), 'tests/dst') - - -def test_specific_valid(src_valid, dst): - groomer = KittenGroomerSpec(src_valid, dst, debug=True) - groomer.processdir() - test_description = 'specific_valid' - save_logs(groomer, test_description) - - -def test_specific_invalid(src_invalid, dst): - groomer = KittenGroomerSpec(src_invalid, dst, debug=True) - groomer.processdir() - test_description = 'specific_invalid' - save_logs(groomer, test_description) - - -def test_pier9_valid(src_invalid, dst): - groomer = KittenGroomerPier9(src_invalid, dst, debug=True) - groomer.processdir() - test_description = 'pier9_valid' - save_logs(groomer, test_description) - - -def test_pier9_invalid(src_invalid, dst): - groomer = KittenGroomerPier9(src_invalid, dst, debug=True) - groomer.processdir() - test_description = 'pier9_invalid' - save_logs(groomer, test_description) From d06832e52422ad9628ebb47e2cdc1767f8164640 Mon Sep 17 00:00:00 2001 From: Dan Puttick Date: Thu, 19 Jan 2017 16:11:58 -0500 Subject: [PATCH 15/22] Remove kittengroomer/data/ --- MANIFEST.in | 2 +- kittengroomer/data/PDFA_def.ps | 40 --------------------------------- kittengroomer/data/srgb.icc | Bin 2576 -> 0 bytes setup.py | 2 -- 4 files changed, 1 insertion(+), 43 deletions(-) delete mode 100644 kittengroomer/data/PDFA_def.ps delete mode 100644 kittengroomer/data/srgb.icc diff --git a/MANIFEST.in b/MANIFEST.in index 4c93c9d..3194a32 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1 @@ -include kittengroomer/data/* README.md CONTRIBUTING.md CHANGELOG dev-requirements.txt +include README.md CONTRIBUTING.md CHANGELOG dev-requirements.txt diff --git a/kittengroomer/data/PDFA_def.ps b/kittengroomer/data/PDFA_def.ps deleted file mode 100644 index f0ff0d1..0000000 --- a/kittengroomer/data/PDFA_def.ps +++ /dev/null @@ -1,40 +0,0 @@ -%! -% This is a sample prefix file for creating a PDF/A document. -% Feel free to modify entries marked with "Customize". -% This assumes an ICC profile to reside in the file (ISO Coated sb.icc), -% unless the user modifies the corresponding line below. - -% Define entries in the document Info dictionary : -/ICCProfile (srgb.icc) % Customise -def - -[ /Title (Title) % Customise - /DOCINFO pdfmark - -% Define an ICC profile : - -[/_objdef {icc_PDFA} /type /stream /OBJ pdfmark -[{icc_PDFA} -<< - /N currentpagedevice /ProcessColorModel known { - currentpagedevice /ProcessColorModel get dup /DeviceGray eq - {pop 1} { - /DeviceRGB eq - {3}{4} ifelse - } ifelse - } { - (ERROR, unable to determine ProcessColorModel) == flush - } ifelse ->> /PUT pdfmark -[{icc_PDFA} ICCProfile (r) file /PUT pdfmark - -% Define the output intent dictionary : - -[/_objdef {OutputIntent_PDFA} /type /dict /OBJ pdfmark -[{OutputIntent_PDFA} << - /Type /OutputIntent % Must be so (the standard requires). - /S /GTS_PDFA1 % Must be so (the standard requires). - /DestOutputProfile {icc_PDFA} % Must be so (see above). - /OutputConditionIdentifier (sRGB) % Customize ->> /PUT pdfmark -[{Catalog} <> /PUT pdfmark diff --git a/kittengroomer/data/srgb.icc b/kittengroomer/data/srgb.icc deleted file mode 100644 index 627e8feb0ec9635c9672deff48b0d3bb9e1bc024..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2576 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 F=|3)_kh}l@ diff --git a/setup.py b/setup.py index 7f84998..f281da5 100644 --- a/setup.py +++ b/setup.py @@ -14,8 +14,6 @@ setup( scripts=[ 'bin/filecheck.py' ], - include_package_data=True, - package_data={'data': ['PDFA_def.ps', 'srgb.icc']}, test_suite="tests", classifiers=[ 'License :: OSI Approved :: BSD License', From faf534c66f3852045e1865b956961b4254d8ea58 Mon Sep 17 00:00:00 2001 From: Dan Puttick Date: Thu, 19 Jan 2017 16:12:32 -0500 Subject: [PATCH 16/22] Add test.obj to src_invalid --- .travis.yml | 1 - tests/src_invalid/test.obj | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 tests/src_invalid/test.obj diff --git a/.travis.yml b/.travis.yml index 8e3dfb7..0d7bfcc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -70,7 +70,6 @@ install: - wget http://www.sample-videos.com/audio/mp3/india-national-anthem.mp3 - wget http://www.sample-videos.com/video/mp4/720/big_buck_bunny_720p_1mb.mp4 - wget http://thewalter.net/stef/software/rtfx/sample.rtf - - echo "blah" > test.obj - popd script: diff --git a/tests/src_invalid/test.obj b/tests/src_invalid/test.obj new file mode 100644 index 0000000..907b308 --- /dev/null +++ b/tests/src_invalid/test.obj @@ -0,0 +1 @@ +blah From 3b1df108d571e1ee246a7c6e26cdd382bf6f16d8 Mon Sep 17 00:00:00 2001 From: Dan Puttick Date: Thu, 19 Jan 2017 16:19:15 -0500 Subject: [PATCH 17/22] Rename test_helpers --- setup.py | 1 - tests/{test_helpers.py => test_kittengroomer.py} | 0 2 files changed, 1 deletion(-) rename tests/{test_helpers.py => test_kittengroomer.py} (100%) diff --git a/setup.py b/setup.py index f281da5..428cf79 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,6 @@ setup( scripts=[ 'bin/filecheck.py' ], - test_suite="tests", classifiers=[ 'License :: OSI Approved :: BSD License', 'Development Status :: 5 - Production/Stable', diff --git a/tests/test_helpers.py b/tests/test_kittengroomer.py similarity index 100% rename from tests/test_helpers.py rename to tests/test_kittengroomer.py From 4eaa639a7a063173161119a4277126eeecea28ae Mon Sep 17 00:00:00 2001 From: Dan Puttick Date: Thu, 19 Jan 2017 16:23:53 -0500 Subject: [PATCH 18/22] Remove /playground --- README.md | 2 +- playground/README.md | 1 - playground/usb_lookup.py | 16 ---------------- 3 files changed, 1 insertion(+), 18 deletions(-) delete mode 100644 playground/README.md delete mode 100644 playground/usb_lookup.py diff --git a/README.md b/README.md index 91583e2..8330ce1 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ pip install . PyCIRCLean is a simple Python library to handle file checking and sanitization. PyCIRCLean is designed as a simple library that can be overloaded to cover specific checking and sanitization workflows in different organizations like industrial environments or restricted/classified ICT environments. A series of practical examples utilizing PyCIRCLean can be found -in the [./bin](./bin) directory. +in the [./examples](./examples) directory. The following simple example using PyCIRCLean will only copy files with a .conf extension matching the 'text/plain' MIME type. If any other file is found in the source directory, the files won't be copied to the destination directory. diff --git a/playground/README.md b/playground/README.md deleted file mode 100644 index 76a9248..0000000 --- a/playground/README.md +++ /dev/null @@ -1 +0,0 @@ -This directory contains extra files that may or may not be used in the project diff --git a/playground/usb_lookup.py b/playground/usb_lookup.py deleted file mode 100644 index 76f14d7..0000000 --- a/playground/usb_lookup.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from usb.core import find -import usb.control - - -def is_mass_storage(dev): - import usb.util - for cfg in dev: - if usb.util.find_descriptor(cfg, bInterfaceClass=8) is not None: - return True - - -for mass in find(find_all=True, custom_match=is_mass_storage): - print(mass) From 573cf51b69863e13f50430a10b608316c67958ee Mon Sep 17 00:00:00 2001 From: Dan Puttick Date: Thu, 19 Jan 2017 16:28:22 -0500 Subject: [PATCH 19/22] Switch back to main officedissector repo --- .travis.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0d7bfcc..c07ab02 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,23 +30,22 @@ install: - wget https://didierstevens.com/files/software/pdfid_v0_2_1.zip - unzip pdfid_v0_2_1.zip - pip install -U pip - - pip install lxml exifread pillow - - pip install olefile + - pip install lxml exifread pillow olefile - pip install git+https://github.com/decalage2/oletools.git - - pip install git+https://github.com/Rafiot/officedissector.git - # Module dependencies + - pip install git+https://github.com/grierforensics/officedissector.git + # PyCIRCLean dependencies - pip install -r dev-requirements.txt - pip install coveralls codecov # Testing dependencies - sudo apt-get install rar # Prepare tests - # Zoo + # Malware from theZoo - git clone https://github.com/Rafiot/theZoo.git - pushd theZoo/malwares/Binaries - python unpackall.py - popd - mv theZoo/malwares/Binaries/out tests/src_invalid/ - # Path traversal + # Path traversal attacks - git clone https://github.com/jwilk/path-traversal-samples - pushd path-traversal-samples - pushd zip @@ -67,6 +66,7 @@ install: - unzip -o fraunhoferlibrary.zip - rm fraunhoferlibrary.zip - 7z x -p42 42.zip + # Some random samples - wget http://www.sample-videos.com/audio/mp3/india-national-anthem.mp3 - wget http://www.sample-videos.com/video/mp4/720/big_buck_bunny_720p_1mb.mp4 - wget http://thewalter.net/stef/software/rtfx/sample.rtf From fd30fb3e088f311217aa709c5ba1cba4002c5685 Mon Sep 17 00:00:00 2001 From: Dan Puttick Date: Wed, 21 Dec 2016 14:48:07 -0500 Subject: [PATCH 20/22] Change _run_process() to use builtin timeout parameter NOTE: this change breaks Python 2 compatability: subprocess.check_call does not take a timeout argument in Python 2.7 --- bin/filecheck.py | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/bin/filecheck.py b/bin/filecheck.py index e9022bb..04a599c 100644 --- a/bin/filecheck.py +++ b/bin/filecheck.py @@ -4,7 +4,6 @@ import os import mimetypes import shlex import subprocess -import time import zipfile import oletools.oleid @@ -206,27 +205,14 @@ class KittenGroomerFileCheck(KittenGroomerBase): else: tmp_log.debug(self.cur_file.log_string) - def _run_process(self, command_string, timeout=0, background=False): + def _run_process(self, command_string, timeout=None): """Run command_string in a subprocess, wait until it finishes.""" - if timeout != 0: - deadline = time.time() + timeout - else: - deadline = None args = shlex.split(command_string) with open(self.log_debug_err, 'ab') as stderr, open(self.log_debug_out, 'ab') as stdout: - p = subprocess.Popen(args, stdout=stdout, stderr=stderr) - if background: - # This timer is here to make sure the unoconv listener is properly started. - time.sleep(10) - return True - while True: - code = p.poll() - if code is not None: - break - if deadline is not None and time.time() > deadline: - p.kill() - break - time.sleep(1) + try: + subprocess.check_call(args, stdout=stdout, stderr=stderr, timeout=timeout) + except (subprocess.TimeoutExpired, subprocess.CalledProcessError): + return return True ####################### From 833ade600883a4ccf59d91b7805806ff004b7e95 Mon Sep 17 00:00:00 2001 From: Dan Puttick Date: Thu, 19 Jan 2017 17:06:34 -0500 Subject: [PATCH 21/22] Change to Python 3 compatible only --- .travis.yml | 3 +-- README.md | 2 +- setup.py | 1 - tests/test_kittengroomer.py | 18 ++++-------------- tox.ini | 2 +- 5 files changed, 7 insertions(+), 19 deletions(-) diff --git a/.travis.yml b/.travis.yml index c07ab02..b778bf8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: python python: - - 2.7 - 3.3 - 3.4 - 3.5 @@ -73,7 +72,7 @@ install: - popd script: - - travis_wait 60 py.test --cov=kittengroomer --cov=bin tests/ + - travis_wait 30 py.test --cov=kittengroomer --cov=bin tests/ notifications: email: diff --git a/README.md b/README.md index 8330ce1..19eb6d3 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ PyCIRCLean is the core Python code used by [CIRCLean](https://github.com/CIRCL/Circlean/), an open-source USB key and document sanitizer created by [CIRCL](https://www.circl.lu/). This module has been separated from the device-specific scripts and can be used for dedicated security applications to sanitize documents from hostile environments -to trusted environments. +to trusted environments. PyCIRCLean is currently Python 3.3+ only. # Installation diff --git a/setup.py b/setup.py index 428cf79..20b4454 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,6 @@ setup( 'Development Status :: 5 - Production/Stable', 'Environment :: Console', 'Intended Audience :: Science/Research', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Topic :: Communications :: File Sharing', 'Topic :: Security', diff --git a/tests/test_kittengroomer.py b/tests/test_kittengroomer.py index 0d0c338..9698a95 100644 --- a/tests/test_kittengroomer.py +++ b/tests/test_kittengroomer.py @@ -2,14 +2,12 @@ # -*- coding: utf-8 -*- import os -import sys import pytest from kittengroomer import FileBase, KittenGroomerBase from kittengroomer.helpers import ImplementationRequired -PY3 = sys.version_info.major == 3 skip = pytest.mark.skip xfail = pytest.mark.xfail fixture = pytest.fixture @@ -89,18 +87,10 @@ class TestFileBase: def test_create_broken(self, tmpdir): with pytest.raises(TypeError): file_no_args = FileBase() - if PY3: - with pytest.raises(FileNotFoundError): - file_empty_args = FileBase('', '') - else: - with pytest.raises(IOError): - file_empty_args = FileBase('', '') - if PY3: - with pytest.raises(IsADirectoryError): - file_directory = FileBase(tmpdir.strpath, tmpdir.strpath) - else: - with pytest.raises(IOError): - file_directory = FileBase(tmpdir.strpath, tmpdir.strpath) + with pytest.raises(FileNotFoundError): + file_empty_args = FileBase('', '') + with pytest.raises(IsADirectoryError): + file_directory = FileBase(tmpdir.strpath, tmpdir.strpath) # are there other cases here? path to a file that doesn't exist? permissions? def test_init(self, generic_conf_file): diff --git a/tox.ini b/tox.ini index 40b9ad6..0215047 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist=py27,py35 +envlist=py35 [testenv] deps=-rdev-requirements.txt commands= pytest --cov=kittengroomer --cov=bin From 3041c6303fb271ce60b13b90babb961f4c5ab663 Mon Sep 17 00:00:00 2001 From: Dan Puttick Date: Thu, 19 Jan 2017 17:48:14 -0500 Subject: [PATCH 22/22] Update changelog to version 2.1 --- CHANGELOG | 9 --------- CHANGELOG.md | 19 +++++++++++++++++++ setup.py | 2 +- 3 files changed, 20 insertions(+), 10 deletions(-) delete mode 100644 CHANGELOG create mode 100644 CHANGELOG.md diff --git a/CHANGELOG b/CHANGELOG deleted file mode 100644 index 7a1ad90..0000000 --- a/CHANGELOG +++ /dev/null @@ -1,9 +0,0 @@ -Changelog -========= - -2.1.0 ---- - -New features: - -Fixes: diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..df1a217 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,19 @@ +Changelog +========= + +2.1.0 +--- + +New features: +- Dropped Python 2.7 support: PyCIRCLean is now Python 3.3+ only +- Tests are now easier to write and run: we have support for pytest and tox! +- More documentation: both docstrings and more detailed readmes +- Added more types of examples for testing +- The Travis build now runs in ~10 minutes vs. ~30 minutes before + + +Fixes: +- Extension matching now catches lower/upper case errors +- Fixed remaining python 3 issues with filecheck.py +- Fixed support for .rtf files +- Many other small filetype related fixes diff --git a/setup.py b/setup.py index 20b4454..c11f64d 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import setup setup( name='kittengroomer', - version='2.0.2', + version='2.1', author='Raphaël Vinot', author_email='raphael.vinot@circl.lu', maintainer='Raphaël Vinot',