diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml new file mode 100644 index 0000000..720419e --- /dev/null +++ b/.github/workflows/unittest.yml @@ -0,0 +1,22 @@ +name: UnitTest + +on: + pull_request: + push: + +jobs: + unittest: + runs-on: ubuntu-latest + name: Run UnitTests + steps: + - uses: actions/checkout@v2 + - name: Set up Python + id: setup-python + uses: actions/setup-python@v2 + with: + python-version: 3 + - name: Install requirements + run: pip install -r requirements.txt + - name: Launch tests + run: python -m unittest discover test + diff --git a/.gitignore b/.gitignore index 0179ed4..19afeff 100755 --- a/.gitignore +++ b/.gitignore @@ -129,3 +129,4 @@ dmypy.json .pyre/ tracevis_data/ +.coverage diff --git a/README.md b/README.md index 043bc5b..18529f6 100755 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ Traceroute with any packet. Visualize the routes. Discover Middleboxes and Firew [![CodeQL](https://github.com/wikicensorship/tracevis/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/wikicensorship/tracevis/actions/workflows/codeql-analysis.yml) [![Dockerise](https://github.com/wikicensorship/tracevis/actions/workflows/docker.yml/badge.svg)](https://github.com/wikicensorship/tracevis/actions/workflows/docker.yml) +[![unittest](https://github.com/wikicensorship/tracevis/actions/workflows/unittest.yml/badge.svg)](https://github.com/wikicensorship/tracevis/actions/workflows/unittest.yml) + TraceVis is a research project whose main goal is to find middleboxes. Where a packet is tampered with or blocked. This tool also has other features such as downloading and visualizing traceroute data from RIPE Atlas probes. diff --git a/requirements.txt b/requirements.txt index b532f95..47bdcc6 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ scapy -pyvis \ No newline at end of file +pyvis diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/test_operationality.py b/test/test_operationality.py new file mode 100644 index 0000000..37e2ee3 --- /dev/null +++ b/test/test_operationality.py @@ -0,0 +1,121 @@ +import unittest +import tracevis +import sys + + +class TestArguments(unittest.TestCase): + def test_help(self): + from io import StringIO + out,err = StringIO(), StringIO() + sys.stdout, sys.stderr = out, err + with self.assertRaises(SystemExit): + tracevis.get_args(['-h'], auto_exit=True) + self.assertIn(err.getvalue(), "usage:") + sys.stdout, sys.stderr = sys.__stdout__, sys.__stderr__ + + def test_no_args(self): + from io import StringIO + out,err = StringIO(), StringIO() + sys.stdout, sys.stderr = out, err + with self.assertRaises(SystemExit): + tracevis.get_args([], auto_exit=True) + self.assertIn(err.getvalue(), "usage:") + sys.stdout, sys.stderr = sys.__stdout__, sys.__stderr__ + + + def test_defaults(self): + from io import StringIO + out,err = StringIO(), StringIO() + sys.stdout, sys.stderr = out, err + args = tracevis.get_args([], auto_exit=False) + expected = {'config_file': None, 'name': None, 'ips': None, 'packet': False, 'packet_input_method': 'hex', + 'packet_data': None, 'dns': False, 'dnstcp': False, 'continue': False, 'maxttl': None, + 'timeout': None, 'repeat': None, 'ripe': None, 'ripemids': None, 'file': None, 'csv': False, + 'csvraw': False, 'attach': False, 'label': None, 'domain1': None, 'domain2': None, 'annot1': None, + 'annot2': None, 'rexmit': False, 'paris': False, 'options': 'new', 'iface': None, 'show_ifaces': False, 'port': None} + self.assertEqual(args, expected) + sys.stdout, sys.stderr = sys.__stdout__, sys.__stderr__ + + def test_config_file(self): + from io import StringIO + import os, json + md = self.maxDiff + self.maxDiff = None + out,err = StringIO(), StringIO() + sys.stdout, sys.stderr = out, err + samples_dir = 'samples/' + for file in os.listdir(samples_dir): + args = tracevis.get_args(['--config-file', os.path.join(samples_dir, file)], auto_exit=False) + with open(os.path.join(samples_dir, file), 'r') as f: + expected = json.load(f) + del args['config_file'] + for k,v in args.items(): + if k in expected: + self.assertEqual(v, expected[k]) + + sys.stdout, sys.stderr = sys.__stdout__, sys.__stderr__ + self.maxDiff = md + + def test_dns_mode(self): + from io import StringIO + out,err = StringIO(), StringIO() + sys.stdout, sys.stderr = out, err + args = tracevis.get_args(['--dns'], auto_exit=False) + expected = {'config_file': None, 'name': None, 'ips': None, 'packet': False, 'packet_input_method': None, + 'packet_data': None, 'dns': True, 'dnstcp': False, 'continue': False, 'maxttl': None, + 'timeout': None, 'repeat': None, 'ripe': None, 'ripemids': None, 'file': None, 'csv': False, + 'csvraw': False, 'attach': False, 'label': None, 'domain1': None, 'domain2': None, 'annot1': None, + 'annot2': None, 'rexmit': False, 'paris': False, 'options': 'new', 'iface': None, 'show_ifaces': False, 'port': None} + self.assertEqual(args, expected) + sys.stdout, sys.stderr = sys.__stdout__, sys.__stderr__ + + def test_packet_mode(self): + from io import StringIO + out,err = StringIO(), StringIO() + sys.stdout, sys.stderr = out, err + args = tracevis.get_args(['--packet'], auto_exit=False) + expected = {'config_file': None, 'name': None, 'ips': None, 'packet': True, 'packet_input_method': 'hex', + 'packet_data': None, 'dns': False, 'dnstcp': False, 'continue': False, 'maxttl': None, + 'timeout': None, 'repeat': None, 'ripe': None, 'ripemids': None, 'file': None, 'csv': False, + 'csvraw': False, 'attach': False, 'label': None, 'domain1': None, 'domain2': None, 'annot1': None, + 'annot2': None, 'rexmit': False, 'paris': False, 'options': 'new', 'iface': None, 'show_ifaces': False, 'port': None} + self.assertEqual(args, expected) + sys.stdout, sys.stderr = sys.__stdout__, sys.__stderr__ + + def test_packet_input_types(self): + from io import StringIO + out,err = StringIO(), StringIO() + sys.stdout, sys.stderr = out, err + args = tracevis.get_args(['--packet', '--packet-input-method', 'hex'], auto_exit=False) + expected = {'config_file': None, 'name': None, 'ips': None, 'packet': True, 'packet_input_method': 'hex', + 'packet_data': None, 'dns': False, 'dnstcp': False, 'continue': False, 'maxttl': None, + 'timeout': None, 'repeat': None, 'ripe': None, 'ripemids': None, 'file': None, 'csv': False, + 'csvraw': False, 'attach': False, 'label': None, 'domain1': None, 'domain2': None, 'annot1': None, + 'annot2': None, 'rexmit': False, 'paris': False, 'options': 'new', 'iface': None, 'show_ifaces': False, 'port': None} + self.assertEqual(args, expected) + + args = tracevis.get_args(['--packet', '--packet-input-method', 'json'], auto_exit=False) + expected = {'config_file': None, 'name': None, 'ips': None, 'packet': True, 'packet_input_method': 'json', + 'packet_data': None, 'dns': False, 'dnstcp': False, 'continue': False, 'maxttl': None, + 'timeout': None, 'repeat': None, 'ripe': None, 'ripemids': None, 'file': None, 'csv': False, + 'csvraw': False, 'attach': False, 'label': None, 'domain1': None, 'domain2': None, 'annot1': None, + 'annot2': None, 'rexmit': False, 'paris': False, 'options': 'new', 'iface': None, 'show_ifaces': False, 'port': None} + self.assertEqual(args, expected) + + args = tracevis.get_args(['--packet', '--packet-input-method', 'interactive'], auto_exit=False) + expected = {'config_file': None, 'name': None, 'ips': None, 'packet': True, 'packet_input_method': 'interactive', + 'packet_data': None, 'dns': False, 'dnstcp': False, 'continue': False, 'maxttl': None, + 'timeout': None, 'repeat': None, 'ripe': None, 'ripemids': None, 'file': None, 'csv': False, + 'csvraw': False, 'attach': False, 'label': None, 'domain1': None, 'domain2': None, 'annot1': None, + 'annot2': None, 'rexmit': False, 'paris': False, 'options': 'new', 'iface': None, 'show_ifaces': False, 'port': None} + self.assertEqual(args, expected) + + args = tracevis.get_args(['--packet', '--packet-input-method', 'json', '--packet-data', 'b64:e30='], auto_exit=False) + expected = {'config_file': None, 'name': None, 'ips': None, 'packet': True, 'packet_input_method': 'json', + 'packet_data': 'b64:e30=', 'dns': False, 'dnstcp': False, 'continue': False, 'maxttl': None, + 'timeout': None, 'repeat': None, 'ripe': None, 'ripemids': None, 'file': None, 'csv': False, + 'csvraw': False, 'attach': False, 'label': None, 'domain1': None, 'domain2': None, 'annot1': None, + 'annot2': None, 'rexmit': False, 'paris': False, 'options': 'new', 'iface': None, 'show_ifaces': False, 'port': None} + self.assertEqual(args, expected) + sys.stdout, sys.stderr = sys.__stdout__, sys.__stderr__ + diff --git a/tracevis.py b/tracevis.py index 9a7b102..5b38721 100755 --- a/tracevis.py +++ b/tracevis.py @@ -84,7 +84,7 @@ def process_input_args(args, parser): return args_dict -def get_args(): +def get_args(sys_args, auto_exit=True): parser = argparse.ArgumentParser( description='Traceroute with any packet. \ Visualize the routes. Discover Middleboxes and Firewalls', formatter_class=argparse.RawTextHelpFormatter) @@ -155,10 +155,10 @@ def get_args(): help="set the target network interface") parser.add_argument('--show-ifaces', action='store_true', help="show the network interfaces (conf.route)") - if len(sys.argv) == 1: + if len(sys_args) == 0 and auto_exit: parser.print_help() sys.exit(1) - args = parser.parse_args() + args = parser.parse_args(sys_args) args_dict = process_input_args(args, parser) return args_dict @@ -339,4 +339,4 @@ def main(args): if __name__ == "__main__": - main(get_args()) + main(get_args(sys.argv[1:]))