diff options
| author | jvoisin | 2019-10-12 16:13:49 -0700 |
|---|---|---|
| committer | jvoisin | 2019-10-12 16:13:49 -0700 |
| commit | 5f0b3beb46d09af26107fe5f80e63ddccb127a59 (patch) | |
| tree | f3d46e6e9dac60daa304d212bed62b17c019f7eb | |
| parent | 3cef7fe7fc81c1495a461a8594b1df69467536ea (diff) | |
Add a way to disable the sandbox
Due to bubblewrap's pickiness, mat2 can now be run
without a sandbox, even if bubblewrap is installed.
Diffstat (limited to '')
| -rw-r--r-- | libmat2/abstract.py | 1 | ||||
| -rw-r--r-- | libmat2/bubblewrap.py (renamed from libmat2/subprocess.py) | 0 | ||||
| -rw-r--r-- | libmat2/exiftool.py | 22 | ||||
| -rw-r--r-- | libmat2/video.py | 12 | ||||
| -rwxr-xr-x | mat2 | 14 | ||||
| -rw-r--r-- | tests/test_climat2.py | 31 | ||||
| -rw-r--r-- | tests/test_libmat2.py | 40 |
7 files changed, 100 insertions, 20 deletions
diff --git a/libmat2/abstract.py b/libmat2/abstract.py index 8861966..5cfd0f2 100644 --- a/libmat2/abstract.py +++ b/libmat2/abstract.py | |||
| @@ -32,6 +32,7 @@ class AbstractParser(abc.ABC): | |||
| 32 | 32 | ||
| 33 | self.output_filename = fname + '.cleaned' + extension | 33 | self.output_filename = fname + '.cleaned' + extension |
| 34 | self.lightweight_cleaning = False | 34 | self.lightweight_cleaning = False |
| 35 | self.sandbox = True | ||
| 35 | 36 | ||
| 36 | @abc.abstractmethod | 37 | @abc.abstractmethod |
| 37 | def get_meta(self) -> Dict[str, Union[str, dict]]: | 38 | def get_meta(self) -> Dict[str, Union[str, dict]]: |
diff --git a/libmat2/subprocess.py b/libmat2/bubblewrap.py index fb6fc9d..fb6fc9d 100644 --- a/libmat2/subprocess.py +++ b/libmat2/bubblewrap.py | |||
diff --git a/libmat2/exiftool.py b/libmat2/exiftool.py index 024f490..89081e2 100644 --- a/libmat2/exiftool.py +++ b/libmat2/exiftool.py | |||
| @@ -2,10 +2,11 @@ import functools | |||
| 2 | import json | 2 | import json |
| 3 | import logging | 3 | import logging |
| 4 | import os | 4 | import os |
| 5 | import subprocess | ||
| 5 | from typing import Dict, Union, Set | 6 | from typing import Dict, Union, Set |
| 6 | 7 | ||
| 7 | from . import abstract | 8 | from . import abstract |
| 8 | from . import subprocess | 9 | from . import bubblewrap |
| 9 | 10 | ||
| 10 | # Make pyflakes happy | 11 | # Make pyflakes happy |
| 11 | assert Set | 12 | assert Set |
| @@ -19,9 +20,13 @@ class ExiftoolParser(abstract.AbstractParser): | |||
| 19 | meta_allowlist = set() # type: Set[str] | 20 | meta_allowlist = set() # type: Set[str] |
| 20 | 21 | ||
| 21 | def get_meta(self) -> Dict[str, Union[str, dict]]: | 22 | def get_meta(self) -> Dict[str, Union[str, dict]]: |
| 22 | out = subprocess.run([_get_exiftool_path(), '-json', self.filename], | 23 | if self.sandbox: |
| 23 | input_filename=self.filename, | 24 | out = bubblewrap.run([_get_exiftool_path(), '-json', self.filename], |
| 24 | check=True, stdout=subprocess.PIPE).stdout | 25 | input_filename=self.filename, |
| 26 | check=True, stdout=subprocess.PIPE).stdout | ||
| 27 | else: | ||
| 28 | out = subprocess.run([_get_exiftool_path(), '-json', self.filename], | ||
| 29 | check=True, stdout=subprocess.PIPE).stdout | ||
| 25 | meta = json.loads(out.decode('utf-8'))[0] | 30 | meta = json.loads(out.decode('utf-8'))[0] |
| 26 | for key in self.meta_allowlist: | 31 | for key in self.meta_allowlist: |
| 27 | meta.pop(key, None) | 32 | meta.pop(key, None) |
| @@ -48,9 +53,12 @@ class ExiftoolParser(abstract.AbstractParser): | |||
| 48 | '-o', self.output_filename, | 53 | '-o', self.output_filename, |
| 49 | self.filename] | 54 | self.filename] |
| 50 | try: | 55 | try: |
| 51 | subprocess.run(cmd, check=True, | 56 | if self.sandbox: |
| 52 | input_filename=self.filename, | 57 | bubblewrap.run(cmd, check=True, |
| 53 | output_filename=self.output_filename) | 58 | input_filename=self.filename, |
| 59 | output_filename=self.output_filename) | ||
| 60 | else: | ||
| 61 | subprocess.run(cmd, check=True) | ||
| 54 | except subprocess.CalledProcessError as e: # pragma: no cover | 62 | except subprocess.CalledProcessError as e: # pragma: no cover |
| 55 | logging.error("Something went wrong during the processing of %s: %s", self.filename, e) | 63 | logging.error("Something went wrong during the processing of %s: %s", self.filename, e) |
| 56 | return False | 64 | return False |
diff --git a/libmat2/video.py b/libmat2/video.py index 1492ba1..2b33bc0 100644 --- a/libmat2/video.py +++ b/libmat2/video.py | |||
| @@ -1,3 +1,4 @@ | |||
| 1 | import subprocess | ||
| 1 | import functools | 2 | import functools |
| 2 | import os | 3 | import os |
| 3 | import logging | 4 | import logging |
| @@ -5,7 +6,7 @@ import logging | |||
| 5 | from typing import Dict, Union | 6 | from typing import Dict, Union |
| 6 | 7 | ||
| 7 | from . import exiftool | 8 | from . import exiftool |
| 8 | from . import subprocess | 9 | from . import bubblewrap |
| 9 | 10 | ||
| 10 | 11 | ||
| 11 | class AbstractFFmpegParser(exiftool.ExiftoolParser): | 12 | class AbstractFFmpegParser(exiftool.ExiftoolParser): |
| @@ -33,9 +34,12 @@ class AbstractFFmpegParser(exiftool.ExiftoolParser): | |||
| 33 | '-flags:a', '+bitexact', # don't add any metadata | 34 | '-flags:a', '+bitexact', # don't add any metadata |
| 34 | self.output_filename] | 35 | self.output_filename] |
| 35 | try: | 36 | try: |
| 36 | subprocess.run(cmd, check=True, | 37 | if self.sandbox: |
| 37 | input_filename=self.filename, | 38 | bubblewrap.run(cmd, check=True, |
| 38 | output_filename=self.output_filename) | 39 | input_filename=self.filename, |
| 40 | output_filename=self.output_filename) | ||
| 41 | else: | ||
| 42 | subprocess.run(cmd, check=True) | ||
| 39 | except subprocess.CalledProcessError as e: | 43 | except subprocess.CalledProcessError as e: |
| 40 | logging.error("Something went wrong during the processing of %s: %s", self.filename, e) | 44 | logging.error("Something went wrong during the processing of %s: %s", self.filename, e) |
| 41 | return False | 45 | return False |
| @@ -55,6 +55,8 @@ def create_arg_parser() -> argparse.ArgumentParser: | |||
| 55 | ', '.join(p.value for p in UnknownMemberPolicy)) | 55 | ', '.join(p.value for p in UnknownMemberPolicy)) |
| 56 | parser.add_argument('--inplace', action='store_true', | 56 | parser.add_argument('--inplace', action='store_true', |
| 57 | help='clean in place, without backup') | 57 | help='clean in place, without backup') |
| 58 | parser.add_argument('--no-sandbox', dest='sandbox', action='store_true', | ||
| 59 | default=False, help='Disable bubblewrap\'s sandboxing.') | ||
| 58 | 60 | ||
| 59 | excl_group = parser.add_mutually_exclusive_group() | 61 | excl_group = parser.add_mutually_exclusive_group() |
| 60 | excl_group.add_argument('files', nargs='*', help='the files to process', | 62 | excl_group.add_argument('files', nargs='*', help='the files to process', |
| @@ -78,7 +80,7 @@ def create_arg_parser() -> argparse.ArgumentParser: | |||
| 78 | return parser | 80 | return parser |
| 79 | 81 | ||
| 80 | 82 | ||
| 81 | def show_meta(filename: str): | 83 | def show_meta(filename: str, sandbox: bool): |
| 82 | if not __check_file(filename): | 84 | if not __check_file(filename): |
| 83 | return | 85 | return |
| 84 | 86 | ||
| @@ -86,6 +88,7 @@ def show_meta(filename: str): | |||
| 86 | if p is None: | 88 | if p is None: |
| 87 | print("[-] %s's format (%s) is not supported" % (filename, mtype)) | 89 | print("[-] %s's format (%s) is not supported" % (filename, mtype)) |
| 88 | return | 90 | return |
| 91 | p.sandbox = sandbox | ||
| 89 | __print_meta(filename, p.get_meta()) | 92 | __print_meta(filename, p.get_meta()) |
| 90 | 93 | ||
| 91 | 94 | ||
| @@ -116,7 +119,7 @@ def __print_meta(filename: str, metadata: dict, depth: int = 1): | |||
| 116 | print(padding + " %s: harmful content" % k) | 119 | print(padding + " %s: harmful content" % k) |
| 117 | 120 | ||
| 118 | 121 | ||
| 119 | def clean_meta(filename: str, is_lightweight: bool, inplace: bool, | 122 | def clean_meta(filename: str, is_lightweight: bool, inplace: bool, sandbox: bool, |
| 120 | policy: UnknownMemberPolicy) -> bool: | 123 | policy: UnknownMemberPolicy) -> bool: |
| 121 | mode = (os.R_OK | os.W_OK) if inplace else os.R_OK | 124 | mode = (os.R_OK | os.W_OK) if inplace else os.R_OK |
| 122 | if not __check_file(filename, mode): | 125 | if not __check_file(filename, mode): |
| @@ -128,6 +131,7 @@ def clean_meta(filename: str, is_lightweight: bool, inplace: bool, | |||
| 128 | return False | 131 | return False |
| 129 | p.unknown_member_policy = policy | 132 | p.unknown_member_policy = policy |
| 130 | p.lightweight_cleaning = is_lightweight | 133 | p.lightweight_cleaning = is_lightweight |
| 134 | p.sandbox = sandbox | ||
| 131 | 135 | ||
| 132 | try: | 136 | try: |
| 133 | logging.debug('Cleaning %s…', filename) | 137 | logging.debug('Cleaning %s…', filename) |
| @@ -140,7 +144,6 @@ def clean_meta(filename: str, is_lightweight: bool, inplace: bool, | |||
| 140 | return False | 144 | return False |
| 141 | 145 | ||
| 142 | 146 | ||
| 143 | |||
| 144 | def show_parsers(): | 147 | def show_parsers(): |
| 145 | print('[+] Supported formats:') | 148 | print('[+] Supported formats:') |
| 146 | formats = set() # Set[str] | 149 | formats = set() # Set[str] |
| @@ -171,6 +174,7 @@ def __get_files_recursively(files: List[str]) -> List[str]: | |||
| 171 | ret.add(f) | 174 | ret.add(f) |
| 172 | return list(ret) | 175 | return list(ret) |
| 173 | 176 | ||
| 177 | |||
| 174 | def main() -> int: | 178 | def main() -> int: |
| 175 | arg_parser = create_arg_parser() | 179 | arg_parser = create_arg_parser() |
| 176 | args = arg_parser.parse_args() | 180 | args = arg_parser.parse_args() |
| @@ -193,7 +197,7 @@ def main() -> int: | |||
| 193 | 197 | ||
| 194 | elif args.show: | 198 | elif args.show: |
| 195 | for f in __get_files_recursively(args.files): | 199 | for f in __get_files_recursively(args.files): |
| 196 | show_meta(f) | 200 | show_meta(f, args.sandbox) |
| 197 | return 0 | 201 | return 0 |
| 198 | 202 | ||
| 199 | else: | 203 | else: |
| @@ -210,7 +214,7 @@ def main() -> int: | |||
| 210 | futures = list() | 214 | futures = list() |
| 211 | for f in files: | 215 | for f in files: |
| 212 | future = executor.submit(clean_meta, f, args.lightweight, | 216 | future = executor.submit(clean_meta, f, args.lightweight, |
| 213 | inplace, policy) | 217 | inplace, args.sandbox, policy) |
| 214 | futures.append(future) | 218 | futures.append(future) |
| 215 | for future in concurrent.futures.as_completed(futures): | 219 | for future in concurrent.futures.as_completed(futures): |
| 216 | no_failure &= future.result() | 220 | no_failure &= future.result() |
diff --git a/tests/test_climat2.py b/tests/test_climat2.py index 6cf8a39..9d816b1 100644 --- a/tests/test_climat2.py +++ b/tests/test_climat2.py | |||
| @@ -20,17 +20,17 @@ class TestHelp(unittest.TestCase): | |||
| 20 | def test_help(self): | 20 | def test_help(self): |
| 21 | proc = subprocess.Popen(mat2_binary + ['--help'], stdout=subprocess.PIPE) | 21 | proc = subprocess.Popen(mat2_binary + ['--help'], stdout=subprocess.PIPE) |
| 22 | stdout, _ = proc.communicate() | 22 | stdout, _ = proc.communicate() |
| 23 | self.assertIn(b'mat2 [-h] [-V] [--unknown-members policy] [--inplace] [-v] [-l]', | 23 | self.assertIn(b'mat2 [-h] [-V] [--unknown-members policy] [--inplace] [--no-sandbox]', |
| 24 | stdout) | 24 | stdout) |
| 25 | self.assertIn(b'[--check-dependencies] [-L | -s]', stdout) | 25 | self.assertIn(b' [-v] [-l] [--check-dependencies] [-L | -s]', stdout) |
| 26 | self.assertIn(b'[files [files ...]]', stdout) | 26 | self.assertIn(b'[files [files ...]]', stdout) |
| 27 | 27 | ||
| 28 | def test_no_arg(self): | 28 | def test_no_arg(self): |
| 29 | proc = subprocess.Popen(mat2_binary, stdout=subprocess.PIPE) | 29 | proc = subprocess.Popen(mat2_binary, stdout=subprocess.PIPE) |
| 30 | stdout, _ = proc.communicate() | 30 | stdout, _ = proc.communicate() |
| 31 | self.assertIn(b'mat2 [-h] [-V] [--unknown-members policy] [--inplace] [-v] [-l]', | 31 | self.assertIn(b'mat2 [-h] [-V] [--unknown-members policy] [--inplace] [--no-sandbox]', |
| 32 | stdout) | 32 | stdout) |
| 33 | self.assertIn(b'[--check-dependencies] [-L | -s]', stdout) | 33 | self.assertIn(b' [-v] [-l] [--check-dependencies] [-L | -s]', stdout) |
| 34 | self.assertIn(b'[files [files ...]]', stdout) | 34 | self.assertIn(b'[files [files ...]]', stdout) |
| 35 | 35 | ||
| 36 | 36 | ||
| @@ -40,12 +40,14 @@ class TestVersion(unittest.TestCase): | |||
| 40 | stdout, _ = proc.communicate() | 40 | stdout, _ = proc.communicate() |
| 41 | self.assertTrue(stdout.startswith(b'MAT2 ')) | 41 | self.assertTrue(stdout.startswith(b'MAT2 ')) |
| 42 | 42 | ||
| 43 | |||
| 43 | class TestDependencies(unittest.TestCase): | 44 | class TestDependencies(unittest.TestCase): |
| 44 | def test_dependencies(self): | 45 | def test_dependencies(self): |
| 45 | proc = subprocess.Popen(mat2_binary + ['--check-dependencies'], stdout=subprocess.PIPE) | 46 | proc = subprocess.Popen(mat2_binary + ['--check-dependencies'], stdout=subprocess.PIPE) |
| 46 | stdout, _ = proc.communicate() | 47 | stdout, _ = proc.communicate() |
| 47 | self.assertTrue(b'MAT2' in stdout) | 48 | self.assertTrue(b'MAT2' in stdout) |
| 48 | 49 | ||
| 50 | |||
| 49 | class TestReturnValue(unittest.TestCase): | 51 | class TestReturnValue(unittest.TestCase): |
| 50 | def test_nonzero(self): | 52 | def test_nonzero(self): |
| 51 | ret = subprocess.call(mat2_binary + ['mat2'], stdout=subprocess.DEVNULL) | 53 | ret = subprocess.call(mat2_binary + ['mat2'], stdout=subprocess.DEVNULL) |
| @@ -112,6 +114,25 @@ class TestCleanMeta(unittest.TestCase): | |||
| 112 | 114 | ||
| 113 | os.remove('./tests/data/clean.jpg') | 115 | os.remove('./tests/data/clean.jpg') |
| 114 | 116 | ||
| 117 | def test_jpg_nosandbox(self): | ||
| 118 | shutil.copy('./tests/data/dirty.jpg', './tests/data/clean.jpg') | ||
| 119 | |||
| 120 | proc = subprocess.Popen(mat2_binary + ['--show', '--no-sandbox', './tests/data/clean.jpg'], | ||
| 121 | stdout=subprocess.PIPE) | ||
| 122 | stdout, _ = proc.communicate() | ||
| 123 | self.assertIn(b'Comment: Created with GIMP', stdout) | ||
| 124 | |||
| 125 | proc = subprocess.Popen(mat2_binary + ['./tests/data/clean.jpg'], | ||
| 126 | stdout=subprocess.PIPE) | ||
| 127 | stdout, _ = proc.communicate() | ||
| 128 | |||
| 129 | proc = subprocess.Popen(mat2_binary + ['--show', './tests/data/clean.cleaned.jpg'], | ||
| 130 | stdout=subprocess.PIPE) | ||
| 131 | stdout, _ = proc.communicate() | ||
| 132 | self.assertNotIn(b'Comment: Created with GIMP', stdout) | ||
| 133 | |||
| 134 | os.remove('./tests/data/clean.jpg') | ||
| 135 | |||
| 115 | 136 | ||
| 116 | class TestIsSupported(unittest.TestCase): | 137 | class TestIsSupported(unittest.TestCase): |
| 117 | def test_pdf(self): | 138 | def test_pdf(self): |
| @@ -181,6 +202,7 @@ class TestGetMeta(unittest.TestCase): | |||
| 181 | self.assertIn(b'i am a : various comment', stdout) | 202 | self.assertIn(b'i am a : various comment', stdout) |
| 182 | self.assertIn(b'artist: jvoisin', stdout) | 203 | self.assertIn(b'artist: jvoisin', stdout) |
| 183 | 204 | ||
| 205 | |||
| 184 | class TestControlCharInjection(unittest.TestCase): | 206 | class TestControlCharInjection(unittest.TestCase): |
| 185 | def test_jpg(self): | 207 | def test_jpg(self): |
| 186 | proc = subprocess.Popen(mat2_binary + ['--show', './tests/data/control_chars.jpg'], | 208 | proc = subprocess.Popen(mat2_binary + ['--show', './tests/data/control_chars.jpg'], |
| @@ -242,6 +264,7 @@ class TestCommandLineParallel(unittest.TestCase): | |||
| 242 | os.remove(path) | 264 | os.remove(path) |
| 243 | os.remove('./tests/data/dirty_%d.docx' % i) | 265 | os.remove('./tests/data/dirty_%d.docx' % i) |
| 244 | 266 | ||
| 267 | |||
| 245 | class TestInplaceCleaning(unittest.TestCase): | 268 | class TestInplaceCleaning(unittest.TestCase): |
| 246 | def test_cleaning(self): | 269 | def test_cleaning(self): |
| 247 | shutil.copy('./tests/data/dirty.jpg', './tests/data/clean.jpg') | 270 | shutil.copy('./tests/data/dirty.jpg', './tests/data/clean.jpg') |
diff --git a/tests/test_libmat2.py b/tests/test_libmat2.py index 13d861d..20e6a01 100644 --- a/tests/test_libmat2.py +++ b/tests/test_libmat2.py | |||
| @@ -721,3 +721,43 @@ class TestCleaningArchives(unittest.TestCase): | |||
| 721 | os.remove('./tests/data/dirty.tar.xz') | 721 | os.remove('./tests/data/dirty.tar.xz') |
| 722 | os.remove('./tests/data/dirty.cleaned.tar.xz') | 722 | os.remove('./tests/data/dirty.cleaned.tar.xz') |
| 723 | os.remove('./tests/data/dirty.cleaned.cleaned.tar.xz') | 723 | os.remove('./tests/data/dirty.cleaned.cleaned.tar.xz') |
| 724 | |||
| 725 | class TestNoSandbox(unittest.TestCase): | ||
| 726 | def test_avi_nosandbox(self): | ||
| 727 | shutil.copy('./tests/data/dirty.avi', './tests/data/clean.avi') | ||
| 728 | p = video.AVIParser('./tests/data/clean.avi') | ||
| 729 | p.sandbox = False | ||
| 730 | |||
| 731 | meta = p.get_meta() | ||
| 732 | self.assertEqual(meta['Software'], 'MEncoder SVN-r33148-4.0.1') | ||
| 733 | |||
| 734 | ret = p.remove_all() | ||
| 735 | self.assertTrue(ret) | ||
| 736 | |||
| 737 | p = video.AVIParser('./tests/data/clean.cleaned.avi') | ||
| 738 | self.assertEqual(p.get_meta(), {}) | ||
| 739 | self.assertTrue(p.remove_all()) | ||
| 740 | |||
| 741 | os.remove('./tests/data/clean.avi') | ||
| 742 | os.remove('./tests/data/clean.cleaned.avi') | ||
| 743 | os.remove('./tests/data/clean.cleaned.cleaned.avi') | ||
| 744 | |||
| 745 | def test_png_nosandbox(self): | ||
| 746 | shutil.copy('./tests/data/dirty.png', './tests/data/clean.png') | ||
| 747 | p = images.PNGParser('./tests/data/clean.png') | ||
| 748 | p.sandbox = False | ||
| 749 | p.lightweight_cleaning = True | ||
| 750 | |||
| 751 | meta = p.get_meta() | ||
| 752 | self.assertEqual(meta['Comment'], 'This is a comment, be careful!') | ||
| 753 | |||
| 754 | ret = p.remove_all() | ||
| 755 | self.assertTrue(ret) | ||
| 756 | |||
| 757 | p = images.PNGParser('./tests/data/clean.cleaned.png') | ||
| 758 | self.assertEqual(p.get_meta(), {}) | ||
| 759 | self.assertTrue(p.remove_all()) | ||
| 760 | |||
| 761 | os.remove('./tests/data/clean.png') | ||
| 762 | os.remove('./tests/data/clean.cleaned.png') | ||
| 763 | os.remove('./tests/data/clean.cleaned.cleaned.png') | ||
