From 8440a63cfa0a5ae443b84158983b505289a0d7d3 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Mon, 8 Oct 2018 19:38:29 +0100 Subject: [PATCH 01/81] add a test to fuzz probe requests --- test/test.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/test.py b/test/test.py index 78bb10e..5cde991 100644 --- a/test/test.py +++ b/test/test.py @@ -92,3 +92,8 @@ def test_empty_essid(self): ) ProbeRequestSniffer.ProbeRequestParser.parse(packet) + + def test_fuzz_packets(self): + for i in range(0,100): + packet = RadioTap()/fuzz(Dot11()/Dot11ProbeReq()/Dot11Elt()) + ProbeRequestSniffer.ProbeRequestParser.parse(packet) From 4b485719773d26eb3099d704069fc252ef9c02f3 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Thu, 11 Oct 2018 11:21:12 +0100 Subject: [PATCH 02/81] fix #4 Drop the packet if the ESSID is not a valid UTF-8 string. --- probequest/probe_request_sniffer.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/probequest/probe_request_sniffer.py b/probequest/probe_request_sniffer.py index 7be973c..2a197bd 100644 --- a/probequest/probe_request_sniffer.py +++ b/probequest/probe_request_sniffer.py @@ -371,11 +371,15 @@ def parse(packet): Parses the raw packet and returns a probe request object. """ - if packet.haslayer(Dot11ProbeReq): - timestamp = packet.getlayer(RadioTap).time - s_mac = packet.getlayer(RadioTap).addr2 - essid = packet.getlayer(Dot11ProbeReq).info.decode("utf-8") + try: + if packet.haslayer(Dot11ProbeReq): + timestamp = packet.getlayer(RadioTap).time + s_mac = packet.getlayer(RadioTap).addr2 + essid = packet.getlayer(Dot11ProbeReq).info.decode("utf-8") - return ProbeRequest(timestamp, s_mac, essid) - else: + return ProbeRequest(timestamp, s_mac, essid) + else: + return None + except UnicodeDecodeError: + # The ESSID is not a valid UTF-8 string. return None From 40e864dea3c5e8666f8bf2463cbbd9e497573a8d Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Fri, 12 Oct 2018 23:49:30 +0100 Subject: [PATCH 03/81] improve fuzzing test --- probequest/probe_request.py | 4 ++-- test/test.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/probequest/probe_request.py b/probequest/probe_request.py index 50a8f15..44d8d1c 100644 --- a/probequest/probe_request.py +++ b/probequest/probe_request.py @@ -8,8 +8,8 @@ class ProbeRequest: def __init__(self, timestamp, s_mac, essid): self.timestamp = timestamp - self.s_mac = s_mac - self.essid = essid + self.s_mac = str(s_mac) + self.essid = str(essid) self.s_mac_oui = self.get_mac_organisation() diff --git a/test/test.py b/test/test.py index 5cde991..6a7e76e 100644 --- a/test/test.py +++ b/test/test.py @@ -94,6 +94,6 @@ def test_empty_essid(self): ProbeRequestSniffer.ProbeRequestParser.parse(packet) def test_fuzz_packets(self): - for i in range(0,100): + for i in range(0, 1000): packet = RadioTap()/fuzz(Dot11()/Dot11ProbeReq()/Dot11Elt()) ProbeRequestSniffer.ProbeRequestParser.parse(packet) From 4cd16dbdece23dd740971a649bc256f904cb905c Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Tue, 11 Dec 2018 23:33:06 +0000 Subject: [PATCH 04/81] add 'pylint' as test dependency --- setup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.py b/setup.py index 2d1a288..287db0e 100755 --- a/setup.py +++ b/setup.py @@ -44,6 +44,9 @@ 'scapy >= 2.4.0', 'urwid>= 2.0.1', ], + tests_require = [ + 'pylint' + ], extras_require = { 'docs': [ 'sphinx >= 1.4.0', From 21470f1b7a48196f2d29891007cc33409afcd6dc Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Tue, 11 Dec 2018 23:36:33 +0000 Subject: [PATCH 05/81] add pylint to the tests --- test/test.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/test.py b/test/test.py index 6a7e76e..9bc60fd 100644 --- a/test/test.py +++ b/test/test.py @@ -1,3 +1,4 @@ +import pylint.lint import unittest from datetime import datetime from netaddr.core import AddrFormatError @@ -97,3 +98,10 @@ def test_fuzz_packets(self): for i in range(0, 1000): packet = RadioTap()/fuzz(Dot11()/Dot11ProbeReq()/Dot11Elt()) ProbeRequestSniffer.ProbeRequestParser.parse(packet) + +class TestLinter(unittest.TestCase): + def test_linter(self): + pylint.lint.Run([ + "probequest", + "test" + ]) From 98b3a125f1c4986af27e946b38aeeb162d62a6a4 Mon Sep 17 00:00:00 2001 From: Skyper Date: Tue, 8 Jan 2019 21:20:00 +0000 Subject: [PATCH 06/81] fix a dead link --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index e6e22a7..619583f 100644 --- a/README.rst +++ b/README.rst @@ -38,7 +38,7 @@ In the Media ProbeQuest has appeared in the following media: - `KitPloit `__ -- `Hakin9 Magazine, VOL.13, NO. 05, "Open Source Hacking Tools" `__ +- `Hakin9 Magazine, VOL.13, NO. 05, "Open Source Hacking Tools" `__ - `WonderHowTo `__ (including a `YouTube video `__) License From 7e486ed5984f8f113a5c6790eec3cdad95810d3a Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Wed, 9 Jan 2019 08:24:00 +0000 Subject: [PATCH 07/81] resize the demo asciicast --- docs/_static/img/probequest_demo.gif | Bin 89269 -> 93817 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/_static/img/probequest_demo.gif b/docs/_static/img/probequest_demo.gif index 91e9a15a2eb3c13524ac8db82cf7c10b49c0e58f..8ec83fcbc8b3b656a887065357e75b0e7253e6e0 100644 GIT binary patch literal 93817 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 +iRs&#RQS#_Z&JY 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 X14WSdLU064aQ8~^1XWP>f&c(Ja*ZFY literal 89269 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
_~vj`4z?6|i(dZrM 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 Date: Sun, 27 Jan 2019 13:01:12 +0000 Subject: [PATCH 08/81] fix some linting errors in 'bin/probequest' --- bin/probequest | 79 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 57 insertions(+), 22 deletions(-) diff --git a/bin/probequest b/bin/probequest index f6ffca2..08f4825 100755 --- a/bin/probequest +++ b/bin/probequest @@ -28,28 +28,63 @@ def get_arg_parser(): Returns the argument parser. """ - ap = ArgumentParser(description="Toolkit for Playing with Wi-Fi Probe Requests") - ap.add_argument("--debug", action="store_true", help="debug mode") - ap.add_argument("--fake", action="store_true", help="display only fake ESSIDs") - ap.add_argument("-i", "--interface", required=True, help="wireless interface to use (must be in monitor mode)") - ap.add_argument("--ignore-case", action="store_true", help="ignore case distinctions in the regex pattern (default: false)") - ap.add_argument("--mode", type=Mode, choices=Mode.__members__.values(), help="set the mode to use") - ap.add_argument("-o", "--output", type=FileType("a"), help="output file to save the captured data (CSV format)") - ap.add_argument("--version", action="version", version=VERSION) - ap.set_defaults(debug=False) - ap.set_defaults(fake=False) - ap.set_defaults(ignore_case=False) - ap.set_defaults(mode=Mode.RAW) - - essid_arguments = ap.add_mutually_exclusive_group() - essid_arguments.add_argument("-e", "--essid", nargs="+", help="ESSID of the APs to filter (space-separated list)") - essid_arguments.add_argument("-r", "--regex", help="regex to filter the ESSIDs") - - station_arguments = ap.add_mutually_exclusive_group() - station_arguments.add_argument("--exclude", nargs="+", help="MAC addresses of the stations to exclude (space-separated list)") - station_arguments.add_argument("-s", "--station", nargs="+", help="MAC addresses of the stations to filter (space-separated list)") - - return ap + arg_parser = ArgumentParser(description="Toolkit for Playing with Wi-Fi Probe Requests") + arg_parser.add_argument( + "--debug", action="store_true", + help="debug mode" + ) + arg_parser.add_argument( + "--fake", action="store_true", + help="display only fake ESSIDs") + arg_parser.add_argument( + "-i", "--interface", + required=True, + help="wireless interface to use (must be in monitor mode)" + ) + arg_parser.add_argument( + "--ignore-case", action="store_true", + help="ignore case distinctions in the regex pattern (default: false)" + ) + arg_parser.add_argument( + "--mode", + type=Mode, choices=Mode.__members__.values(), + help="set the mode to use" + ) + arg_parser.add_argument( + "-o", "--output", + type=FileType("a"), + help="output file to save the captured data (CSV format)" + ) + arg_parser.add_argument("--version", action="version", version=VERSION) + arg_parser.set_defaults(debug=False) + arg_parser.set_defaults(fake=False) + arg_parser.set_defaults(ignore_case=False) + arg_parser.set_defaults(mode=Mode.RAW) + + essid_arguments = arg_parser.add_mutually_exclusive_group() + essid_arguments.add_argument( + "-e", "--essid", + nargs="+", + help="ESSID of the APs to filter (space-separated list)" + ) + essid_arguments.add_argument( + "-r", "--regex", + help="regex to filter the ESSIDs" + ) + + station_arguments = arg_parser.add_mutually_exclusive_group() + station_arguments.add_argument( + "--exclude", + nargs="+", + help="MAC addresses of the stations to exclude (space-separated list)" + ) + station_arguments.add_argument( + "-s", "--station", + nargs="+", + help="MAC addresses of the stations to filter (space-separated list)" + ) + + return arg_parser if __name__ == "__main__": from os import geteuid From 0dd6e79119259436888b5ebf2a9f06dd46f4a9e0 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sun, 27 Jan 2019 18:57:53 +0000 Subject: [PATCH 09/81] fix linting errors in the unit tests --- test/test.py | 130 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 126 insertions(+), 4 deletions(-) diff --git a/test/test.py b/test/test.py index 9bc60fd..9d2818e 100644 --- a/test/test.py +++ b/test/test.py @@ -1,23 +1,54 @@ -import pylint.lint +""" +Unit tests written with the 'unittest' module. +""" + import unittest -from datetime import datetime +import pylint.lint +from scapy.layers.dot11 import RadioTap, Dot11, Dot11Elt, Dot11ProbeReq +from scapy.packet import fuzz from netaddr.core import AddrFormatError from probequest.probe_request import ProbeRequest from probequest.probe_request_sniffer import ProbeRequestSniffer -from scapy.all import * + +# pylint: disable=unused-variable class TestProbeRequest(unittest.TestCase): + """ + Unit tests for the ProbeRequest class. + """ + def test_without_parameters(self): + """ + Initialises a ProbeRequest object + without any parameter. + """ + + # pylint: disable=no-value-for-parameter + with self.assertRaises(TypeError): probe_req = ProbeRequest() def test_with_only_one_parameter(self): + """ + Initialises a ProbeRequest object + with only one parameter. + """ + + # pylint: disable=no-value-for-parameter + timestamp = 1517872027.0 with self.assertRaises(TypeError): probe_req = ProbeRequest(timestamp) def test_with_only_two_parameters(self): + """ + Initialises a ProbeRequest object + with only two parameters. + """ + + # pylint: disable=no-value-for-parameter + timestamp = 1517872027.0 s_mac = "aa:bb:cc:dd:ee:ff" @@ -25,6 +56,13 @@ def test_with_only_two_parameters(self): probe_req = ProbeRequest(timestamp, s_mac) def test_create_a_probe_request(self): + """ + Creates a new ProbeRequest with all + the required parameters. + """ + + # pylint: disable=no-self-use + timestamp = 1517872027.0 s_mac = "aa:bb:cc:dd:ee:ff" essid = "Test ESSID" @@ -32,6 +70,11 @@ def test_create_a_probe_request(self): probe_req = ProbeRequest(timestamp, s_mac, essid) def test_bad_mac_address(self): + """ + Initialises a ProbeRequest object + with a malformed MAC address. + """ + timestamp = 1517872027.0 s_mac = "aa:bb:cc:dd:ee" essid = "Test ESSID" @@ -40,6 +83,11 @@ def test_bad_mac_address(self): probe_req = ProbeRequest(timestamp, s_mac, essid) def test_print_a_probe_request(self): + """ + Initialises a ProbeRequest object + and prints it. + """ + timestamp = 1517872027.0 s_mac = "aa:bb:cc:dd:ee:ff" essid = "Test ESSID" @@ -50,27 +98,74 @@ def test_print_a_probe_request(self): self.assertNotEqual(str(probe_req).find("aa:bb:cc:dd:ee:ff (None) -> Test ESSID"), -1) class TestProbeRequestSniffer(unittest.TestCase): + """ + Unit tests for the ProbeRequestSniffer class. + """ + def test_without_parameters(self): + """ + Initialises a ProbeRequestSniffer object + without parameters. + """ + + # pylint: disable=no-value-for-parameter + with self.assertRaises(TypeError): sniffer = ProbeRequestSniffer() def test_bad_display_function(self): + """ + Initialises a ProbeRequestSniffer object + with a non-callable display function. + """ + with self.assertRaises(TypeError): sniffer = ProbeRequestSniffer("wlan0", display_func="Test") def test_bad_storage_function(self): + """ + Initialises a ProbeRequestSniffer object + with a non-callable storage function. + """ + with self.assertRaises(TypeError): sniffer = ProbeRequestSniffer("wlan0", storage_func="Test") def test_create_sniffer(self): + """ + Creates a ProbeRequestSniffer object. + """ + + # pylint: disable=no-self-use + sniffer = ProbeRequestSniffer("wlan0") def test_stop_before_start(self): + """ + Creates a ProbeRequestSniffer object + and stops the sniffer before starting + it. + """ + + # pylint: disable=no-self-use + sniffer = ProbeRequestSniffer("wlan0") sniffer.stop() class TestProbeRequestParser(unittest.TestCase): + """ + Unit tests for the ProbeRequestParser class. + """ + def test_no_probe_request_layer(self): + """ + Creates a non-probe-request Wi-Fi + packet and parses it with the + 'ProbeRequestParser.parse()' function. + """ + + # pylint: disable=no-self-use + packet = RadioTap() \ / Dot11( addr1="ff:ff:ff:ff:ff:ff", @@ -81,6 +176,15 @@ def test_no_probe_request_layer(self): ProbeRequestSniffer.ProbeRequestParser.parse(packet) def test_empty_essid(self): + """ + Creates a probe request packet + with an empty ESSID field and + parses it with the + 'ProbeRequestParser.parse()' function. + """ + + # pylint: disable=no-self-use + packet = RadioTap() \ / Dot11( addr1="ff:ff:ff:ff:ff:ff", @@ -95,12 +199,30 @@ def test_empty_essid(self): ProbeRequestSniffer.ProbeRequestParser.parse(packet) def test_fuzz_packets(self): + """ + Parses 1000 randomly-generated probe + requests with the ProbeRequestParser.parse() + function. + """ + + # pylint: disable=no-self-use + for i in range(0, 1000): packet = RadioTap()/fuzz(Dot11()/Dot11ProbeReq()/Dot11Elt()) ProbeRequestSniffer.ProbeRequestParser.parse(packet) class TestLinter(unittest.TestCase): - def test_linter(self): + """ + Unit tests for Python linters. + """ + + def test_pylint(self): + """ + Executes Pylint. + """ + + # pylint: disable=no-self-use + pylint.lint.Run([ "probequest", "test" From 3f4ce13805e91b9e121ead1d9b1cd361a0273bf2 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sun, 27 Jan 2019 19:02:27 +0000 Subject: [PATCH 10/81] add missing docstring in 'probequest/version.py' --- probequest/version.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/probequest/version.py b/probequest/version.py index f406109..05ec734 100644 --- a/probequest/version.py +++ b/probequest/version.py @@ -1 +1,5 @@ +""" +Actual version number of ProbeQuest. +""" + VERSION = "0.7.0" From ebf9212039e8b3706d557b2400fb940e63b1262a Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Mon, 28 Jan 2019 08:37:42 +0000 Subject: [PATCH 11/81] fix some linting errors in 'probequest/probe_request_sniffer.py' --- probequest/probe_request_sniffer.py | 26 ++++++++++++++++++-------- test/test.py | 2 +- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/probequest/probe_request_sniffer.py b/probequest/probe_request_sniffer.py index 2a197bd..a254a48 100644 --- a/probequest/probe_request_sniffer.py +++ b/probequest/probe_request_sniffer.py @@ -1,13 +1,21 @@ +""" +A Wi-Fi probe request sniffer. +""" + from queue import Queue, Empty from re import compile as rcompile, match, IGNORECASE -from scapy.all import * from threading import Thread, Event +from scapy.config import conf +from scapy.data import ETH_P_ALL +from scapy.layers.dot11 import RadioTap, Dot11, Dot11ProbeReq, Dot11Elt +from scapy.sendrecv import sniff + from probequest.probe_request import ProbeRequest class ProbeRequestSniffer: """ - A Wi-Fi probe request sniffer. + Probe request sniffer class. """ SNIFFER_STOP_TIMEOUT = 2.0 @@ -210,7 +218,7 @@ def new_packet(self, packet): self.new_packets.put(packet) - def should_stop_sniffer(self, packet): + def should_stop_sniffer(self): """ Returns true if the sniffer should be stopped and false otherwise. """ @@ -286,7 +294,7 @@ def new_packet(self): self.new_packets.put(fake_probe_req) - def should_stop_sniffer(self, packet): + def should_stop_sniffer(self): """ Returns true if the fake sniffer should be stopped and false otherwise. """ @@ -344,10 +352,12 @@ def run(self): if not probe_request.essid: continue - if self.essid_filters is not None and not probe_request.essid in self.essid_filters: + if (self.essid_filters is not None + and not probe_request.essid in self.essid_filters): continue - if self.essid_regex is not None and not match(self.essid_regex, probe_request.essid): + if (self.essid_regex is not None + and not match(self.essid_regex, probe_request.essid)): continue self.display_func(probe_request) @@ -378,8 +388,8 @@ def parse(packet): essid = packet.getlayer(Dot11ProbeReq).info.decode("utf-8") return ProbeRequest(timestamp, s_mac, essid) - else: - return None + + return None except UnicodeDecodeError: # The ESSID is not a valid UTF-8 string. return None diff --git a/test/test.py b/test/test.py index 9d2818e..fcb219a 100644 --- a/test/test.py +++ b/test/test.py @@ -4,7 +4,7 @@ import unittest import pylint.lint -from scapy.layers.dot11 import RadioTap, Dot11, Dot11Elt, Dot11ProbeReq +from scapy.layers.dot11 import RadioTap, Dot11, Dot11ProbeReq, Dot11Elt from scapy.packet import fuzz from netaddr.core import AddrFormatError from probequest.probe_request import ProbeRequest From 1e4c4c270d3f14761d49dc53bf48ca22d01017e6 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Mon, 28 Jan 2019 08:37:42 +0000 Subject: [PATCH 12/81] fix some linting errors in 'probequest/probe_request.py' --- probequest/probe_request.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/probequest/probe_request.py b/probequest/probe_request.py index 44d8d1c..28487d0 100644 --- a/probequest/probe_request.py +++ b/probequest/probe_request.py @@ -1,9 +1,13 @@ -from netaddr import EUI, NotRegisteredError +""" +A Wi-Fi probe request. +""" + from time import localtime, strftime +from netaddr import EUI, NotRegisteredError class ProbeRequest: """ - A Wi-Fi probe request. + Probe request class. """ def __init__(self, timestamp, s_mac, essid): From a05c605e46fa86117c02e8ef53fcc5adb648073f Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Thu, 14 Feb 2019 09:20:18 +0000 Subject: [PATCH 13/81] Update badges in readme file --- README.rst | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 619583f..c658596 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ ProbeQuest ========== -|PyPI Package| |Build Status| |Code Coverage| |Documentation Status| +|PyPI Package| |PyPI Downloads| |PyPI Python Versions| |Build Status Master Branch| |Build Status Develop Branch| |Code Coverage| |Documentation Status| Toolkit allowing to sniff and display the Wi-Fi probe requests passing near your wireless interface. @@ -46,9 +46,12 @@ License `GPL version 3 `__ -.. |Build Status| image:: https://travis-ci.org/SkypLabs/probequest.svg +.. |Build Status Master Branch| image:: https://img.shields.io/travis/SkypLabs/probequest/master.svg?label=master&logo=travis&style=flat :target: https://travis-ci.org/SkypLabs/probequest - :alt: Build Status + :alt: Build Status Master Branch +.. |Build Status Develop Branch| image:: https://img.shields.io/travis/SkypLabs/probequest/develop.svg?label=develop&logo=travis&style=flat + :target: https://travis-ci.org/SkypLabs/probequest + :alt: Build Status Develop Branch .. |Code Coverage| image:: https://api.codacy.com/project/badge/Grade/16b9e70e51744256b37099ae8fe9132d :target: https://www.codacy.com/app/skyper/probequest?utm_source=github.com&utm_medium=referral&utm_content=SkypLabs/probequest&utm_campaign=Badge_Grade :alt: Code Coverage @@ -58,6 +61,12 @@ License .. |Known Vulnerabilities| image:: https://snyk.io/test/github/SkypLabs/probequest/badge.svg :target: https://snyk.io/test/github/SkypLabs/probequest :alt: Known Vulnerabilities -.. |PyPI Package| image:: https://badge.fury.io/py/probequest.svg - :target: https://badge.fury.io/py/probequest - :alt: PyPI Package +.. |PyPI Downloads| image:: https://img.shields.io/pypi/dm/probequest.svg?style=flat + :target: https://pypi.org/project/probequest/ + :alt: PyPI Package Downloads Per Month +.. |PyPI Package| image:: https://img.shields.io/pypi/v/probequest.svg?style=flat + :target: https://pypi.org/project/probequest/ + :alt: PyPI Package Latest Release +.. |PyPI Python Versions| image:: https://img.shields.io/pypi/pyversions/probequest.svg?logo=python&style=flat + :target: https://pypi.org/project/probequest/ + :alt: PyPI Package Python Versions From 6c30b368059a4f82e6275c725bb42c8fd7a64017 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Thu, 14 Feb 2019 09:24:33 +0000 Subject: [PATCH 14/81] Add Python 3.7 to the build matrix --- .travis.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index b198aa7..1096ebd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,16 +2,18 @@ sudo: false language: python -python: - - 3.3 - - 3.4 - - 3.5 - - 3.6 +install: script: python setup.py test jobs: include: + - python: 3.3 + - python: 3.4 + - python: 3.5 + - python: 3.6 + - python: 3.7 + dist: xenial - stage: PyPI release python: '3.6' deploy: From bdb1580a1cb948823b48e6eba5c000c8c71435c6 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Thu, 14 Feb 2019 09:29:30 +0000 Subject: [PATCH 15/81] Add Python 3.7 to the package's metadata --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 2d1a288..a50b7bf 100755 --- a/setup.py +++ b/setup.py @@ -32,6 +32,7 @@ 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', ], packages = find_packages(), From 5f7733ae682639419fa843818597c4505317698b Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Fri, 15 Feb 2019 08:06:35 +0000 Subject: [PATCH 16/81] Fix indentation in '.travis.yml' --- .travis.yml | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1096ebd..bec2cd7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,20 +8,21 @@ script: python setup.py test jobs: include: + - python: 3.2 - python: 3.3 - python: 3.4 - python: 3.5 - python: 3.6 - python: 3.7 dist: xenial - - stage: PyPI release - python: '3.6' - deploy: - provider: pypi - distributions: sdist - user: skyplabs - password: - secure: AAdbZ/WxjoCcGV9BAvphrxNp48D3n6yt1FEWZwA2V1BK2SnVV6YwQX/r7ryat2uoYrTONnGf/P5QgXc2wThl5mzPiRMaaONOIkyrmi2ZCD86yHYy2QQPR1gO1QOYyIsx545FRB6PaGXBC99Hw30vh3HPQaCKoQ8/PY+u3QbVn47BSBPuIdBi/FLl81uYk0ZE1457TxHkYrSDheRA3JVs72oD+izeoS+JJ+1YX+2zreukk9xuNfuwtcXPAn5B8LH8yYkFp1dd2pRsQzYMB4aH3rz7QEzSZ0mLBr3/J2bFCldmT8NToRijIjZNk04ik8XlGQ7xmpYG9rYIAkWwSBYSsZbfeLBxoxxIBcUy9xVxveZSaNl7TcRchCsyVO8leuL9aLmKz3wuKhWCRxJQSUDLlo83LYoBaTifqstUO85gC8IxR/Y/yqkW8wfSfVgVaDi9ET3/7UgSZEQJFEqfiYGdnD6/IkAy2tUCRO5xNsXsOyVJ5A0CsDTTtvlEfGxf1UtQyt0BmRSGYLMTnDBStW1Oua2QfVcTKIJOdfEOyL/VWnn/f0RCJQiUkRo9OFmSywoFjgSC9Arejwsff5smEd5i/jTKk6rOoHgIMnAGxn+75BjF3vQ7usAJeEOlLzHB5puc5dKeCpn5rwxOHha1lfmr6kDs1ec5XhcgQKgvulYfjVQ= - on: - tags: true - repo: SkypLabs/probequest + - stage: PyPI release + python: 3.6 + deploy: + provider: pypi + distributions: sdist + user: skyplabs + password: + secure: AAdbZ/WxjoCcGV9BAvphrxNp48D3n6yt1FEWZwA2V1BK2SnVV6YwQX/r7ryat2uoYrTONnGf/P5QgXc2wThl5mzPiRMaaONOIkyrmi2ZCD86yHYy2QQPR1gO1QOYyIsx545FRB6PaGXBC99Hw30vh3HPQaCKoQ8/PY+u3QbVn47BSBPuIdBi/FLl81uYk0ZE1457TxHkYrSDheRA3JVs72oD+izeoS+JJ+1YX+2zreukk9xuNfuwtcXPAn5B8LH8yYkFp1dd2pRsQzYMB4aH3rz7QEzSZ0mLBr3/J2bFCldmT8NToRijIjZNk04ik8XlGQ7xmpYG9rYIAkWwSBYSsZbfeLBxoxxIBcUy9xVxveZSaNl7TcRchCsyVO8leuL9aLmKz3wuKhWCRxJQSUDLlo83LYoBaTifqstUO85gC8IxR/Y/yqkW8wfSfVgVaDi9ET3/7UgSZEQJFEqfiYGdnD6/IkAy2tUCRO5xNsXsOyVJ5A0CsDTTtvlEfGxf1UtQyt0BmRSGYLMTnDBStW1Oua2QfVcTKIJOdfEOyL/VWnn/f0RCJQiUkRo9OFmSywoFjgSC9Arejwsff5smEd5i/jTKk6rOoHgIMnAGxn+75BjF3vQ7usAJeEOlLzHB5puc5dKeCpn5rwxOHha1lfmr6kDs1ec5XhcgQKgvulYfjVQ= + on: + tags: true + repo: SkypLabs/probequest From a2af8d67c3dd7c6aa791f93d81f16436002bc397 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Fri, 15 Feb 2019 08:16:31 +0000 Subject: [PATCH 17/81] Remove Python 3.2 from the build matrix Python 3.2 has been added to the build matrix by mistake while copy-pasting a chunk of a functional '.travis.yml' file --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index bec2cd7..fd6e0f5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,6 @@ script: python setup.py test jobs: include: - - python: 3.2 - python: 3.3 - python: 3.4 - python: 3.5 From 3b03f8db3eada10b11f383576d9f4170a651784e Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Fri, 15 Feb 2019 08:24:52 +0000 Subject: [PATCH 18/81] Deploy only on 'master' with Travis CI --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index fd6e0f5..e19462f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,5 +23,6 @@ jobs: password: secure: AAdbZ/WxjoCcGV9BAvphrxNp48D3n6yt1FEWZwA2V1BK2SnVV6YwQX/r7ryat2uoYrTONnGf/P5QgXc2wThl5mzPiRMaaONOIkyrmi2ZCD86yHYy2QQPR1gO1QOYyIsx545FRB6PaGXBC99Hw30vh3HPQaCKoQ8/PY+u3QbVn47BSBPuIdBi/FLl81uYk0ZE1457TxHkYrSDheRA3JVs72oD+izeoS+JJ+1YX+2zreukk9xuNfuwtcXPAn5B8LH8yYkFp1dd2pRsQzYMB4aH3rz7QEzSZ0mLBr3/J2bFCldmT8NToRijIjZNk04ik8XlGQ7xmpYG9rYIAkWwSBYSsZbfeLBxoxxIBcUy9xVxveZSaNl7TcRchCsyVO8leuL9aLmKz3wuKhWCRxJQSUDLlo83LYoBaTifqstUO85gC8IxR/Y/yqkW8wfSfVgVaDi9ET3/7UgSZEQJFEqfiYGdnD6/IkAy2tUCRO5xNsXsOyVJ5A0CsDTTtvlEfGxf1UtQyt0BmRSGYLMTnDBStW1Oua2QfVcTKIJOdfEOyL/VWnn/f0RCJQiUkRo9OFmSywoFjgSC9Arejwsff5smEd5i/jTKk6rOoHgIMnAGxn+75BjF3vQ7usAJeEOlLzHB5puc5dKeCpn5rwxOHha1lfmr6kDs1ec5XhcgQKgvulYfjVQ= on: + branch: master tags: true repo: SkypLabs/probequest From 4f77b5e293820ed2fa2789a6cfb6e58db31432ac Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Fri, 15 Feb 2019 08:25:18 +0000 Subject: [PATCH 19/81] Do not email on success with Travis CI --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index e19462f..9ac3092 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,3 +26,7 @@ jobs: branch: master tags: true repo: SkypLabs/probequest + +notifications: + email: + on_success: never From 8dec50d4f2b788c68588e232b699408ca0adc202 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Wed, 6 Mar 2019 21:27:10 +0000 Subject: [PATCH 20/81] Display help even if not root --- bin/probequest | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/probequest b/bin/probequest index f6ffca2..a67a50f 100755 --- a/bin/probequest +++ b/bin/probequest @@ -55,12 +55,12 @@ if __name__ == "__main__": from os import geteuid from sys import exit as sys_exit - if not geteuid() == 0: - sys_exit("[!] You must be root") - args = vars(get_arg_parser().parse_args()) interface = args.pop("interface") + if not geteuid() == 0: + sys_exit("[!] You must be root") + # Default mode. if args["mode"] == Mode.RAW: from time import sleep From 5bb109fa6751182c01b5760e7183a12e965a5d4d Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Fri, 8 Mar 2019 08:24:24 +0000 Subject: [PATCH 21/81] Fix some linting errors in the UI classes --- probequest/ui/pnl.py | 40 +++++++++++++++++++++++++++++----------- probequest/ui/raw.py | 12 ++++++++++++ 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/probequest/ui/pnl.py b/probequest/ui/pnl.py index 8f1742b..0dff147 100644 --- a/probequest/ui/pnl.py +++ b/probequest/ui/pnl.py @@ -1,6 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +""" +Preferred network list viewer. +""" + import urwid from probequest.probe_request_sniffer import ProbeRequestSniffer @@ -28,6 +32,7 @@ class PNLViewer: def __init__(self, interface, **kwargs): self.interface = interface self.stations = dict() + self.loop = None self.sniffer = ProbeRequestSniffer( self.interface, @@ -54,8 +59,14 @@ def setup_view(self): footer = urwid.AttrWrap(urwid.Text(self.footer_text), "footer") vline = urwid.AttrWrap(urwid.SolidFill(u"\u2502"), "line") - station_panel = urwid.Padding(self.setup_menu("List of Stations", self.stations.keys()), align="center", width=("relative", 90)) - pnl_panel = urwid.Padding(urwid.ListBox(urwid.SimpleListWalker([])), align="center", width=("relative", 90)) + station_panel = urwid.Padding( + self.setup_menu("List of Stations", self.stations.keys()), + align="center", width=("relative", 90) + ) + pnl_panel = urwid.Padding( + urwid.ListBox(urwid.SimpleListWalker([])), + align="center", width=("relative", 90) + ) self.station_list = station_panel.base_widget self.pnl_list = pnl_panel.base_widget @@ -70,7 +81,7 @@ def setup_view(self): header=self.header, body=body, footer=footer, - focus_part = "body" + focus_part="body" ) return top @@ -83,9 +94,9 @@ def setup_menu(self, title, choices): body = [urwid.Text(title), urwid.Divider()] - for c in choices: - button = urwid.Button(c) - urwid.connect_signal(button, "click", self.station_chosen, c) + for choice in choices: + button = urwid.Button(choice) + urwid.connect_signal(button, "click", self.station_chosen, choice) body.append(urwid.AttrMap(button, None, focus_map="selected")) return urwid.ListBox(urwid.SimpleFocusListWalker(body)) @@ -103,7 +114,7 @@ def new_probe_req(self, probe_req): self.stations[probe_req.s_mac].append(urwid.Text(probe_req.essid)) if len(self.stations.keys()) == 1: - self.station_list.set_focus(2); + self.station_list.set_focus(2) self.station_chosen(None, probe_req.s_mac) self.loop.draw_screen() @@ -123,6 +134,9 @@ def station_chosen(self, button, choice): in the stations list. """ + # pylint: disable=unused-argument + # 'button' is the widget object passed by Urwid. + self.pnl_list.body = self.stations[choice] def start_sniffer(self): @@ -132,7 +146,7 @@ def start_sniffer(self): self.sniffer.start() self.sniffer_state_text.set_text("Running") - self.header.set_attr("header_running"); + self.header.set_attr("header_running") def stop_sniffer(self): """ @@ -141,7 +155,7 @@ def stop_sniffer(self): self.sniffer.stop() self.sniffer_state_text.set_text("Stopped") - self.header.set_attr("header_stopped"); + self.header.set_attr("header_stopped") def toggle_sniffer_state(self): """ @@ -158,7 +172,11 @@ def main(self): Starts the TUI. """ - self.loop = urwid.MainLoop(self.view, self.palette, unhandled_input=self.unhandled_keypress) + self.loop = urwid.MainLoop( + self.view, + self.palette, + unhandled_input=self.unhandled_keypress + ) self.loop.run() def exit_program(self): @@ -180,6 +198,6 @@ def unhandled_keypress(self, key): elif key in ("p", "P"): self.toggle_sniffer_state() else: - return + return False return True diff --git a/probequest/ui/raw.py b/probequest/ui/raw.py index feabbb1..868e523 100644 --- a/probequest/ui/raw.py +++ b/probequest/ui/raw.py @@ -1,6 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +""" +Raw probe request viewer. +""" + from probequest.probe_request_sniffer import ProbeRequestSniffer class RawProbeRequestViewer: @@ -37,9 +41,17 @@ def display_probe_req(probe_req): ) def start(self): + """ + Starts the probe request sniffer. + """ + self.sniffer.start() def stop(self): + """ + Stops the probe request sniffer. + """ + self.sniffer.stop() if self.output is not None: From 503b725fa2e1fa9e50a90a69faf219995a7221e1 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Fri, 8 Mar 2019 08:57:57 +0000 Subject: [PATCH 22/81] Expect Pylint to fail Some linting errors haven't been fixed yet because they will be addressed while refactoring some aspects of the code. --- test/test.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/test.py b/test/test.py index fcb219a..dcbda48 100644 --- a/test/test.py +++ b/test/test.py @@ -216,6 +216,9 @@ class TestLinter(unittest.TestCase): Unit tests for Python linters. """ + # Some linting errors will be fixed while + # refactoring the code. + @unittest.expectedFailure def test_pylint(self): """ Executes Pylint. From 66cca26b73594f042b288dac79b4a9f47cf9fb9e Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Fri, 8 Mar 2019 09:07:33 +0000 Subject: [PATCH 23/81] Drop support for Python 3.3 Python 3.3 is EOL and is no longer receiving bug fixes, including security fixes. In addition, latest version of Pylint doesn't support it either which causes testing errors. --- .travis.yml | 1 - setup.py | 1 - 2 files changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9ac3092..aed614c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,6 @@ script: python setup.py test jobs: include: - - python: 3.3 - python: 3.4 - python: 3.5 - python: 3.6 diff --git a/setup.py b/setup.py index 5a7f13c..9da19a1 100755 --- a/setup.py +++ b/setup.py @@ -28,7 +28,6 @@ 'Intended Audience :: Developers', 'Topic :: Software Development :: Libraries :: Python Modules', 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', From da05bc9ca9bae4b557a1fc3471f4e994b15d05f4 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Fri, 8 Mar 2019 09:14:52 +0000 Subject: [PATCH 24/81] Replace method 'isAlive()' by 'is_alive()' The method 'isAlive()' is being deprecated and is replaced by 'is_alive()'. See https://docs.python.org/3/library/threading.html#threading.Thread.is_alive. --- probequest/probe_request_sniffer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/probequest/probe_request_sniffer.py b/probequest/probe_request_sniffer.py index 601fa67..7851763 100644 --- a/probequest/probe_request_sniffer.py +++ b/probequest/probe_request_sniffer.py @@ -82,7 +82,7 @@ def stop(self): # stop() has been called before start(). pass finally: - if self.sniffer.isAlive(): + if self.sniffer.is_alive(): self.sniffer.socket.close() try: From 7d9ac670e07cdda8e81ff282790eee0a3a1529ea Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sun, 10 Mar 2019 17:47:02 +0000 Subject: [PATCH 25/81] Add Probot Settings' configuration file See https://probot.github.io/apps/settings/. --- .github/settings.yml | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/settings.yml diff --git a/.github/settings.yml b/.github/settings.yml new file mode 100644 index 0000000..98de154 --- /dev/null +++ b/.github/settings.yml @@ -0,0 +1,38 @@ +repository: + # See https://developer.github.com/v3/repos/#edit for all available settings. + + # The name of the repository. Changing this will rename the repository + name: probequest + + # A short description of the repository that will show up on GitHub + description: Toolkit for Playing with Wi-Fi Probe Requests + + # A URL with more information about the repository + homepage: https://probequest.readthedocs.io/en/latest/ + + # Either `true` to make the repository private, or `false` to make it public. + private: false + + # Either `true` to enable issues for this repository, `false` to disable them. + has_issues: true + + # Either `true` to enable the wiki for this repository, `false` to disable it. + has_wiki: false + + # Either `true` to enable downloads for this repository, `false` to disable them. + has_downloads: true + + # Updates the default branch for this repository. + default_branch: develop + + # Either `true` to allow squash-merging pull requests, or `false` to prevent + # squash-merging. + allow_squash_merge: true + + # Either `true` to allow merging pull requests with a merge commit, or `false` + # to prevent merging pull requests with merge commits. + allow_merge_commit: true + + # Either `true` to allow rebase-merging pull requests, or `false` to prevent + # rebase-merging. + allow_rebase_merge: true From 44b9154c8c13a9b7f57a58258743980d98fe6096 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sun, 10 Mar 2019 22:20:34 +0000 Subject: [PATCH 26/81] Add labels --- .github/settings.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/.github/settings.yml b/.github/settings.yml index 98de154..7336562 100644 --- a/.github/settings.yml +++ b/.github/settings.yml @@ -36,3 +36,33 @@ repository: # Either `true` to allow rebase-merging pull requests, or `false` to prevent # rebase-merging. allow_rebase_merge: true + +# Labels: define labels for Issues and Pull Requests +labels: + - name: bug + description: New bug + color: ee0701 + - name: documentation + color: 2d2de2 + - name: duplicate + color: cccccc + - name: enhancement + color: 84b6eb + - name: featue request + color: 96f7c3 + - name: good first issue + color: 7057ff + - name: help wanted + color: 33aa3f + - name: invalid + color: e6e6e6 + - name: question + color: cc317c + - name: regression + description: Software regression + color: ee0701 + - name: security + description: Security issue + color: ee0701 + - name: wontfix + color: ffffff From b0893e22165b1d402cb6d6f3ff15b71fd30360d4 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sun, 10 Mar 2019 22:28:50 +0000 Subject: [PATCH 27/81] Add topics --- .github/settings.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/settings.yml b/.github/settings.yml index 7336562..97697ac 100644 --- a/.github/settings.yml +++ b/.github/settings.yml @@ -1,15 +1,18 @@ repository: # See https://developer.github.com/v3/repos/#edit for all available settings. - # The name of the repository. Changing this will rename the repository + # The name of the repository. Changing this will rename the repository. name: probequest - # A short description of the repository that will show up on GitHub + # A short description of the repository that will show up on GitHub. description: Toolkit for Playing with Wi-Fi Probe Requests - # A URL with more information about the repository + # A URL with more information about the repository. homepage: https://probequest.readthedocs.io/en/latest/ + # List of topics. + topics: python, python3, scapy, wifi-security, sniffer, dot11, network-attacks, monitoring, security, wireless, wifi, toolkit + # Either `true` to make the repository private, or `false` to make it public. private: false @@ -37,7 +40,7 @@ repository: # rebase-merging. allow_rebase_merge: true -# Labels: define labels for Issues and Pull Requests +# Labels: define labels for Issues and Pull Requests. labels: - name: bug description: New bug From 44d7d9715dcd15216309c238dcf1b3860e9dae89 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sun, 10 Mar 2019 22:43:38 +0000 Subject: [PATCH 28/81] Add branches --- .github/settings.yml | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/.github/settings.yml b/.github/settings.yml index 97697ac..4f1ced1 100644 --- a/.github/settings.yml +++ b/.github/settings.yml @@ -69,3 +69,38 @@ labels: color: ee0701 - name: wontfix color: ffffff + +branches: + - name: master + # https://developer.github.com/v3/repos/branches/#update-branch-protection + # Branch Protection settings. Set to null to disable + protection: + # Required. Require at least one approving review on a pull request, before merging. Set to null to disable. + required_pull_request_reviews: + # Required. Require status checks to pass before merging. Set to null to disable + required_status_checks: + # Required. Require branches to be up to date before merging. + strict: false + # Required. The list of status checks to require in order to merge into this branch + contexts: + - continuous-integration/travis-ci + # Required. Enforce all configured restrictions for administrators. Set to true to enforce required status checks for repository administrators. Set to null to disable. + enforce_admins: true + # Required. Restrict who can push to this branch. Team and user restrictions are only available for organization-owned repositories. Set to null to disable. + restrictions: + users: [] + teams: [] + - name: develop + # https://developer.github.com/v3/repos/branches/#update-branch-protection + # Branch Protection settings. Set to null to disable + protection: + # Required. Require at least one approving review on a pull request, before merging. Set to null to disable. + required_pull_request_reviews: + # Required. Require status checks to pass before merging. Set to null to disable + required_status_checks: + # Required. Enforce all configured restrictions for administrators. Set to true to enforce required status checks for repository administrators. Set to null to disable. + enforce_admins: false + # Required. Restrict who can push to this branch. Team and user restrictions are only available for organization-owned repositories. Set to null to disable. + restrictions: + users: [] + teams: [] From ad1fbffb1a0f1c15999dda4dd1e5f9f38403a786 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sun, 10 Mar 2019 22:45:27 +0000 Subject: [PATCH 29/81] Remove comments The settings' names are explicite enough not to need comments. --- .github/settings.yml | 45 ++------------------------------------------ 1 file changed, 2 insertions(+), 43 deletions(-) diff --git a/.github/settings.yml b/.github/settings.yml index 4f1ced1..9592186 100644 --- a/.github/settings.yml +++ b/.github/settings.yml @@ -1,46 +1,19 @@ -repository: - # See https://developer.github.com/v3/repos/#edit for all available settings. +# See https://github.com/probot/settings for more information. - # The name of the repository. Changing this will rename the repository. +repository: name: probequest - - # A short description of the repository that will show up on GitHub. description: Toolkit for Playing with Wi-Fi Probe Requests - - # A URL with more information about the repository. homepage: https://probequest.readthedocs.io/en/latest/ - - # List of topics. topics: python, python3, scapy, wifi-security, sniffer, dot11, network-attacks, monitoring, security, wireless, wifi, toolkit - - # Either `true` to make the repository private, or `false` to make it public. private: false - - # Either `true` to enable issues for this repository, `false` to disable them. has_issues: true - - # Either `true` to enable the wiki for this repository, `false` to disable it. has_wiki: false - - # Either `true` to enable downloads for this repository, `false` to disable them. has_downloads: true - - # Updates the default branch for this repository. default_branch: develop - - # Either `true` to allow squash-merging pull requests, or `false` to prevent - # squash-merging. allow_squash_merge: true - - # Either `true` to allow merging pull requests with a merge commit, or `false` - # to prevent merging pull requests with merge commits. allow_merge_commit: true - - # Either `true` to allow rebase-merging pull requests, or `false` to prevent - # rebase-merging. allow_rebase_merge: true -# Labels: define labels for Issues and Pull Requests. labels: - name: bug description: New bug @@ -72,35 +45,21 @@ labels: branches: - name: master - # https://developer.github.com/v3/repos/branches/#update-branch-protection - # Branch Protection settings. Set to null to disable protection: - # Required. Require at least one approving review on a pull request, before merging. Set to null to disable. required_pull_request_reviews: - # Required. Require status checks to pass before merging. Set to null to disable required_status_checks: - # Required. Require branches to be up to date before merging. strict: false - # Required. The list of status checks to require in order to merge into this branch contexts: - continuous-integration/travis-ci - # Required. Enforce all configured restrictions for administrators. Set to true to enforce required status checks for repository administrators. Set to null to disable. enforce_admins: true - # Required. Restrict who can push to this branch. Team and user restrictions are only available for organization-owned repositories. Set to null to disable. restrictions: users: [] teams: [] - name: develop - # https://developer.github.com/v3/repos/branches/#update-branch-protection - # Branch Protection settings. Set to null to disable protection: - # Required. Require at least one approving review on a pull request, before merging. Set to null to disable. required_pull_request_reviews: - # Required. Require status checks to pass before merging. Set to null to disable required_status_checks: - # Required. Enforce all configured restrictions for administrators. Set to true to enforce required status checks for repository administrators. Set to null to disable. enforce_admins: false - # Required. Restrict who can push to this branch. Team and user restrictions are only available for organization-owned repositories. Set to null to disable. restrictions: users: [] teams: [] From 2c6d3909b3d1c2ec17b9a602d228cc635589170d Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sun, 10 Mar 2019 22:47:27 +0000 Subject: [PATCH 30/81] Add 'has_projects' --- .github/settings.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/settings.yml b/.github/settings.yml index 9592186..3c3dc93 100644 --- a/.github/settings.yml +++ b/.github/settings.yml @@ -7,6 +7,7 @@ repository: topics: python, python3, scapy, wifi-security, sniffer, dot11, network-attacks, monitoring, security, wireless, wifi, toolkit private: false has_issues: true + has_projects: true has_wiki: false has_downloads: true default_branch: develop From 88088f1ab8de7736c27a711115b79f66fdd286eb Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sun, 10 Mar 2019 22:54:51 +0000 Subject: [PATCH 31/81] Disable restrictions --- .github/settings.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/settings.yml b/.github/settings.yml index 3c3dc93..e13df92 100644 --- a/.github/settings.yml +++ b/.github/settings.yml @@ -54,13 +54,9 @@ branches: - continuous-integration/travis-ci enforce_admins: true restrictions: - users: [] - teams: [] - name: develop protection: required_pull_request_reviews: required_status_checks: enforce_admins: false restrictions: - users: [] - teams: [] From 68a5e0dce026184ea42122ead8b22e2bccd0d71a Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sun, 10 Mar 2019 23:00:39 +0000 Subject: [PATCH 32/81] Add a description to each label --- .github/settings.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/settings.yml b/.github/settings.yml index e13df92..5a1a58a 100644 --- a/.github/settings.yml +++ b/.github/settings.yml @@ -20,20 +20,28 @@ labels: description: New bug color: ee0701 - name: documentation + description: Documentation-related issue color: 2d2de2 - name: duplicate + description: Duplicate issue color: cccccc - name: enhancement + description: Enhancement color: 84b6eb - name: featue request + description: Feature request color: 96f7c3 - name: good first issue + description: Good first issue color: 7057ff - name: help wanted + description: Help wanted color: 33aa3f - name: invalid + description: Invalid issue color: e6e6e6 - name: question + description: General question color: cc317c - name: regression description: Software regression @@ -42,6 +50,7 @@ labels: description: Security issue color: ee0701 - name: wontfix + description: The issue won't be fixed color: ffffff branches: From 119c42be9a725af1a252b820e6947dd67b794f65 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sun, 10 Mar 2019 23:08:54 +0000 Subject: [PATCH 33/81] Use the same colour for status-related labels --- .github/settings.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/settings.yml b/.github/settings.yml index 5a1a58a..68a4ce7 100644 --- a/.github/settings.yml +++ b/.github/settings.yml @@ -39,7 +39,7 @@ labels: color: 33aa3f - name: invalid description: Invalid issue - color: e6e6e6 + color: cccccc - name: question description: General question color: cc317c @@ -51,7 +51,7 @@ labels: color: ee0701 - name: wontfix description: The issue won't be fixed - color: ffffff + color: cccccc branches: - name: master From f0530a80b24ebd8ab0d79fe4b5fb976133062696 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sun, 10 Mar 2019 23:26:04 +0000 Subject: [PATCH 34/81] Improve the labels --- .github/settings.yml | 69 ++++++++++++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 25 deletions(-) diff --git a/.github/settings.yml b/.github/settings.yml index 68a4ce7..4086f82 100644 --- a/.github/settings.yml +++ b/.github/settings.yml @@ -16,43 +16,62 @@ repository: allow_rebase_merge: true labels: + - name: feature + oldname: enhancement + description: New feature + color: 84b6eb + - name: optimisation + description: Optimisation + color: 84b6eb + + - name: sniffer + description: Related to the sniffer + color: 1d76db + - name: parser + description: Related to the parser + color: 1d76db + - name: ui + description: Related to the user interface + color: 1d76db + - name: bug description: New bug color: ee0701 - - name: documentation - description: Documentation-related issue - color: 2d2de2 - - name: duplicate - description: Duplicate issue - color: cccccc - - name: enhancement - description: Enhancement - color: 84b6eb - - name: featue request - description: Feature request - color: 96f7c3 - - name: good first issue - description: Good first issue - color: 7057ff - - name: help wanted - description: Help wanted - color: 33aa3f - - name: invalid - description: Invalid issue - color: cccccc - - name: question - description: General question - color: cc317c - name: regression description: Software regression color: ee0701 - name: security description: Security issue color: ee0701 - - name: wontfix + + - name: duplicate + description: Duplicate issue + color: cccccc + - name: invalid + description: Invalid issue + color: cccccc + - name: on hold + description: On hold (waiting for an answer, action required...) + color: cccccc + - name: won't fix description: The issue won't be fixed color: cccccc + - name: help wanted + description: Help wanted + color: 33aa3f + - name: question + description: Question + color: 33aa3f + + - name: documentation + description: Documentation-related issue + color: 2d2de2 + + - name: good first issue + description: Good first issue + color: 7057ff + branches: - name: master protection: From 6ae5388949072c6ce52d3c94e91072b404bd7022 Mon Sep 17 00:00:00 2001 From: Skyper Date: Sun, 10 Mar 2019 23:58:46 +0000 Subject: [PATCH 35/81] Add issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 29 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..6d6e3f7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: SkypLabs + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behaviour. + +**Expected behaviour** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Execution environment (please complete the following information):** + - OS: [e.g. Debian Stretch] + - Python version [e.g. 3.6] + - ProbeQuest version [e.g. 0.7.0] + - Method of installation [e.g. pip] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..18e7eb7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: feature +assignees: SkypLabs + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. From bd2282e5b1dc0e874df6de193dcf2dfc0b4986f2 Mon Sep 17 00:00:00 2001 From: Skyper Date: Mon, 11 Mar 2019 00:03:42 +0000 Subject: [PATCH 36/81] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 28 +++++++++++++++-------- .github/ISSUE_TEMPLATE/feature_request.md | 12 ++++++---- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 6d6e3f7..9e48777 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,23 +7,31 @@ assignees: SkypLabs --- -**Describe the bug** +## Describe the bug + A clear and concise description of what the bug is. -**To Reproduce** +## To Reproduce + Steps to reproduce the behaviour. -**Expected behaviour** +## Expected behaviour + A clear and concise description of what you expected to happen. -**Screenshots** +## Screenshots + If applicable, add screenshots to help explain your problem. -**Execution environment (please complete the following information):** - - OS: [e.g. Debian Stretch] - - Python version [e.g. 3.6] - - ProbeQuest version [e.g. 0.7.0] - - Method of installation [e.g. pip] +## Execution environment + +Please complete the following information: + + - **OS:** [e.g. Debian Stretch] + - **Python version:** [e.g. 3.6] + - **ProbeQuest version:** [e.g. 0.7.0] + - **Method of installation:** [e.g. pip] + +## Additional context -**Additional context** Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 18e7eb7..aaa0705 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -7,14 +7,18 @@ assignees: SkypLabs --- -**Is your feature request related to a problem? Please describe.** +## Is your feature request related to a problem? + A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -**Describe the solution you'd like** +## Describe the solution you'd like + A clear and concise description of what you want to happen. -**Describe alternatives you've considered** +## Describe alternatives you've considered + A clear and concise description of any alternative solutions or features you've considered. -**Additional context** +## Additional context + Add any other context or screenshots about the feature request here. From 9d477ad6b748c7fd1317a84415e9c80f0646ca63 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sun, 10 Mar 2019 23:28:50 +0000 Subject: [PATCH 37/81] Add 'CHANGELOG.md' --- CHANGELOG.md | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..df4c531 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,98 @@ +## v0.7.1 - Mar 6, 2019 + +### Fixes + +* Error when trying to decode ESSIDs using invalid UTF-8 characters ([#4](https://github.com/SkypLabs/probequest/issues/4)) +* Arguments not working (-e, -r) ([#17](https://github.com/SkypLabs/probequest/issues/17)) + +## v0.7.0 - Oct 8, 2018 + +### Features + +* Add the `--fake` option to display fake Wi-Fi EDDISs for development purposes + +### Fixes + +* Add unit tests following [#5](https://github.com/SkypLabs/probequest/issues/5) + +## v0.6.2 - Jul 31, 2018 + +### Fixes + +* Test if a packet has a `Dot11ProbeReq` layer before parsing it ([#5](https://github.com/SkypLabs/probequest/issues/5), [#8](https://github.com/SkypLabs/probequest/issues/8)) + +## v0.6.1 - May 28, 2018 + +### Features + +* Change the short description in `setup.py` + +### Documentation + +* Update the installation documentation + +### Fixes + +* Fix a missing dependency + +## v0.6.0 - May 27, 2018 + +The project has been renamed to ProbeQuest. + +### Features + +* Refactor the software architecture +* Add a TUI + +### Documentation + +* Use Sphinx for the documentation + +## v0.5.1 - Feb 18, 2018 + +### Features + +* Improve the debug mode + +### Fixes + +* The sniffer stops after having received the first frame ([#3](https://github.com/SkypLabs/probequest/issues/3)) + +## v0.5.0 - Feb 7, 2018 + +### Features + +* Refactor the software architecture +* Add the `--ignore-case` argument +* Add a mutual exclusion for the `--exclude` and `--station` arguments +* Add a debug mode +* Display the timestamp as a readable time +* Add unit tests + +## v0.4.0 - Sep 19, 2017 + +### Features + +* Display MAC address's OUI if available + +## v0.3.0 - Sep 10, 2017 + +### Features + +* Add regex filtering + +### Infrastructure + +* Deploy automatically the new releases to PyPI using Travis CI + +## v0.2.0 - Sep 10, 2017 + +### Features + +* Add station filtering +* Add ESSID filtering +* Add exclusion filtering + +## v0.1.0 - Sep 10, 2017 + +First pre-release. From f6337c8cb05e63a1843fa43f03b062f569ccaca0 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Wed, 8 May 2019 08:40:22 +0100 Subject: [PATCH 38/81] Add LGTM badges --- README.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index c658596..187e2b9 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ ProbeQuest ========== -|PyPI Package| |PyPI Downloads| |PyPI Python Versions| |Build Status Master Branch| |Build Status Develop Branch| |Code Coverage| |Documentation Status| +|PyPI Package| |PyPI Downloads| |PyPI Python Versions| |Build Status Master Branch| |Build Status Develop Branch| |Code Coverage| |LGTM Grade| |LGTM Alerts| |Documentation Status| Toolkit allowing to sniff and display the Wi-Fi probe requests passing near your wireless interface. @@ -61,6 +61,12 @@ License .. |Known Vulnerabilities| image:: https://snyk.io/test/github/SkypLabs/probequest/badge.svg :target: https://snyk.io/test/github/SkypLabs/probequest :alt: Known Vulnerabilities +.. |LGTM Alerts| image:: https://img.shields.io/lgtm/alerts/g/SkypLabs/probequest.svg?logo=lgtm&logoWidth=18 + :target: https://lgtm.com/projects/g/SkypLabs/probequest/alerts/ + :alt: LGTM Alerts +.. |LGTM Grade| image:: https://img.shields.io/lgtm/grade/python/g/SkypLabs/probequest.svg?logo=lgtm&logoWidth=18 + :target: https://lgtm.com/projects/g/SkypLabs/probequest/context:python + :alt: LGTM Grade .. |PyPI Downloads| image:: https://img.shields.io/pypi/dm/probequest.svg?style=flat :target: https://pypi.org/project/probequest/ :alt: PyPI Package Downloads Per Month From 6e0014e4146cb10f16c6895308ba0fcb53ac9575 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Wed, 8 May 2019 20:12:30 +0100 Subject: [PATCH 39/81] Add the pre-commit framework to install Git hooks See https://pre-commit.com/ for further details. --- .pre-commit-config.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..6c3a487 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,9 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.2.1 + hooks: + - id: check-ast + - id: check-executables-have-shebangs + - id: check-yaml + - id: flake8 + - id: trailing-whitespace From 87367d87eecb032dbf10794b36bf11c005649f67 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Wed, 8 May 2019 22:38:38 +0100 Subject: [PATCH 40/81] Add Pylint as Git pre-commit hook --- .pre-commit-config.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6c3a487..a3807d8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,3 +7,7 @@ repos: - id: check-yaml - id: flake8 - id: trailing-whitespace + - repo: https://github.com/pre-commit/mirrors-pylint + rev: v2.3.1 + hooks: + - id: pylint From 26d16aef6a7912c3cebbfa51513787f323e87ce0 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Wed, 8 May 2019 23:56:13 +0100 Subject: [PATCH 41/81] Fix linting and style errors in 'setup.py' --- setup.py | 45 +++++++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/setup.py b/setup.py index 9da19a1..d4386b4 100755 --- a/setup.py +++ b/setup.py @@ -1,6 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +""" +Setuptools build system configuration file +for ProbeQuest. + +See https://setuptools.readthedocs.io. +""" + from codecs import open as fopen from os.path import dirname, abspath, join from setuptools import setup, find_packages @@ -10,20 +17,22 @@ DIR = dirname(abspath(__file__)) with fopen(join(DIR, 'README.rst'), encoding='utf-8') as f: - long_description = f.read() + LONG_DESCRIPTION = f.read() + setup( - name = 'probequest', - version = VERSION, - description = 'Toolkit for Playing with Wi-Fi Probe Requests', - long_description = long_description, - license = 'GPLv3', - keywords = 'wifi wireless security sniffer', - author = 'Paul-Emmanuel Raoul', - author_email = 'skyper@skyplabs.net', - url = 'https://github.com/SkypLabs/probequest', - download_url = 'https://github.com/SkypLabs/probequest/archive/v{0}.zip'.format(VERSION), - classifiers = [ + name='probequest', + version=VERSION, + description='Toolkit for Playing with Wi-Fi Probe Requests', + long_description=LONG_DESCRIPTION, + license='GPLv3', + keywords='wifi wireless security sniffer', + author='Paul-Emmanuel Raoul', + author_email='skyper@skyplabs.net', + url='https://github.com/SkypLabs/probequest', + download_url='https://github.com/SkypLabs/probequest/archive/v{0}.zip' + .format(VERSION), + classifiers=[ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'Topic :: Software Development :: Libraries :: Python Modules', @@ -34,20 +43,20 @@ 'Programming Language :: Python :: 3.7', 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', ], - packages = find_packages(), - scripts = ['bin/probequest'], - test_suite = 'test', - install_requires = [ + packages=find_packages(), + scripts=['bin/probequest'], + test_suite='test', + install_requires=[ 'argparse >= 1.4.0', 'faker_wifi_essid', 'netaddr >= 0.7.19', 'scapy >= 2.4.0', 'urwid>= 2.0.1', ], - tests_require = [ + tests_require=[ 'pylint' ], - extras_require = { + extras_require={ 'docs': [ 'sphinx >= 1.4.0', 'sphinxcontrib-seqdiag >= 0.8.5', From 944d7ca94d9f2b16690474bf84584805f22a37ff Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Wed, 8 May 2019 23:56:13 +0100 Subject: [PATCH 42/81] Fix linting and style errors in 'test/test.py' --- test/test.py | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/test/test.py b/test/test.py index dcbda48..02951d8 100644 --- a/test/test.py +++ b/test/test.py @@ -2,6 +2,9 @@ Unit tests written with the 'unittest' module. """ +# pylint: disable=import-error +# pylint: disable=unused-variable + import unittest import pylint.lint from scapy.layers.dot11 import RadioTap, Dot11, Dot11ProbeReq, Dot11Elt @@ -10,7 +13,6 @@ from probequest.probe_request import ProbeRequest from probequest.probe_request_sniffer import ProbeRequestSniffer -# pylint: disable=unused-variable class TestProbeRequest(unittest.TestCase): """ @@ -26,7 +28,7 @@ def test_without_parameters(self): # pylint: disable=no-value-for-parameter with self.assertRaises(TypeError): - probe_req = ProbeRequest() + probe_req = ProbeRequest() # noqa: F841 def test_with_only_one_parameter(self): """ @@ -39,7 +41,7 @@ def test_with_only_one_parameter(self): timestamp = 1517872027.0 with self.assertRaises(TypeError): - probe_req = ProbeRequest(timestamp) + probe_req = ProbeRequest(timestamp) # noqa: F841 def test_with_only_two_parameters(self): """ @@ -53,7 +55,7 @@ def test_with_only_two_parameters(self): s_mac = "aa:bb:cc:dd:ee:ff" with self.assertRaises(TypeError): - probe_req = ProbeRequest(timestamp, s_mac) + probe_req = ProbeRequest(timestamp, s_mac) # noqa: F841 def test_create_a_probe_request(self): """ @@ -67,7 +69,7 @@ def test_create_a_probe_request(self): s_mac = "aa:bb:cc:dd:ee:ff" essid = "Test ESSID" - probe_req = ProbeRequest(timestamp, s_mac, essid) + probe_req = ProbeRequest(timestamp, s_mac, essid) # noqa: F841 def test_bad_mac_address(self): """ @@ -80,7 +82,7 @@ def test_bad_mac_address(self): essid = "Test ESSID" with self.assertRaises(AddrFormatError): - probe_req = ProbeRequest(timestamp, s_mac, essid) + probe_req = ProbeRequest(timestamp, s_mac, essid) # noqa: F841 def test_print_a_probe_request(self): """ @@ -94,8 +96,15 @@ def test_print_a_probe_request(self): probe_req = ProbeRequest(timestamp, s_mac, essid) - self.assertNotEqual(str(probe_req).find("Mon, 05 Feb 2018 23:07:07"), -1) - self.assertNotEqual(str(probe_req).find("aa:bb:cc:dd:ee:ff (None) -> Test ESSID"), -1) + self.assertNotEqual( + str(probe_req).find("Mon, 05 Feb 2018 23:07:07"), + -1 + ) + self.assertNotEqual( + str(probe_req).find("aa:bb:cc:dd:ee:ff (None) -> Test ESSID"), + -1 + ) + class TestProbeRequestSniffer(unittest.TestCase): """ @@ -111,7 +120,7 @@ def test_without_parameters(self): # pylint: disable=no-value-for-parameter with self.assertRaises(TypeError): - sniffer = ProbeRequestSniffer() + sniffer = ProbeRequestSniffer() # noqa: F841 def test_bad_display_function(self): """ @@ -120,7 +129,10 @@ def test_bad_display_function(self): """ with self.assertRaises(TypeError): - sniffer = ProbeRequestSniffer("wlan0", display_func="Test") + sniffer = ProbeRequestSniffer( # noqa: F841 + "wlan0", + display_func="Test" + ) def test_bad_storage_function(self): """ @@ -129,7 +141,10 @@ def test_bad_storage_function(self): """ with self.assertRaises(TypeError): - sniffer = ProbeRequestSniffer("wlan0", storage_func="Test") + sniffer = ProbeRequestSniffer( # noqa: F841 + "wlan0", + storage_func="Test" + ) def test_create_sniffer(self): """ @@ -138,7 +153,7 @@ def test_create_sniffer(self): # pylint: disable=no-self-use - sniffer = ProbeRequestSniffer("wlan0") + sniffer = ProbeRequestSniffer("wlan0") # noqa: F841 def test_stop_before_start(self): """ @@ -152,6 +167,7 @@ def test_stop_before_start(self): sniffer = ProbeRequestSniffer("wlan0") sniffer.stop() + class TestProbeRequestParser(unittest.TestCase): """ Unit tests for the ProbeRequestParser class. @@ -211,6 +227,7 @@ def test_fuzz_packets(self): packet = RadioTap()/fuzz(Dot11()/Dot11ProbeReq()/Dot11Elt()) ProbeRequestSniffer.ProbeRequestParser.parse(packet) + class TestLinter(unittest.TestCase): """ Unit tests for Python linters. From 09e5da17f7b3f4697112039258b65d906462ae65 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Wed, 8 May 2019 23:56:13 +0100 Subject: [PATCH 43/81] Fix linting and style errors in 'probequest/ui/raw.py' --- probequest/ui/raw.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/probequest/ui/raw.py b/probequest/ui/raw.py index 868e523..9b21d70 100644 --- a/probequest/ui/raw.py +++ b/probequest/ui/raw.py @@ -7,6 +7,7 @@ from probequest.probe_request_sniffer import ProbeRequestSniffer + class RawProbeRequestViewer: """ Displays the raw probe requests passing near the Wi-Fi interface. @@ -28,7 +29,7 @@ def write_csv(probe_req): probe_req.essid ]) else: - write_csv = lambda p: None + write_csv = lambda p: None # noqa: E731 def display_probe_req(probe_req): print(probe_req) From de2beb125af4598e1889856db68d06305572a8ef Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sun, 26 May 2019 10:34:05 +0100 Subject: [PATCH 44/81] Fix linting and style errors in 'bin/probequest' --- bin/probequest | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/bin/probequest b/bin/probequest index 1ad30ad..2738b60 100755 --- a/bin/probequest +++ b/bin/probequest @@ -6,11 +6,14 @@ Toolkit allowing to sniff and display the Wi-Fi probe requests passing near your wireless interface. """ +# pylint: disable=no-name-in-module + from argparse import ArgumentParser, FileType from enum import Enum from probequest.version import VERSION + class Mode(Enum): """ Enumeration of the different operational modes @@ -23,12 +26,15 @@ class Mode(Enum): def __str__(self): return self.value + def get_arg_parser(): """ Returns the argument parser. """ - arg_parser = ArgumentParser(description="Toolkit for Playing with Wi-Fi Probe Requests") + arg_parser = ArgumentParser( + description="Toolkit for Playing with Wi-Fi Probe Requests" + ) arg_parser.add_argument( "--debug", action="store_true", help="debug mode" @@ -86,7 +92,12 @@ def get_arg_parser(): return arg_parser -if __name__ == "__main__": + +def main(): + """ + Main function. + """ + from os import geteuid from sys import exit as sys_exit @@ -110,7 +121,11 @@ if __name__ == "__main__": sleep(100) except OSError: raw_viewer.stop() - sys_exit("[!] Interface {interface} doesn't exist".format(interface=args["interface"])) + sys_exit( + "[!] Interface {interface} doesn't exist".format( + interface=args["interface"] + ) + ) except KeyboardInterrupt: print("[*] Stopping the threads...") raw_viewer.stop() @@ -122,8 +137,16 @@ if __name__ == "__main__": pnl_viewer = PNLViewer(interface, **args) pnl_viewer.main() except OSError: - sys_exit("[!] Interface {interface} doesn't exist".format(interface=args["interface"])) + sys_exit( + "[!] Interface {interface} doesn't exist".format( + interface=args["interface"] + ) + ) except KeyboardInterrupt: pnl_viewer.sniffer.stop() else: sys_exit("[x] Invalid mode") + + +if __name__ == "__main__": + main() From eeb23e4004511353d286127a6a34e910e28db9ea Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sun, 14 Jul 2019 22:26:04 +0100 Subject: [PATCH 45/81] Use commit ids in the pre-commit configuration Use the commit ids of the remote pre-commit hooks instead of the Git tags for security reasons. See https://github.com/pre-commit/pre-commit/issues/1068 for further details. --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a3807d8..bb226f2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.2.1 + rev: 0b70e285e369bcb24b57b74929490ea7be9c4b19 # v2.2.3 hooks: - id: check-ast - id: check-executables-have-shebangs @@ -8,6 +8,6 @@ repos: - id: flake8 - id: trailing-whitespace - repo: https://github.com/pre-commit/mirrors-pylint - rev: v2.3.1 + rev: 135c0cb79ced730834391aa6eeb5a27b6f5867ff # v2.3.1 hooks: - id: pylint From 65d092860e59f4b8e5ce2444b200a15253b9db0e Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sun, 14 Jul 2019 22:40:00 +0100 Subject: [PATCH 46/81] Fix import issues with the pylint pre-commit hook Solution found on https://github.com/pre-commit/mirrors-pylint/issues/11. --- .pre-commit-config.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bb226f2..ebda618 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,3 +11,5 @@ repos: rev: 135c0cb79ced730834391aa6eeb5a27b6f5867ff # v2.3.1 hooks: - id: pylint + entry: python3 -m pylint.__main__ + language: system From 21df3064b62a529a85ee47d434423cf0700f3ce6 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sun, 14 Jul 2019 22:44:17 +0100 Subject: [PATCH 47/81] Fix linting and style errors in 'probequest/probe_request.py' --- probequest/probe_request.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/probequest/probe_request.py b/probequest/probe_request.py index 28487d0..6fd480d 100644 --- a/probequest/probe_request.py +++ b/probequest/probe_request.py @@ -5,6 +5,7 @@ from time import localtime, strftime from netaddr import EUI, NotRegisteredError + class ProbeRequest: """ Probe request class. @@ -19,7 +20,10 @@ def __init__(self, timestamp, s_mac, essid): def __str__(self): return "{timestamp} - {s_mac} ({mac_org}) -> {essid}".format( - timestamp=strftime("%a, %d %b %Y %H:%M:%S %Z", localtime(self.timestamp)), + timestamp=strftime( + "%a, %d %b %Y %H:%M:%S %Z", + localtime(self.timestamp) + ), s_mac=self.s_mac, mac_org=self.s_mac_oui, essid=self.essid @@ -30,6 +34,8 @@ def get_mac_organisation(self): Returns the OUI of the MAC address as a string. """ + # pylint: disable=no-member + try: return EUI(self.s_mac).oui.registration().org except NotRegisteredError: From 088697e6da623915074e8421b0a8b8cfb9ef1bfa Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sun, 14 Jul 2019 23:21:07 +0100 Subject: [PATCH 48/81] Fix linting and syntax errors in 'probequest/probe_request_sniffer.py' --- probequest/probe_request_sniffer.py | 58 +++++++++++++++++++---------- 1 file changed, 39 insertions(+), 19 deletions(-) diff --git a/probequest/probe_request_sniffer.py b/probequest/probe_request_sniffer.py index 7851763..09b1484 100644 --- a/probequest/probe_request_sniffer.py +++ b/probequest/probe_request_sniffer.py @@ -13,11 +13,14 @@ from probequest.probe_request import ProbeRequest + class ProbeRequestSniffer: """ Probe request sniffer class. """ + # pylint: disable=too-many-instance-attributes + SNIFFER_STOP_TIMEOUT = 2.0 def __init__(self, interface, **kwargs): @@ -33,9 +36,13 @@ def __init__(self, interface, **kwargs): self.debug = kwargs.get("debug", False) if not hasattr(self.display_func, "__call__"): - raise TypeError("The display function parameter is not a callable object") + raise TypeError( + "The display function parameter is not a callable object" + ) if not hasattr(self.storage_func, "__call__"): - raise TypeError("The storage function parameter is not a callable object") + raise TypeError( + "The storage function parameter is not a callable object" + ) self.new_packets = Queue() self.new_sniffer() @@ -62,10 +69,10 @@ def start(self): self.new_parser() self.parser.start() - e = self.sniffer.get_exception() + exception = self.sniffer.get_exception() - if e is not None: - raise e + if exception is not None: + raise exception self.sniffer_running = True @@ -163,9 +170,13 @@ def __init__(self, interface, new_packets, **kwargs): for i, station in enumerate(self.mac_exclusions): if i == 0: - self.frame_filters += "ether src host {s_mac}".format(s_mac=station) + self.frame_filters += "\ + ether src host {s_mac}".format( + s_mac=station) else: - self.frame_filters += " || ether src host {s_mac}".format(s_mac=station) + self.frame_filters += "\ + || ether src host {s_mac}".format( + s_mac=station) self.frame_filters += ")" @@ -174,9 +185,13 @@ def __init__(self, interface, new_packets, **kwargs): for i, station in enumerate(self.mac_filters): if i == 0: - self.frame_filters += "ether src host {s_mac}".format(s_mac=station) + self.frame_filters += "\ + ether src host {s_mac}".format( + s_mac=station) else: - self.frame_filters += " || ether src host {s_mac}".format(s_mac=station) + self.frame_filters += "\ + || ether src host {s_mac}".format( + s_mac=station) self.frame_filters += ")" @@ -197,11 +212,12 @@ def run(self): prn=self.new_packet, stop_filter=self.should_stop_sniffer ) - except Exception as e: - self.exception = e + # pylint: disable=broad-except + except Exception as exception: + self.exception = exception if self.debug: - print("[!] Exception: " + str(e)) + print("[!] Exception: " + str(exception)) def join(self, timeout=None): """ @@ -239,7 +255,6 @@ class FakePacketSniffer(Thread): and test purposes. """ - def __init__(self, new_packets, **kwargs): super().__init__() @@ -262,11 +277,12 @@ def run(self): while not self.stop_sniffer.isSet(): sleep(1) self.new_packet() - except Exception as e: - self.exception = e + # pylint: disable=broad-except + except Exception as exception: + self.exception = exception if self.debug: - print("[!] Exception: " + str(e)) + print("[!] Exception: " + str(exception)) def join(self, timeout=None): """ @@ -281,6 +297,8 @@ def new_packet(self): Adds a new fake packet to the queue to be processed. """ + # pylint: disable=no-member + fake_probe_req = RadioTap() \ / Dot11( addr1="ff:ff:ff:ff:ff:ff", @@ -296,7 +314,8 @@ def new_packet(self): def should_stop_sniffer(self): """ - Returns true if the fake sniffer should be stopped and false otherwise. + Returns true if the fake sniffer should be stopped + and false otherwise. """ return self.stop_sniffer.isSet() @@ -353,11 +372,12 @@ def run(self): continue if (self.essid_filters is not None - and not probe_request.essid in self.essid_filters): + and probe_request.essid not in self.essid_filters): continue if (self.essid_regex is not None - and not match(self.essid_regex, probe_request.essid)): + and not + match(self.essid_regex, probe_request.essid)): continue self.display_func(probe_request) From 5945d5c06fcf378de3ae6398aa2e058357a7c4eb Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sun, 14 Jul 2019 23:25:31 +0100 Subject: [PATCH 49/81] Fix linting and syntax errors in 'probequest/ui/pnl.py' --- probequest/ui/pnl.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/probequest/ui/pnl.py b/probequest/ui/pnl.py index 0dff147..5a8397c 100644 --- a/probequest/ui/pnl.py +++ b/probequest/ui/pnl.py @@ -9,12 +9,14 @@ from probequest.probe_request_sniffer import ProbeRequestSniffer + class PNLViewer: """ - TUI used to display the PNL of the nearby devices sending - probe requests. + TUI used to display the PNL of the nearby devices sending probe requests. """ + # pylint: disable=too-many-instance-attributes + palette = [ ("header_running", "white", "dark green", "bold"), ("header_stopped", "white", "dark red", "bold"), @@ -88,8 +90,8 @@ def setup_view(self): def setup_menu(self, title, choices): """ - Creates and returns a dynamic ListBox object containing - a title and the choices given as parameters. + Creates and returns a dynamic ListBox object containing a title and the + choices given as parameters. """ body = [urwid.Text(title), urwid.Divider()] @@ -110,7 +112,8 @@ def new_probe_req(self, probe_req): self.stations[probe_req.s_mac] = [] self.add_station(probe_req.s_mac) - if not any(essid.text == probe_req.essid for essid in self.stations[probe_req.s_mac]): + if not any(essid.text == probe_req.essid + for essid in self.stations[probe_req.s_mac]): self.stations[probe_req.s_mac].append(urwid.Text(probe_req.essid)) if len(self.stations.keys()) == 1: @@ -126,12 +129,13 @@ def add_station(self, name): button = urwid.Button(name) urwid.connect_signal(button, "click", self.station_chosen, name) - self.station_list.body.append(urwid.AttrMap(button, None, focus_map="selected")) + self.station_list.body.append( + urwid.AttrMap(button, None, focus_map="selected") + ) def station_chosen(self, button, choice): """ - Callback method called when a station is selected - in the stations list. + Callback method called when a station is selected in the station list. """ # pylint: disable=unused-argument @@ -189,8 +193,8 @@ def exit_program(self): def unhandled_keypress(self, key): """ - Contains handlers for each keypress that is not handled - by the widgets being displayed. + Contains handlers for each keypress that is not handled by the widgets + being displayed. """ if key in ("q", "Q"): From eab23dd4d5ea4b094b7f55014d0bce29da199d2f Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sun, 14 Jul 2019 23:28:31 +0100 Subject: [PATCH 50/81] Disable Pylint on Sphinx configuration file --- docs/conf.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index fe97a76..ae36a3c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,10 +12,14 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # + +# pylint: skip-file + import os import sys -sys.path.insert(0, os.path.abspath('..')) +from probequest.version import VERSION +sys.path.insert(0, os.path.abspath('..')) # -- Project information ----------------------------------------------------- @@ -23,8 +27,6 @@ copyright = '2018, Paul-Emmanuel Raoul' author = 'Paul-Emmanuel Raoul' -from probequest.version import VERSION - # The short X.Y version version = ".".join(VERSION.split(".")[:2]) # The full version, including alpha/beta/rc tags @@ -200,9 +202,9 @@ # -- Options for GitHub integration ------------------------------------------ html_context = { - 'display_github': True, # Integrate GitHub - 'github_user': 'SkypLabs', # Username - 'github_repo': 'probequest', # Repo name - 'github_version': 'master', # Version - 'conf_py_path': '/docs/', # Path in the checkout to the docs root + 'display_github': True, # Integrate GitHub + 'github_user': 'SkypLabs', # Username + 'github_repo': 'probequest', # Repo name + 'github_version': 'master', # Version + 'conf_py_path': '/docs/', # Path in the checkout to the docs root } From 7cea779eb34abc2a92e120f29e2fdda77a709193 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sat, 3 Aug 2019 13:05:48 +0100 Subject: [PATCH 51/81] Add the 'refactor' GitHub issue label --- .github/settings.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/settings.yml b/.github/settings.yml index 4086f82..478e8a6 100644 --- a/.github/settings.yml +++ b/.github/settings.yml @@ -23,6 +23,9 @@ labels: - name: optimisation description: Optimisation color: 84b6eb + - name: refactor + description: Refactoring + color: 84b6eb - name: sniffer description: Related to the sniffer From ddf36998b4c62e8be4700ab8062d70494421db3d Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sat, 3 Aug 2019 13:11:35 +0100 Subject: [PATCH 52/81] Add platform-specific GitHub issue labels --- .github/settings.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/settings.yml b/.github/settings.yml index 478e8a6..2690785 100644 --- a/.github/settings.yml +++ b/.github/settings.yml @@ -37,6 +37,19 @@ labels: description: Related to the user interface color: 1d76db + - name: android + description: Android platform support issues + color: #047277 + - name: linux + description: Linux platform support issues + color: #047277 + - name: macos + description: Apple macOS platform support issues + color: #047277 + - name: windows + description: Microsoft Windows platform support issues + color: #047277 + - name: bug description: New bug color: ee0701 From 0ea8f6e213f8ebd83bc7a10c5bd473fb2c0f41b5 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sat, 3 Aug 2019 13:15:14 +0100 Subject: [PATCH 53/81] Fix the 'color' values of some GitHub issue labels --- .github/settings.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/settings.yml b/.github/settings.yml index 2690785..6828696 100644 --- a/.github/settings.yml +++ b/.github/settings.yml @@ -39,16 +39,16 @@ labels: - name: android description: Android platform support issues - color: #047277 + color: 047277 - name: linux description: Linux platform support issues - color: #047277 + color: 047277 - name: macos description: Apple macOS platform support issues - color: #047277 + color: 047277 - name: windows description: Microsoft Windows platform support issues - color: #047277 + color: 047277 - name: bug description: New bug From 398df0d247348e330443931367f6dda13ddb3f41 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sat, 3 Aug 2019 13:29:51 +0100 Subject: [PATCH 54/81] Change the colour of the platform-specific labels --- .github/settings.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/settings.yml b/.github/settings.yml index 6828696..91eef3b 100644 --- a/.github/settings.yml +++ b/.github/settings.yml @@ -39,16 +39,16 @@ labels: - name: android description: Android platform support issues - color: 047277 + color: 04727a - name: linux description: Linux platform support issues - color: 047277 + color: 04727a - name: macos description: Apple macOS platform support issues - color: 047277 + color: 04727a - name: windows description: Microsoft Windows platform support issues - color: 047277 + color: 04727a - name: bug description: New bug From e32ca242de1a0a1a453132b0dc4063ce84fc8bfa Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sat, 17 Aug 2019 13:25:05 +0100 Subject: [PATCH 55/81] Add the 'Config' object --- bin/probequest | 41 +++++---- probequest/config.py | 79 ++++++++++++++++++ probequest/probe_request_sniffer.py | 125 ++++++++++------------------ probequest/ui/pnl.py | 15 ++-- probequest/ui/raw.py | 16 ++-- 5 files changed, 158 insertions(+), 118 deletions(-) create mode 100644 probequest/config.py diff --git a/bin/probequest b/bin/probequest index 2738b60..4133b79 100755 --- a/bin/probequest +++ b/bin/probequest @@ -9,24 +9,11 @@ probe requests passing near your wireless interface. # pylint: disable=no-name-in-module from argparse import ArgumentParser, FileType -from enum import Enum +from probequest.config import Config, Mode from probequest.version import VERSION -class Mode(Enum): - """ - Enumeration of the different operational modes - supported by this software. - """ - - RAW = "raw" - PNL = "pnl" - - def __str__(self): - return self.value - - def get_arg_parser(): """ Returns the argument parser. @@ -37,28 +24,34 @@ def get_arg_parser(): ) arg_parser.add_argument( "--debug", action="store_true", + dest="debug", help="debug mode" ) arg_parser.add_argument( "--fake", action="store_true", + dest="fake", help="display only fake ESSIDs") arg_parser.add_argument( "-i", "--interface", required=True, + dest="interface", help="wireless interface to use (must be in monitor mode)" ) arg_parser.add_argument( "--ignore-case", action="store_true", + dest="ignore_case", help="ignore case distinctions in the regex pattern (default: false)" ) arg_parser.add_argument( "--mode", type=Mode, choices=Mode.__members__.values(), + dest="mode", help="set the mode to use" ) arg_parser.add_argument( "-o", "--output", type=FileType("a"), + dest="output_file", help="output file to save the captured data (CSV format)" ) arg_parser.add_argument("--version", action="version", version=VERSION) @@ -71,10 +64,12 @@ def get_arg_parser(): essid_arguments.add_argument( "-e", "--essid", nargs="+", + dest="essid_filters", help="ESSID of the APs to filter (space-separated list)" ) essid_arguments.add_argument( "-r", "--regex", + dest="essid_regex", help="regex to filter the ESSIDs" ) @@ -82,11 +77,13 @@ def get_arg_parser(): station_arguments.add_argument( "--exclude", nargs="+", + dest="mac_exclusions", help="MAC addresses of the stations to exclude (space-separated list)" ) station_arguments.add_argument( "-s", "--station", nargs="+", + dest="mac_filters", help="MAC addresses of the stations to filter (space-separated list)" ) @@ -101,20 +98,20 @@ def main(): from os import geteuid from sys import exit as sys_exit - args = vars(get_arg_parser().parse_args()) - interface = args.pop("interface") + config = Config() + get_arg_parser().parse_args(namespace=config) if not geteuid() == 0: sys_exit("[!] You must be root") # Default mode. - if args["mode"] == Mode.RAW: + if config.mode == Mode.RAW: from time import sleep from probequest.ui.raw import RawProbeRequestViewer try: print("[*] Start sniffing probe requests...") - raw_viewer = RawProbeRequestViewer(interface, **args) + raw_viewer = RawProbeRequestViewer(config) raw_viewer.start() while True: @@ -123,23 +120,23 @@ def main(): raw_viewer.stop() sys_exit( "[!] Interface {interface} doesn't exist".format( - interface=args["interface"] + interface=config.interface ) ) except KeyboardInterrupt: print("[*] Stopping the threads...") raw_viewer.stop() print("[*] Bye!") - elif args["mode"] == Mode.PNL: + elif config.mode == Mode.PNL: from probequest.ui.pnl import PNLViewer try: - pnl_viewer = PNLViewer(interface, **args) + pnl_viewer = PNLViewer(config) pnl_viewer.main() except OSError: sys_exit( "[!] Interface {interface} doesn't exist".format( - interface=args["interface"] + interface=config.interface ) ) except KeyboardInterrupt: diff --git a/probequest/config.py b/probequest/config.py new file mode 100644 index 0000000..046c9ec --- /dev/null +++ b/probequest/config.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +ProbeQuest configuration. +""" + +from enum import Enum + + +class Mode(Enum): + """ + Enumeration of the different operational modes + supported by this software. + """ + + RAW = "raw" + PNL = "pnl" + + def __str__(self): + return self.value + + +class Config: + """ + Configuration object. + """ + + interface = None + + essid_filters = None + essid_regex = None + ignore_case = True + + mac_exclusions = None + mac_filters = None + + output_file = None + + mode = Mode.RAW + fake = False + debug = False + + _display_func = lambda *args: None # noqa: E731 + _storage_func = lambda *args: None # noqa: E731 + + @property + def display_func(self): + """ + Callback function triggered when a packet needs to be displayed. + """ + + return self._display_func + + @property + def storage_func(self): + """ + Callback function triggered when a packet needs to be stored. + """ + + return self._storage_func + + @display_func.setter + def display_func(self, func): + if not hasattr(func, "__call__"): + raise TypeError( + "The display function property is not a callable object" + ) + + self._display_func = func + + @storage_func.setter + def storage_func(self, func): + if not hasattr(func, "__call__"): + raise TypeError( + "The storage function property is not a callable object" + ) + + self._storage_func = func diff --git a/probequest/probe_request_sniffer.py b/probequest/probe_request_sniffer.py index 09b1484..68aa33a 100644 --- a/probequest/probe_request_sniffer.py +++ b/probequest/probe_request_sniffer.py @@ -6,7 +6,7 @@ from re import compile as rcompile, match, IGNORECASE from threading import Thread, Event -from scapy.config import conf +from scapy.config import conf as sconf from scapy.data import ETH_P_ALL from scapy.layers.dot11 import RadioTap, Dot11, Dot11ProbeReq, Dot11Elt from scapy.sendrecv import sniff @@ -19,30 +19,10 @@ class ProbeRequestSniffer: Probe request sniffer class. """ - # pylint: disable=too-many-instance-attributes - SNIFFER_STOP_TIMEOUT = 2.0 - def __init__(self, interface, **kwargs): - self.interface = interface - self.essid_filters = kwargs.get("essid", None) - self.essid_regex = kwargs.get("regex", None) - self.ignore_case = kwargs.get("ignore_case", None) - self.mac_exclusions = kwargs.get("exclude", None) - self.mac_filters = kwargs.get("station", None) - self.display_func = kwargs.get("display_func", lambda p: None) - self.storage_func = kwargs.get("storage_func", lambda p: None) - self.fake = kwargs.get("fake", False) - self.debug = kwargs.get("debug", False) - - if not hasattr(self.display_func, "__call__"): - raise TypeError( - "The display function parameter is not a callable object" - ) - if not hasattr(self.storage_func, "__call__"): - raise TypeError( - "The storage function parameter is not a callable object" - ) + def __init__(self, config): + self.config = config self.new_packets = Queue() self.new_sniffer() @@ -105,18 +85,15 @@ def new_sniffer(self): Creates a new sniffing thread. """ - if self.fake: + if self.config.fake: self.sniffer = self.FakePacketSniffer( - self.new_packets, - debug=self.debug + self.config, + self.new_packets ) else: self.sniffer = self.PacketSniffer( - self.interface, - self.new_packets, - mac_exclusions=self.mac_exclusions, - mac_filters=self.mac_filters, - debug=self.debug + self.config, + self.new_packets ) def new_parser(self): @@ -125,13 +102,8 @@ def new_parser(self): """ self.parser = self.ProbeRequestParser( - self.new_packets, - essid_filters=self.essid_filters, - essid_regex=self.essid_regex, - ignore_case=self.ignore_case, - display_func=self.display_func, - storage_func=self.storage_func, - debug=self.debug + self.config, + self.new_packets ) def is_running(self): @@ -147,28 +119,24 @@ class PacketSniffer(Thread): A packet sniffing thread. """ - def __init__(self, interface, new_packets, **kwargs): + def __init__(self, config, new_packets): super().__init__() self.daemon = True - self.interface = interface + self.config = config self.new_packets = new_packets - self.mac_exclusions = kwargs.get("mac_exclusions", None) - self.mac_filters = kwargs.get("mac_filters", None) - self.debug = kwargs.get("debug", False) - self.frame_filters = "type mgt subtype probe-req" self.socket = None self.stop_sniffer = Event() self.exception = None - if self.mac_exclusions is not None: + if self.config.mac_exclusions is not None: self.frame_filters += " and not (" - for i, station in enumerate(self.mac_exclusions): + for i, station in enumerate(self.config.mac_exclusions): if i == 0: self.frame_filters += "\ ether src host {s_mac}".format( @@ -180,10 +148,10 @@ def __init__(self, interface, new_packets, **kwargs): self.frame_filters += ")" - if self.mac_filters is not None: + if self.config.mac_filters is not None: self.frame_filters += " and (" - for i, station in enumerate(self.mac_filters): + for i, station in enumerate(self.config.mac_filters): if i == 0: self.frame_filters += "\ ether src host {s_mac}".format( @@ -195,14 +163,14 @@ def __init__(self, interface, new_packets, **kwargs): self.frame_filters += ")" - if self.debug: + if self.config.debug: print("[!] Frame filters: " + self.frame_filters) def run(self): try: - self.socket = conf.L2listen( + self.socket = sconf.L2listen( type=ETH_P_ALL, - iface=self.interface, + iface=self.config.interface, filter=self.frame_filters ) @@ -216,7 +184,7 @@ def run(self): except Exception as exception: self.exception = exception - if self.debug: + if self.config.debug: print("[!] Exception: " + str(exception)) def join(self, timeout=None): @@ -255,15 +223,15 @@ class FakePacketSniffer(Thread): and test purposes. """ - def __init__(self, new_packets, **kwargs): + def __init__(self, config, new_packets): super().__init__() + self.config = config self.new_packets = new_packets + self.stop_sniffer = Event() self.exception = None - self.debug = kwargs.get("debug", False) - from faker import Faker from faker_wifi_essid import WifiESSID @@ -281,7 +249,7 @@ def run(self): except Exception as exception: self.exception = exception - if self.debug: + if self.config.debug: print("[!] Exception: " + str(exception)) def join(self, timeout=None): @@ -331,31 +299,29 @@ class ProbeRequestParser(Thread): A Wi-Fi probe request parsing thread. """ - def __init__(self, new_packets, **kwargs): + def __init__(self, config, new_packets): super().__init__() + self.config = config self.new_packets = new_packets - self.essid_filters = kwargs.get("essid_filters", None) - self.essid_regex = kwargs.get("essid_regex", None) - self.ignore_case = kwargs.get("ignore_case", False) - self.display_func = kwargs.get("display_func", lambda p: None) - self.storage_func = kwargs.get("storage_func", lambda p: None) - self.debug = kwargs.get("debug", False) self.stop_parser = Event() - if self.debug: - print("[!] ESSID filters: " + str(self.essid_filters)) - print("[!] ESSID regex: " + str(self.essid_regex)) - print("[!] Ignore case: " + str(self.ignore_case)) - - if self.essid_regex is not None: - if self.ignore_case: - self.essid_regex = rcompile(self.essid_regex, IGNORECASE) + if self.config.debug: + print("[!] ESSID filters: " + str(self.config.essid_filters)) + print("[!] ESSID regex: " + str(self.config.essid_regex)) + print("[!] Ignore case: " + str(self.config.ignore_case)) + + if self.config.essid_regex is not None: + if self.config.ignore_case: + self.cregex = rcompile( + self.config.essid_regex, + IGNORECASE + ) else: - self.essid_regex = rcompile(self.essid_regex) + self.cregex = rcompile(self.config.essid_regex) else: - self.essid_regex = None + self.cregex = None def run(self): # The parser continues to do its job even after the call of the @@ -371,17 +337,18 @@ def run(self): if not probe_request.essid: continue - if (self.essid_filters is not None - and probe_request.essid not in self.essid_filters): + if (self.config.essid_filters is not None + and probe_request.essid + not in self.config.essid_filters): continue - if (self.essid_regex is not None + if (self.cregex is not None and not - match(self.essid_regex, probe_request.essid)): + match(self.cregex, probe_request.essid)): continue - self.display_func(probe_request) - self.storage_func(probe_request) + self.config.display_func(probe_request) + self.config.storage_func(probe_request) self.new_packets.task_done() except Empty: diff --git a/probequest/ui/pnl.py b/probequest/ui/pnl.py index 5a8397c..04d61a4 100644 --- a/probequest/ui/pnl.py +++ b/probequest/ui/pnl.py @@ -31,16 +31,14 @@ class PNLViewer: ("key", "Q"), " quit", ]) - def __init__(self, interface, **kwargs): - self.interface = interface + def __init__(self, config): + self.config = config + self.stations = dict() self.loop = None - self.sniffer = ProbeRequestSniffer( - self.interface, - display_func=self.new_probe_req, - **kwargs - ) + self.config.display_func = self.new_probe_req + self.sniffer = ProbeRequestSniffer(config) self.view = self.setup_view() @@ -49,7 +47,7 @@ def setup_view(self): Returns the root widget. """ - self.interface_text = urwid.Text(self.interface) + self.interface_text = urwid.Text(self.config.interface) self.sniffer_state_text = urwid.Text("Stopped") self.header = urwid.AttrWrap(urwid.Columns([ @@ -139,6 +137,7 @@ def station_chosen(self, button, choice): """ # pylint: disable=unused-argument + # 'button' is the widget object passed by Urwid. self.pnl_list.body = self.stations[choice] diff --git a/probequest/ui/raw.py b/probequest/ui/raw.py index 9b21d70..e28277f 100644 --- a/probequest/ui/raw.py +++ b/probequest/ui/raw.py @@ -13,8 +13,8 @@ class RawProbeRequestViewer: Displays the raw probe requests passing near the Wi-Fi interface. """ - def __init__(self, interface, **kwargs): - self.output = kwargs.get("output", None) + def __init__(self, config): + self.output = config.output_file if self.output is not None: from csv import writer @@ -29,17 +29,15 @@ def write_csv(probe_req): probe_req.essid ]) else: - write_csv = lambda p: None # noqa: E731 + write_csv = lambda *args: None # noqa: E731 def display_probe_req(probe_req): print(probe_req) - self.sniffer = ProbeRequestSniffer( - interface, - display_func=display_probe_req, - storage_func=write_csv, - **kwargs - ) + config.display_func = display_probe_req + config.storage_func = write_csv + + self.sniffer = ProbeRequestSniffer(config) def start(self): """ From 71965559a1f42d478e81da94d9cf26d10c622aac Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sat, 17 Aug 2019 16:07:36 +0100 Subject: [PATCH 56/81] Fix the unit tests --- test/test.py | 57 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/test/test.py b/test/test.py index 02951d8..8ab02f3 100644 --- a/test/test.py +++ b/test/test.py @@ -10,6 +10,7 @@ from scapy.layers.dot11 import RadioTap, Dot11, Dot11ProbeReq, Dot11Elt from scapy.packet import fuzz from netaddr.core import AddrFormatError +from probequest.config import Config from probequest.probe_request import ProbeRequest from probequest.probe_request_sniffer import ProbeRequestSniffer @@ -106,45 +107,55 @@ def test_print_a_probe_request(self): ) -class TestProbeRequestSniffer(unittest.TestCase): +class TestConfig(unittest.TestCase): """ - Unit tests for the ProbeRequestSniffer class. + Unit tests for the 'Config' class. """ - def test_without_parameters(self): + def test_bad_display_function(self): """ - Initialises a ProbeRequestSniffer object - without parameters. + Assigns a non-callable object to the display callback function. """ - # pylint: disable=no-value-for-parameter - with self.assertRaises(TypeError): - sniffer = ProbeRequestSniffer() # noqa: F841 + config = Config() + config.display_func = "test" - def test_bad_display_function(self): + def test_bad_storage_function(self): """ - Initialises a ProbeRequestSniffer object - with a non-callable display function. + Assigns a non-callable object to the storage callback function. """ with self.assertRaises(TypeError): - sniffer = ProbeRequestSniffer( # noqa: F841 - "wlan0", - display_func="Test" - ) + config = Config() + config.storage_func = "test" - def test_bad_storage_function(self): + +class TestProbeRequestSniffer(unittest.TestCase): + """ + Unit tests for the 'ProbeRequestSniffer' class. + """ + + def test_without_parameters(self): """ Initialises a ProbeRequestSniffer object with a non-callable storage function. """ + # pylint: disable=no-value-for-parameter + with self.assertRaises(TypeError): - sniffer = ProbeRequestSniffer( # noqa: F841 - "wlan0", - storage_func="Test" - ) + sniffer = ProbeRequestSniffer() # noqa: F841 + + def test_bad_parameter(self): + """ + Initialises a 'ProbeRequestSniffer' object with a bad parameter. + """ + + # pylint: disable=no-value-for-parameter + + with self.assertRaises(AttributeError): + sniffer = ProbeRequestSniffer("test") # noqa: F841 def test_create_sniffer(self): """ @@ -153,7 +164,8 @@ def test_create_sniffer(self): # pylint: disable=no-self-use - sniffer = ProbeRequestSniffer("wlan0") # noqa: F841 + config = Config() + sniffer = ProbeRequestSniffer(config) # noqa: F841 def test_stop_before_start(self): """ @@ -164,7 +176,8 @@ def test_stop_before_start(self): # pylint: disable=no-self-use - sniffer = ProbeRequestSniffer("wlan0") + config = Config() + sniffer = ProbeRequestSniffer(config) sniffer.stop() From 6fc883d4e2bb89df7d2aaee9aa44960c194dc3d5 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sat, 17 Aug 2019 16:11:52 +0100 Subject: [PATCH 57/81] Update comments in the unit tests --- test/test.py | 46 +++++++++++++++++----------------------------- 1 file changed, 17 insertions(+), 29 deletions(-) diff --git a/test/test.py b/test/test.py index 8ab02f3..363019a 100644 --- a/test/test.py +++ b/test/test.py @@ -17,13 +17,12 @@ class TestProbeRequest(unittest.TestCase): """ - Unit tests for the ProbeRequest class. + Unit tests for the 'ProbeRequest' class. """ def test_without_parameters(self): """ - Initialises a ProbeRequest object - without any parameter. + Initialises a 'ProbeRequest' object without any parameter. """ # pylint: disable=no-value-for-parameter @@ -33,8 +32,7 @@ def test_without_parameters(self): def test_with_only_one_parameter(self): """ - Initialises a ProbeRequest object - with only one parameter. + Initialises a 'ProbeRequest' object with only one parameter. """ # pylint: disable=no-value-for-parameter @@ -46,8 +44,7 @@ def test_with_only_one_parameter(self): def test_with_only_two_parameters(self): """ - Initialises a ProbeRequest object - with only two parameters. + Initialises a 'ProbeRequest' object with only two parameters. """ # pylint: disable=no-value-for-parameter @@ -60,8 +57,7 @@ def test_with_only_two_parameters(self): def test_create_a_probe_request(self): """ - Creates a new ProbeRequest with all - the required parameters. + Creates a new 'ProbeRequest' with all the required parameters. """ # pylint: disable=no-self-use @@ -74,8 +70,7 @@ def test_create_a_probe_request(self): def test_bad_mac_address(self): """ - Initialises a ProbeRequest object - with a malformed MAC address. + Initialises a 'ProbeRequest' object with a malformed MAC address. """ timestamp = 1517872027.0 @@ -87,8 +82,7 @@ def test_bad_mac_address(self): def test_print_a_probe_request(self): """ - Initialises a ProbeRequest object - and prints it. + Initialises a 'ProbeRequest' object and prints it. """ timestamp = 1517872027.0 @@ -138,8 +132,7 @@ class TestProbeRequestSniffer(unittest.TestCase): def test_without_parameters(self): """ - Initialises a ProbeRequestSniffer object - with a non-callable storage function. + Initialises a 'ProbeRequestSniffer' object without parameters. """ # pylint: disable=no-value-for-parameter @@ -159,7 +152,7 @@ def test_bad_parameter(self): def test_create_sniffer(self): """ - Creates a ProbeRequestSniffer object. + Creates a 'ProbeRequestSniffer' object with the correct parameter. """ # pylint: disable=no-self-use @@ -169,9 +162,8 @@ def test_create_sniffer(self): def test_stop_before_start(self): """ - Creates a ProbeRequestSniffer object - and stops the sniffer before starting - it. + Creates a 'ProbeRequestSniffer' object and stops the sniffer before + starting it. """ # pylint: disable=no-self-use @@ -183,13 +175,12 @@ def test_stop_before_start(self): class TestProbeRequestParser(unittest.TestCase): """ - Unit tests for the ProbeRequestParser class. + Unit tests for the 'ProbeRequestParser' class. """ def test_no_probe_request_layer(self): """ - Creates a non-probe-request Wi-Fi - packet and parses it with the + Creates a non-probe-request Wi-Fi packet and parses it with the 'ProbeRequestParser.parse()' function. """ @@ -206,10 +197,8 @@ def test_no_probe_request_layer(self): def test_empty_essid(self): """ - Creates a probe request packet - with an empty ESSID field and - parses it with the - 'ProbeRequestParser.parse()' function. + Creates a probe request packet with an empty ESSID field and parses + it with the 'ProbeRequestParser.parse()' function. """ # pylint: disable=no-self-use @@ -229,9 +218,8 @@ def test_empty_essid(self): def test_fuzz_packets(self): """ - Parses 1000 randomly-generated probe - requests with the ProbeRequestParser.parse() - function. + Parses 1000 randomly-generated probe requests with the + 'ProbeRequestParser.parse()' function. """ # pylint: disable=no-self-use From ae3805cf69197b585ee9d3c0bfbdc47651b83e60 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sat, 17 Aug 2019 19:09:26 +0100 Subject: [PATCH 58/81] Add the 'generate_frame_filter' method to 'Config' --- probequest/config.py | 40 +++++++++++++++++++++++++++++ probequest/probe_request_sniffer.py | 37 +++----------------------- 2 files changed, 44 insertions(+), 33 deletions(-) diff --git a/probequest/config.py b/probequest/config.py index 046c9ec..023364f 100644 --- a/probequest/config.py +++ b/probequest/config.py @@ -77,3 +77,43 @@ def storage_func(self, func): ) self._storage_func = func + + def generate_frame_filter(self): + """ + Generates and returns the frame filter according to the different + options set of the current 'Config' object. + """ + + frame_filter = "type mgt subtype probe-req" + + if self.mac_exclusions is not None: + frame_filter += " and not (" + + for i, station in enumerate(self.mac_exclusions): + if i == 0: + frame_filter += "\ + ether src host {s_mac}".format( + s_mac=station) + else: + frame_filter += "\ + || ether src host {s_mac}".format( + s_mac=station) + + frame_filter += ")" + + if self.mac_filters is not None: + frame_filter += " and (" + + for i, station in enumerate(self.mac_filters): + if i == 0: + frame_filter += "\ + ether src host {s_mac}".format( + s_mac=station) + else: + frame_filter += "\ + || ether src host {s_mac}".format( + s_mac=station) + + frame_filter += ")" + + return frame_filter diff --git a/probequest/probe_request_sniffer.py b/probequest/probe_request_sniffer.py index 68aa33a..cdee8da 100644 --- a/probequest/probe_request_sniffer.py +++ b/probequest/probe_request_sniffer.py @@ -127,51 +127,22 @@ def __init__(self, config, new_packets): self.config = config self.new_packets = new_packets - self.frame_filters = "type mgt subtype probe-req" + self.frame_filter = self.config.generate_frame_filter() + self.socket = None self.stop_sniffer = Event() self.exception = None - if self.config.mac_exclusions is not None: - self.frame_filters += " and not (" - - for i, station in enumerate(self.config.mac_exclusions): - if i == 0: - self.frame_filters += "\ - ether src host {s_mac}".format( - s_mac=station) - else: - self.frame_filters += "\ - || ether src host {s_mac}".format( - s_mac=station) - - self.frame_filters += ")" - - if self.config.mac_filters is not None: - self.frame_filters += " and (" - - for i, station in enumerate(self.config.mac_filters): - if i == 0: - self.frame_filters += "\ - ether src host {s_mac}".format( - s_mac=station) - else: - self.frame_filters += "\ - || ether src host {s_mac}".format( - s_mac=station) - - self.frame_filters += ")" - if self.config.debug: - print("[!] Frame filters: " + self.frame_filters) + print("[!] Frame filter: " + self.frame_filter) def run(self): try: self.socket = sconf.L2listen( type=ETH_P_ALL, iface=self.config.interface, - filter=self.frame_filters + filter=self.frame_filter ) sniff( From c1575e26c7779ac8c33c214c7b7ed9ec1c31c42f Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sat, 17 Aug 2019 19:10:08 +0100 Subject: [PATCH 59/81] Fix the default value of '--ignore-case' --- probequest/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/probequest/config.py b/probequest/config.py index 023364f..cf509f3 100644 --- a/probequest/config.py +++ b/probequest/config.py @@ -30,7 +30,7 @@ class Config: essid_filters = None essid_regex = None - ignore_case = True + ignore_case = False mac_exclusions = None mac_filters = None From 46ed183736c5a73d6fec338ae6f11033bd3fb8f0 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sat, 17 Aug 2019 19:37:42 +0100 Subject: [PATCH 60/81] Add the 'compile_essid_regex' method to 'Config' --- probequest/config.py | 17 +++++++++++++++++ probequest/probe_request_sniffer.py | 15 +++------------ 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/probequest/config.py b/probequest/config.py index cf509f3..e960e33 100644 --- a/probequest/config.py +++ b/probequest/config.py @@ -6,6 +6,7 @@ """ from enum import Enum +from re import compile as rcompile, IGNORECASE class Mode(Enum): @@ -117,3 +118,19 @@ def generate_frame_filter(self): frame_filter += ")" return frame_filter + + def complile_essid_regex(self): + """ + Returns the compiled version of the ESSID regex. + """ + + if self.essid_regex is not None: + if self.ignore_case: + return rcompile( + self.essid_regex, + IGNORECASE + ) + + return rcompile(self.essid_regex) + + return None diff --git a/probequest/probe_request_sniffer.py b/probequest/probe_request_sniffer.py index cdee8da..d47a57f 100644 --- a/probequest/probe_request_sniffer.py +++ b/probequest/probe_request_sniffer.py @@ -3,7 +3,7 @@ """ from queue import Queue, Empty -from re import compile as rcompile, match, IGNORECASE +from re import match from threading import Thread, Event from scapy.config import conf as sconf @@ -276,6 +276,8 @@ def __init__(self, config, new_packets): self.config = config self.new_packets = new_packets + self.cregex = self.config.complile_essid_regex() + self.stop_parser = Event() if self.config.debug: @@ -283,17 +285,6 @@ def __init__(self, config, new_packets): print("[!] ESSID regex: " + str(self.config.essid_regex)) print("[!] Ignore case: " + str(self.config.ignore_case)) - if self.config.essid_regex is not None: - if self.config.ignore_case: - self.cregex = rcompile( - self.config.essid_regex, - IGNORECASE - ) - else: - self.cregex = rcompile(self.config.essid_regex) - else: - self.cregex = None - def run(self): # The parser continues to do its job even after the call of the # join method if the queue is not empty. From 395736d2d8685a56efb612ec3aaa5e1be0dfdf40 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sat, 17 Aug 2019 19:40:30 +0100 Subject: [PATCH 61/81] Fix missing parameter in 'should_stop_sniffer' --- probequest/probe_request_sniffer.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/probequest/probe_request_sniffer.py b/probequest/probe_request_sniffer.py index d47a57f..c966233 100644 --- a/probequest/probe_request_sniffer.py +++ b/probequest/probe_request_sniffer.py @@ -173,11 +173,13 @@ def new_packet(self, packet): self.new_packets.put(packet) - def should_stop_sniffer(self): + def should_stop_sniffer(self, packet): """ Returns true if the sniffer should be stopped and false otherwise. """ + # pylint: disable=unused-argument + return self.stop_sniffer.isSet() def get_exception(self): @@ -251,12 +253,14 @@ def new_packet(self): self.new_packets.put(fake_probe_req) - def should_stop_sniffer(self): + def should_stop_sniffer(self, packet): """ Returns true if the fake sniffer should be stopped and false otherwise. """ + # pylint: disable=unused-argument + return self.stop_sniffer.isSet() def get_exception(self): From 56dd651fa2b9143634e30c2ce2adb79642cbaa20 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sat, 17 Aug 2019 20:24:00 +0100 Subject: [PATCH 62/81] Add tests for the 'generate_frame_filter' method --- probequest/config.py | 20 ++++++++------------ test/test.py | 45 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 12 deletions(-) diff --git a/probequest/config.py b/probequest/config.py index e960e33..60d173c 100644 --- a/probequest/config.py +++ b/probequest/config.py @@ -92,13 +92,11 @@ def generate_frame_filter(self): for i, station in enumerate(self.mac_exclusions): if i == 0: - frame_filter += "\ - ether src host {s_mac}".format( - s_mac=station) + frame_filter += "ether src host {s_mac}".format( + s_mac=station) else: - frame_filter += "\ - || ether src host {s_mac}".format( - s_mac=station) + frame_filter += "|| ether src host {s_mac}".format( + s_mac=station) frame_filter += ")" @@ -107,13 +105,11 @@ def generate_frame_filter(self): for i, station in enumerate(self.mac_filters): if i == 0: - frame_filter += "\ - ether src host {s_mac}".format( - s_mac=station) + frame_filter += "ether src host {s_mac}".format( + s_mac=station) else: - frame_filter += "\ - || ether src host {s_mac}".format( - s_mac=station) + frame_filter += "|| ether src host {s_mac}".format( + s_mac=station) frame_filter += ")" diff --git a/test/test.py b/test/test.py index 363019a..efd43e9 100644 --- a/test/test.py +++ b/test/test.py @@ -124,6 +124,51 @@ def test_bad_storage_function(self): config = Config() config.storage_func = "test" + def test_default_frame_filter(self): + """ + Tests the default frame filter. + """ + + config = Config() + frame_filter = config.generate_frame_filter() + + self.assertEqual( + frame_filter, + "type mgt subtype probe-req" + ) + + def test_frame_filter_with_mac_filtering(self): + """ + Tests the frame filter when some MAC addresses need to be filtered. + """ + + config = Config() + config.mac_filters = ["a4:77:33:9a:73:5c", "b0:05:94:5d:5a:4d"] + frame_filter = config.generate_frame_filter() + + self.assertEqual( + frame_filter, + "type mgt subtype probe-req" + + " and (ether src host a4:77:33:9a:73:5c" + + "|| ether src host b0:05:94:5d:5a:4d)" + ) + + def test_frame_filter_with_mac_exclusion(self): + """ + Tests the frame filter when some MAC addresses need to be excluded. + """ + + config = Config() + config.mac_exclusions = ["a4:77:33:9a:73:5c", "b0:05:94:5d:5a:4d"] + frame_filter = config.generate_frame_filter() + + self.assertEqual( + frame_filter, + "type mgt subtype probe-req" + + " and not (ether src host a4:77:33:9a:73:5c" + + "|| ether src host b0:05:94:5d:5a:4d)" + ) + class TestProbeRequestSniffer(unittest.TestCase): """ From 729e277f45643d8a7639b66ab3adbe55bac4958b Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sat, 17 Aug 2019 21:01:06 +0100 Subject: [PATCH 63/81] Add tests for the 'compile_essid_regex' method --- test/test.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/test/test.py b/test/test.py index efd43e9..3e392c7 100644 --- a/test/test.py +++ b/test/test.py @@ -169,6 +169,44 @@ def test_frame_filter_with_mac_exclusion(self): "|| ether src host b0:05:94:5d:5a:4d)" ) + def test_compile_essid_regex_with_an_empty_regex(self): + """ + Tests 'complile_essid_regex' with an empty regex. + """ + + config = Config() + compiled_regex = config.complile_essid_regex() + + self.assertEqual(compiled_regex, None) + + def test_compile_essid_regex_with_a_case_sensitive_regex(self): + """ + Tests 'complile_essid_regex' with a case-sensitive regex. + """ + + from re import compile as rcompile + + config = Config() + config.essid_regex = "Free Wi-Fi" + compiled_regex = config.complile_essid_regex() + + self.assertEqual(compiled_regex, rcompile(config.essid_regex)) + + def test_compile_essid_regex_with_a_case_insensitive_regex(self): + """ + Tests 'complile_essid_regex' with a case-insensitive regex. + """ + + from re import compile as rcompile, IGNORECASE + + config = Config() + config.essid_regex = "Free Wi-Fi" + config.ignore_case = True + compiled_regex = config.complile_essid_regex() + + self.assertEqual(compiled_regex, rcompile( + config.essid_regex, IGNORECASE)) + class TestProbeRequestSniffer(unittest.TestCase): """ From 3e25f90a15e9341537df6ed8fefc85452e1ac6b0 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sat, 17 Aug 2019 21:23:32 +0100 Subject: [PATCH 64/81] Add the 'config' module to the documentation --- docs/modules/config.rst | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/modules/config.rst diff --git a/docs/modules/config.rst b/docs/modules/config.rst new file mode 100644 index 0000000..0af391a --- /dev/null +++ b/docs/modules/config.rst @@ -0,0 +1,5 @@ +ProbeQuest Configuration +------------------------ + +.. automodule:: probequest.config + :members: From 7af62c537432d0d4bfa9eb7790844ef009b4075e Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sat, 17 Aug 2019 22:03:29 +0100 Subject: [PATCH 65/81] Update the minimum required version of Scapy Asynchronous sniffing is only available since Scapy 2.4.3. See https://scapy.readthedocs.io/en/latest/usage.html#asynchronous-sniffing. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d4386b4..26d604c 100755 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ 'argparse >= 1.4.0', 'faker_wifi_essid', 'netaddr >= 0.7.19', - 'scapy >= 2.4.0', + 'scapy >= 2.4.3', 'urwid>= 2.0.1', ], tests_require=[ From 305476c092e90b8041dd266b98cf31f74fb98406 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sat, 17 Aug 2019 23:00:24 +0100 Subject: [PATCH 66/81] Replace 'PacketSniffer' by 'AsyncSniffer' The 'AsyncSniffer' class has been introduced in Scapy since version 2.4.3. As a consequence, the 'PacketSniffer' class can be removed. --- probequest/probe_request_sniffer.py | 116 ++++------------------------ 1 file changed, 15 insertions(+), 101 deletions(-) diff --git a/probequest/probe_request_sniffer.py b/probequest/probe_request_sniffer.py index c966233..a7e1b14 100644 --- a/probequest/probe_request_sniffer.py +++ b/probequest/probe_request_sniffer.py @@ -6,10 +6,9 @@ from re import match from threading import Thread, Event -from scapy.config import conf as sconf -from scapy.data import ETH_P_ALL from scapy.layers.dot11 import RadioTap, Dot11, Dot11ProbeReq, Dot11Elt -from scapy.sendrecv import sniff +from scapy.sendrecv import AsyncSniffer +from scapy.error import Scapy_Exception from probequest.probe_request import ProbeRequest @@ -19,8 +18,6 @@ class ProbeRequestSniffer: Probe request sniffer class. """ - SNIFFER_STOP_TIMEOUT = 2.0 - def __init__(self, config): self.config = config @@ -28,8 +25,6 @@ def __init__(self, config): self.new_sniffer() self.new_parser() - self.sniffer_running = False - def start(self): """ Starts the probe request sniffer. @@ -37,11 +32,7 @@ def start(self): This method will start the sniffing and parsing threads. """ - try: - self.sniffer.start() - except RuntimeError: - self.new_sniffer() - self.sniffer.start() + self.sniffer.start() try: self.parser.start() @@ -49,13 +40,6 @@ def start(self): self.new_parser() self.parser.start() - exception = self.sniffer.get_exception() - - if exception is not None: - raise exception - - self.sniffer_running = True - def stop(self): """ Stops the probe request sniffer. @@ -64,13 +48,10 @@ def stop(self): """ try: - self.sniffer.join(timeout=ProbeRequestSniffer.SNIFFER_STOP_TIMEOUT) - except RuntimeError: - # stop() has been called before start(). + self.sniffer.stop() + except Scapy_Exception: + # The sniffer was not running. pass - finally: - if self.sniffer.is_alive(): - self.sniffer.socket.close() try: self.parser.join() @@ -78,8 +59,6 @@ def stop(self): # stop() has been called before start(). pass - self.sniffer_running = False - def new_sniffer(self): """ Creates a new sniffing thread. @@ -91,9 +70,11 @@ def new_sniffer(self): self.new_packets ) else: - self.sniffer = self.PacketSniffer( - self.config, - self.new_packets + self.sniffer = AsyncSniffer( + iface=self.config.interface, + filter=self.config.generate_frame_filter(), + store=False, + prn=self.new_packet ) def new_parser(self): @@ -112,81 +93,14 @@ def is_running(self): false otherwise. """ - return self.sniffer_running + return self.sniffer.running - class PacketSniffer(Thread): + def new_packet(self, packet): """ - A packet sniffing thread. + Adds a new packet to the queue to be processed. """ - def __init__(self, config, new_packets): - super().__init__() - - self.daemon = True - - self.config = config - self.new_packets = new_packets - - self.frame_filter = self.config.generate_frame_filter() - - self.socket = None - self.stop_sniffer = Event() - - self.exception = None - - if self.config.debug: - print("[!] Frame filter: " + self.frame_filter) - - def run(self): - try: - self.socket = sconf.L2listen( - type=ETH_P_ALL, - iface=self.config.interface, - filter=self.frame_filter - ) - - sniff( - opened_socket=self.socket, - store=False, - prn=self.new_packet, - stop_filter=self.should_stop_sniffer - ) - # pylint: disable=broad-except - except Exception as exception: - self.exception = exception - - if self.config.debug: - print("[!] Exception: " + str(exception)) - - def join(self, timeout=None): - """ - Stops the packet sniffer. - """ - - self.stop_sniffer.set() - super().join(timeout) - - def new_packet(self, packet): - """ - Adds a new packet to the queue to be processed. - """ - - self.new_packets.put(packet) - - def should_stop_sniffer(self, packet): - """ - Returns true if the sniffer should be stopped and false otherwise. - """ - - # pylint: disable=unused-argument - - return self.stop_sniffer.isSet() - - def get_exception(self): - """ - Returns the raised exception if any, otherwise returns none. - """ - return self.exception + self.new_packets.put(packet) class FakePacketSniffer(Thread): """ From df501e0f0a46e235107e90bf9fb21548d266f668 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sat, 17 Aug 2019 23:23:13 +0100 Subject: [PATCH 67/81] Add a sponsor button displayed on GitHub See https://help.github.com/en/articles/displaying-a-sponsor-button-in-your-repository. --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..5d517c8 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: https://blog.skyplabs.net/support/ From fcfd23350988179ded940ea75b2ba68b801feb3b Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Tue, 23 Jul 2019 09:43:20 +0100 Subject: [PATCH 68/81] Add a security policy displayed on GitHub See https://help.github.com/en/articles/adding-a-security-policy-to-your-repository. --- .github/SECURITY.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/SECURITY.md diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..bc3a93f --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,14 @@ +# Security Policy + +## Reporting a Vulnerability + +If you have found a security issue in ProbeQuest, please disclose it responsibly by emailing me at `skyper(at)skyplabs[dot]net`. My PGP public key can be found on my [Keybase profile][skyplabs-keybase]: + +[![PGP key fingerprint][pgp-key-badge]][pgp-key] + +To facilitate the encryption process, you can use [this online tool][keybase-encrypt]. You can also use it to verify my signatures. + + [keybase-encrypt]: https://keybase.io/encrypt#skyplabs + [pgp-key]: https://keybase.io/skyplabs/pgp_keys.asc + [pgp-key-badge]: https://img.shields.io/keybase/pgp/skyplabs.svg + [skyplabs-keybase]: https://keybase.io/skyplabs From ce733847921a68dad6d71009370b317b281950d5 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sun, 18 Aug 2019 23:21:42 +0100 Subject: [PATCH 69/81] Add the security policy to the documentation --- docs/index.rst | 1 + docs/security.rst | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 docs/security.rst diff --git a/docs/index.rst b/docs/index.rst index dc72e01..16c3a66 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -24,5 +24,6 @@ This project has been inspired by `this paper`_. mitigation modules development + security .. _this paper: https://brambonne.com/docs/bonne14sasquatch.pdf diff --git a/docs/security.rst b/docs/security.rst new file mode 100644 index 0000000..1e6e7ee --- /dev/null +++ b/docs/security.rst @@ -0,0 +1,17 @@ +=============== +Security Policy +=============== + +Reporting a Vulnerability +------------------------- + +If you have found a security issue in ProbeQuest, please disclose it responsibly by emailing me at `skyper(at)skyplabs[dot]net`. My PGP public key can be found on my `Keybase profile`_: + +.. image:: https://img.shields.io/keybase/pgp/skyplabs.svg + :target: https://keybase.io/skyplabs/pgp_keys.asc + :alt: PGP key fingerprint + +To facilitate the encryption process, you can use `this online tool`_. You can also use it to verify my signatures. + +.. _Keybase profile: https://keybase.io/skyplabs +.. _this online tool: https://keybase.io/encrypt#skyplabs From 5b7c7dc050b54ae4011c8415ce4402e146b9e2bf Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sun, 25 Aug 2019 19:12:12 +0100 Subject: [PATCH 70/81] Split classes across separate files --- probequest/fake_packet_sniffer.py | 73 ++++++++++ probequest/packet_sniffer.py | 51 +++++++ probequest/probe_request_parser.py | 90 ++++++++++++ probequest/probe_request_sniffer.py | 210 ++++------------------------ 4 files changed, 240 insertions(+), 184 deletions(-) create mode 100644 probequest/fake_packet_sniffer.py create mode 100644 probequest/packet_sniffer.py create mode 100644 probequest/probe_request_parser.py diff --git a/probequest/fake_packet_sniffer.py b/probequest/fake_packet_sniffer.py new file mode 100644 index 0000000..cdc6e71 --- /dev/null +++ b/probequest/fake_packet_sniffer.py @@ -0,0 +1,73 @@ +""" +Fake packet sniffer module. +""" + +from threading import Thread, Event + +from scapy.layers.dot11 import RadioTap, Dot11, Dot11ProbeReq, Dot11Elt + +from faker import Faker +from faker_wifi_essid import WifiESSID + + +class FakePacketSniffer(Thread): + """ + A fake packet sniffing thread. + + This thread returns fake Wi-Fi ESSIDs for development and test purposes. + """ + + def __init__(self, config, new_packets): + super().__init__() + + self.config = config + self.new_packets = new_packets + + self.stop_sniffer = Event() + + self.fake = Faker() + self.fake.add_provider(WifiESSID) + + def run(self): + from time import sleep + + while not self.stop_sniffer.isSet(): + sleep(1) + self.new_packet() + + def join(self, timeout=None): + """ + Stops the fake packet sniffer. + """ + + self.stop_sniffer.set() + super().join(timeout) + + def stop(self): + """ + Stops the fake packet sniffer. + + Alias for 'join()'. + """ + + self.join() + + def new_packet(self): + """ + Adds a new fake packet to the queue to be processed. + """ + + # pylint: disable=no-member + + fake_probe_req = RadioTap() \ + / Dot11( + addr1="ff:ff:ff:ff:ff:ff", + addr2=self.fake.mac_address(), + addr3=self.fake.mac_address() + ) \ + / Dot11ProbeReq() \ + / Dot11Elt( + info=self.fake.wifi_essid() + ) + + self.new_packets.put(fake_probe_req) diff --git a/probequest/packet_sniffer.py b/probequest/packet_sniffer.py new file mode 100644 index 0000000..3938460 --- /dev/null +++ b/probequest/packet_sniffer.py @@ -0,0 +1,51 @@ +""" +Packet sniffer module. +""" + +from scapy.sendrecv import AsyncSniffer + + +class PacketSniffer: + """ + Wrapper around the 'AsyncSniffer' class from the Scapy project. + """ + + def __init__(self, config, new_packets): + self.config = config + self.new_packets = new_packets + + self.sniffer = AsyncSniffer( + iface=self.config.interface, + filter=self.config.generate_frame_filter(), + store=False, + prn=self.new_packet + ) + + def start(self): + """ + Starts the packet sniffer. + """ + + self.sniffer.start() + + def stop(self): + """ + Stops the packet sniffer. + """ + + self.sniffer.stop() + + def is_running(self): + """ + Returns true if the sniffer is running, false otherwise. + """ + + return self.sniffer.running + + def new_packet(self, packet): + """ + Adds the packet given as parameter to the queue to be processed by the + parser. + """ + + self.new_packets.put(packet) diff --git a/probequest/probe_request_parser.py b/probequest/probe_request_parser.py new file mode 100644 index 0000000..1e9a4d6 --- /dev/null +++ b/probequest/probe_request_parser.py @@ -0,0 +1,90 @@ +""" +Probe request parser module. +""" + +from queue import Empty +from threading import Thread, Event +from re import match + +from scapy.layers.dot11 import RadioTap, Dot11ProbeReq + +from probequest.probe_request import ProbeRequest + + +class ProbeRequestParser(Thread): + """ + A Wi-Fi probe request parsing thread. + """ + + def __init__(self, config, new_packets): + super().__init__() + + self.config = config + self.new_packets = new_packets + + self.cregex = self.config.complile_essid_regex() + + self.stop_parser = Event() + + if self.config.debug: + print("[!] ESSID filters: " + str(self.config.essid_filters)) + print("[!] ESSID regex: " + str(self.config.essid_regex)) + print("[!] Ignore case: " + str(self.config.ignore_case)) + + def run(self): + # The parser continues to do its job even after the call of the + # join method if the queue is not empty. + while not self.stop_parser.isSet() or not self.new_packets.empty(): + try: + packet = self.new_packets.get(timeout=1) + probe_request = self.parse(packet) + + if probe_request is None: + continue + + if not probe_request.essid: + continue + + if (self.config.essid_filters is not None + and probe_request.essid + not in self.config.essid_filters): + continue + + if (self.cregex is not None + and not + match(self.cregex, probe_request.essid)): + continue + + self.config.display_func(probe_request) + self.config.storage_func(probe_request) + + self.new_packets.task_done() + except Empty: + pass + + def join(self, timeout=None): + """ + Stops the probe request parsing thread. + """ + + self.stop_parser.set() + super().join(timeout) + + @staticmethod + def parse(packet): + """ + Parses the raw packet and returns a probe request object. + """ + + try: + if packet.haslayer(Dot11ProbeReq): + timestamp = packet.getlayer(RadioTap).time + s_mac = packet.getlayer(RadioTap).addr2 + essid = packet.getlayer(Dot11ProbeReq).info.decode("utf-8") + + return ProbeRequest(timestamp, s_mac, essid) + + return None + except UnicodeDecodeError: + # The ESSID is not a valid UTF-8 string. + return None diff --git a/probequest/probe_request_sniffer.py b/probequest/probe_request_sniffer.py index a7e1b14..c6ba9ec 100644 --- a/probequest/probe_request_sniffer.py +++ b/probequest/probe_request_sniffer.py @@ -1,21 +1,23 @@ """ -A Wi-Fi probe request sniffer. +Wi-Fi probe request sniffer. """ -from queue import Queue, Empty -from re import match -from threading import Thread, Event +from queue import Queue -from scapy.layers.dot11 import RadioTap, Dot11, Dot11ProbeReq, Dot11Elt -from scapy.sendrecv import AsyncSniffer +from scapy.arch import get_if_hwaddr from scapy.error import Scapy_Exception -from probequest.probe_request import ProbeRequest +from probequest.packet_sniffer import PacketSniffer +from probequest.fake_packet_sniffer import FakePacketSniffer +from probequest.probe_request_parser import ProbeRequestParser class ProbeRequestSniffer: """ - Probe request sniffer class. + Wi-Fi probe request sniffer. + + It is composed of a packet sniffer and a packet parser, both running + in a thread and intercommunicating using a queue. """ def __init__(self, config): @@ -32,6 +34,12 @@ def start(self): This method will start the sniffing and parsing threads. """ + try: + # Test if the interface exists. + get_if_hwaddr(self.config.interface) + except Scapy_Exception: + pass + self.sniffer.start() try: @@ -62,19 +70,19 @@ def stop(self): def new_sniffer(self): """ Creates a new sniffing thread. + + If the '--fake' option is set, a fake packet sniffer will be used. """ if self.config.fake: - self.sniffer = self.FakePacketSniffer( + self.sniffer = FakePacketSniffer( self.config, self.new_packets ) else: - self.sniffer = AsyncSniffer( - iface=self.config.interface, - filter=self.config.generate_frame_filter(), - store=False, - prn=self.new_packet + self.sniffer = PacketSniffer( + self.config, + self.new_packets ) def new_parser(self): @@ -82,181 +90,15 @@ def new_parser(self): Creates a new parsing thread. """ - self.parser = self.ProbeRequestParser( + self.parser = ProbeRequestParser( self.config, self.new_packets ) def is_running(self): """ - Returns true if the probe request sniffer is running and - false otherwise. - """ - - return self.sniffer.running - - def new_packet(self, packet): - """ - Adds a new packet to the queue to be processed. - """ - - self.new_packets.put(packet) - - class FakePacketSniffer(Thread): - """ - A fake packet sniffing thread. - - This thread returns fake Wi-Fi ESSIDs for development - and test purposes. + Returns true if the probe request sniffer is running and false + otherwise. """ - def __init__(self, config, new_packets): - super().__init__() - - self.config = config - self.new_packets = new_packets - - self.stop_sniffer = Event() - self.exception = None - - from faker import Faker - from faker_wifi_essid import WifiESSID - - self.fake = Faker() - self.fake.add_provider(WifiESSID) - - def run(self): - from time import sleep - - try: - while not self.stop_sniffer.isSet(): - sleep(1) - self.new_packet() - # pylint: disable=broad-except - except Exception as exception: - self.exception = exception - - if self.config.debug: - print("[!] Exception: " + str(exception)) - - def join(self, timeout=None): - """ - Stops the fake packet sniffer. - """ - - self.stop_sniffer.set() - super().join(timeout) - - def new_packet(self): - """ - Adds a new fake packet to the queue to be processed. - """ - - # pylint: disable=no-member - - fake_probe_req = RadioTap() \ - / Dot11( - addr1="ff:ff:ff:ff:ff:ff", - addr2=self.fake.mac_address(), - addr3=self.fake.mac_address() - ) \ - / Dot11ProbeReq() \ - / Dot11Elt( - info=self.fake.wifi_essid() - ) - - self.new_packets.put(fake_probe_req) - - def should_stop_sniffer(self, packet): - """ - Returns true if the fake sniffer should be stopped - and false otherwise. - """ - - # pylint: disable=unused-argument - - return self.stop_sniffer.isSet() - - def get_exception(self): - """ - Returns the raised exception if any, otherwise returns none. - """ - return self.exception - - class ProbeRequestParser(Thread): - """ - A Wi-Fi probe request parsing thread. - """ - - def __init__(self, config, new_packets): - super().__init__() - - self.config = config - self.new_packets = new_packets - - self.cregex = self.config.complile_essid_regex() - - self.stop_parser = Event() - - if self.config.debug: - print("[!] ESSID filters: " + str(self.config.essid_filters)) - print("[!] ESSID regex: " + str(self.config.essid_regex)) - print("[!] Ignore case: " + str(self.config.ignore_case)) - - def run(self): - # The parser continues to do its job even after the call of the - # join method if the queue is not empty. - while not self.stop_parser.isSet() or not self.new_packets.empty(): - try: - packet = self.new_packets.get(timeout=1) - probe_request = self.parse(packet) - - if probe_request is None: - continue - - if not probe_request.essid: - continue - - if (self.config.essid_filters is not None - and probe_request.essid - not in self.config.essid_filters): - continue - - if (self.cregex is not None - and not - match(self.cregex, probe_request.essid)): - continue - - self.config.display_func(probe_request) - self.config.storage_func(probe_request) - - self.new_packets.task_done() - except Empty: - pass - - def join(self, timeout=None): - """ - Stops the probe request parsing thread. - """ - - self.stop_parser.set() - super().join(timeout) - - @staticmethod - def parse(packet): - """ - Parses the raw packet and returns a probe request object. - """ - - try: - if packet.haslayer(Dot11ProbeReq): - timestamp = packet.getlayer(RadioTap).time - s_mac = packet.getlayer(RadioTap).addr2 - essid = packet.getlayer(Dot11ProbeReq).info.decode("utf-8") - - return ProbeRequest(timestamp, s_mac, essid) - - return None - except UnicodeDecodeError: - # The ESSID is not a valid UTF-8 string. - return None + return self.sniffer.is_running() From 4444a3404750881f585321949dc6cc3de091c5d2 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sun, 25 Aug 2019 19:17:53 +0100 Subject: [PATCH 71/81] Fix some tests --- test/test.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/test/test.py b/test/test.py index 3e392c7..7473734 100644 --- a/test/test.py +++ b/test/test.py @@ -7,12 +7,16 @@ import unittest import pylint.lint + +from netaddr.core import AddrFormatError + from scapy.layers.dot11 import RadioTap, Dot11, Dot11ProbeReq, Dot11Elt from scapy.packet import fuzz -from netaddr.core import AddrFormatError + from probequest.config import Config from probequest.probe_request import ProbeRequest from probequest.probe_request_sniffer import ProbeRequestSniffer +from probequest.probe_request_parser import ProbeRequestParser class TestProbeRequest(unittest.TestCase): @@ -276,7 +280,7 @@ def test_no_probe_request_layer(self): addr3="dd:ee:ff:11:22:33" ) - ProbeRequestSniffer.ProbeRequestParser.parse(packet) + ProbeRequestParser.parse(packet) def test_empty_essid(self): """ @@ -297,7 +301,7 @@ def test_empty_essid(self): info="" ) - ProbeRequestSniffer.ProbeRequestParser.parse(packet) + ProbeRequestParser.parse(packet) def test_fuzz_packets(self): """ @@ -309,7 +313,7 @@ def test_fuzz_packets(self): for i in range(0, 1000): packet = RadioTap()/fuzz(Dot11()/Dot11ProbeReq()/Dot11Elt()) - ProbeRequestSniffer.ProbeRequestParser.parse(packet) + ProbeRequestParser.parse(packet) class TestLinter(unittest.TestCase): From 4991264882dc3f75675b0a3f3495f09f35ac7571 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Sun, 25 Aug 2019 23:40:27 +0100 Subject: [PATCH 72/81] Add tests for PacketSniffer and FakePacketSniffer --- test/test.py | 117 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 116 insertions(+), 1 deletion(-) diff --git a/test/test.py b/test/test.py index 7473734..ba5d3ac 100644 --- a/test/test.py +++ b/test/test.py @@ -5,17 +5,20 @@ # pylint: disable=import-error # pylint: disable=unused-variable +from queue import Queue import unittest import pylint.lint - from netaddr.core import AddrFormatError from scapy.layers.dot11 import RadioTap, Dot11, Dot11ProbeReq, Dot11Elt from scapy.packet import fuzz +from scapy.error import Scapy_Exception from probequest.config import Config from probequest.probe_request import ProbeRequest from probequest.probe_request_sniffer import ProbeRequestSniffer +from probequest.packet_sniffer import PacketSniffer +from probequest.fake_packet_sniffer import FakePacketSniffer from probequest.probe_request_parser import ProbeRequestParser @@ -260,6 +263,118 @@ def test_stop_before_start(self): sniffer.stop() +class TestPacketSniffer(unittest.TestCase): + """ + Unit tests for the 'PacketSniffer' class. + """ + + def test_new_packet(self): + """ + Tests the 'new_packet' method. + """ + + config = Config() + new_packets = Queue() + sniffer = PacketSniffer(config, new_packets) + + self.assertEqual(sniffer.new_packets.qsize(), 0) + + packet = RadioTap() \ + / Dot11( + addr1="ff:ff:ff:ff:ff:ff", + addr2="aa:bb:cc:11:22:33", + addr3="dd:ee:ff:11:22:33" + ) \ + / Dot11ProbeReq() \ + / Dot11Elt( + info="Test" + ) + + sniffer.new_packet(packet) + self.assertEqual(sniffer.new_packets.qsize(), 1) + + ProbeRequestParser.parse(sniffer.new_packets.get(timeout=1)) + + def test_stop_before_start(self): + """ + Creates a 'PacketSniffer' object and stops the sniffer before starting + it. + """ + + config = Config() + new_packets = Queue() + sniffer = PacketSniffer(config, new_packets) + + with self.assertRaises(Scapy_Exception): + sniffer.stop() + + def test_is_running_before_start(self): + """ + Creates a 'PacketSniffer' object and runs 'is_running' before starting + the sniffer. + """ + + config = Config() + new_packets = Queue() + sniffer = PacketSniffer(config, new_packets) + + self.assertFalse(sniffer.is_running()) + + +class TestFakePacketSniffer(unittest.TestCase): + """ + Unit tests for the 'FakePacketSniffer' class. + """ + + def test_new_packet(self): + """ + Tests the 'new_packet' method. + """ + + config = Config() + new_packets = Queue() + sniffer = FakePacketSniffer(config, new_packets) + + self.assertEqual(sniffer.new_packets.qsize(), 0) + + sniffer.new_packet() + self.assertEqual(sniffer.new_packets.qsize(), 1) + sniffer.new_packet() + self.assertEqual(sniffer.new_packets.qsize(), 2) + sniffer.new_packet() + self.assertEqual(sniffer.new_packets.qsize(), 3) + + ProbeRequestParser.parse(sniffer.new_packets.get(timeout=1)) + ProbeRequestParser.parse(sniffer.new_packets.get(timeout=1)) + ProbeRequestParser.parse(sniffer.new_packets.get(timeout=1)) + + def test_stop_before_start(self): + """ + Creates a 'FakePacketSniffer' object and stops the sniffer before + starting it. + """ + + config = Config() + new_packets = Queue() + sniffer = FakePacketSniffer(config, new_packets) + + with self.assertRaises(RuntimeError): + sniffer.stop() + + def test_stop_before_start_using_join(self): + """ + Creates a 'FakePacketSniffer' object and stops the sniffer before + starting it. + """ + + config = Config() + new_packets = Queue() + sniffer = FakePacketSniffer(config, new_packets) + + with self.assertRaises(RuntimeError): + sniffer.join() + + class TestProbeRequestParser(unittest.TestCase): """ Unit tests for the 'ProbeRequestParser' class. From 5aafea7ad3b1c8ff9af07ec3530337e919d27db4 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Mon, 26 Aug 2019 00:03:55 +0100 Subject: [PATCH 73/81] Fix a typo --- README.rst | 11 +++-------- bin/probequest | 4 ++-- docs/index.rst | 2 +- probequest/ui/raw.py | 2 +- 4 files changed, 7 insertions(+), 12 deletions(-) diff --git a/README.rst b/README.rst index 187e2b9..d8a3892 100644 --- a/README.rst +++ b/README.rst @@ -4,14 +4,9 @@ ProbeQuest |PyPI Package| |PyPI Downloads| |PyPI Python Versions| |Build Status Master Branch| |Build Status Develop Branch| |Code Coverage| |LGTM Grade| |LGTM Alerts| |Documentation Status| -Toolkit allowing to sniff and display the Wi-Fi probe requests passing near -your wireless interface. - -Probe requests are sent by a station to elicit information about access -points, in particular to determine if an access point is present or not -in the nearby environment. Some devices (mostly smartphones and tablets) -use these requests to determine if one of the networks they have -previously been connected to is in range, leaking personal information. +Toolkit allowing to sniff and display the Wi-Fi probe requests passing nearby your wireless interface. + +Probe requests are sent by a station to elicit information about access points, in particular to determine if an access point is present or not in the nearby environment. Some devices (mostly smartphones and tablets) use these requests to determine if one of the networks they have previously been connected to is in range, leaking personal information. Further details are discussed in `this paper `__. diff --git a/bin/probequest b/bin/probequest index 4133b79..a2ae32e 100755 --- a/bin/probequest +++ b/bin/probequest @@ -2,8 +2,8 @@ # -*- coding: utf-8 -*- """ -Toolkit allowing to sniff and display the Wi-Fi -probe requests passing near your wireless interface. +Toolkit allowing to sniff and display the Wi-Fi probe requests passing nearby +your wireless interface. """ # pylint: disable=no-name-in-module diff --git a/docs/index.rst b/docs/index.rst index 16c3a66..8f252ed 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,7 +6,7 @@ Welcome to ProbeQuest's documentation! ====================================== -ProbeQuest is a toolkit allowing to sniff and display the Wi-Fi probe requests passing near your wireless interface. +ProbeQuest is a toolkit allowing to sniff and display the Wi-Fi probe requests passing nearby your wireless interface. This project has been inspired by `this paper`_. diff --git a/probequest/ui/raw.py b/probequest/ui/raw.py index e28277f..d8ddf02 100644 --- a/probequest/ui/raw.py +++ b/probequest/ui/raw.py @@ -10,7 +10,7 @@ class RawProbeRequestViewer: """ - Displays the raw probe requests passing near the Wi-Fi interface. + Displays the raw probe requests passing nearby the Wi-Fi interface. """ def __init__(self, config): From 501919aa3447c7dcdbf37c99b9ceb1f729b98384 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Mon, 26 Aug 2019 00:12:31 +0100 Subject: [PATCH 74/81] Update the 'In the Media' list --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index d8a3892..1686b1a 100644 --- a/README.rst +++ b/README.rst @@ -35,6 +35,8 @@ ProbeQuest has appeared in the following media: - `KitPloit `__ - `Hakin9 Magazine, VOL.13, NO. 05, "Open Source Hacking Tools" `__ - `WonderHowTo `__ (including a `YouTube video `__) +- `ShellVoide `__ +- `Cyber Pi Projects `__ (`Worksheet `__) License ======= From 98af713b3b85b4f7784a21e5d00287b014d4613c Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Mon, 26 Aug 2019 00:24:37 +0100 Subject: [PATCH 75/81] Update the copyright year --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index ae36a3c..86724fa 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -24,7 +24,7 @@ # -- Project information ----------------------------------------------------- project = 'ProbeQuest' -copyright = '2018, Paul-Emmanuel Raoul' +copyright = '2019, Paul-Emmanuel Raoul' author = 'Paul-Emmanuel Raoul' # The short X.Y version From 6514eccd1c237578508484c8afd218a725e8152e Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Mon, 26 Aug 2019 00:26:01 +0100 Subject: [PATCH 76/81] Update list of modules in the documentation --- docs/modules/fake_packet_sniffer.rst | 5 +++++ docs/modules/packet_sniffer.rst | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 docs/modules/fake_packet_sniffer.rst create mode 100644 docs/modules/packet_sniffer.rst diff --git a/docs/modules/fake_packet_sniffer.rst b/docs/modules/fake_packet_sniffer.rst new file mode 100644 index 0000000..bbd8e07 --- /dev/null +++ b/docs/modules/fake_packet_sniffer.rst @@ -0,0 +1,5 @@ +Fake Packet Sniffer +------------------- + +.. automodule:: probequest.fake_packet_sniffer + :members: diff --git a/docs/modules/packet_sniffer.rst b/docs/modules/packet_sniffer.rst new file mode 100644 index 0000000..9e885cc --- /dev/null +++ b/docs/modules/packet_sniffer.rst @@ -0,0 +1,5 @@ +Packet Sniffer +-------------- + +.. automodule:: probequest.packet_sniffer + :members: From 4eab9ff080e42429f4f3314aee3d8a19e68b3c55 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Mon, 26 Aug 2019 00:27:21 +0100 Subject: [PATCH 77/81] Update the 'Releasing a new version' instructions --- docs/development.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/development.rst b/docs/development.rst index 9a3ceb2..d7a9a2c 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -23,6 +23,8 @@ Below are the different steps to do before releasing a new version: - Update the package's metadata (description, classifiers, etc) in `setup.py` if needed - Update `README.rst` if needed - Update the documentation if needed and make sure it compiles well (`cd ./docs && make html`) +- Update the copyright year in `docs/conf.py` if needed +- Add the corresponding release note to `CHANGELOG.md` After having pushed the new release: From 31bb873359df3329c01c79b01dccc540ae1ecffb Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Wed, 8 May 2019 08:22:30 +0100 Subject: [PATCH 78/81] Add 'bdist_wheel' to Travis CI deployment See https://pythonwheels.com/ and https://docs.travis-ci.com/user/deployment/pypi/#uploading-different-distributions. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index aed614c..def3cb5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ jobs: python: 3.6 deploy: provider: pypi - distributions: sdist + distributions: "sdist bdist_wheel" user: skyplabs password: secure: AAdbZ/WxjoCcGV9BAvphrxNp48D3n6yt1FEWZwA2V1BK2SnVV6YwQX/r7ryat2uoYrTONnGf/P5QgXc2wThl5mzPiRMaaONOIkyrmi2ZCD86yHYy2QQPR1gO1QOYyIsx545FRB6PaGXBC99Hw30vh3HPQaCKoQ8/PY+u3QbVn47BSBPuIdBi/FLl81uYk0ZE1457TxHkYrSDheRA3JVs72oD+izeoS+JJ+1YX+2zreukk9xuNfuwtcXPAn5B8LH8yYkFp1dd2pRsQzYMB4aH3rz7QEzSZ0mLBr3/J2bFCldmT8NToRijIjZNk04ik8XlGQ7xmpYG9rYIAkWwSBYSsZbfeLBxoxxIBcUy9xVxveZSaNl7TcRchCsyVO8leuL9aLmKz3wuKhWCRxJQSUDLlo83LYoBaTifqstUO85gC8IxR/Y/yqkW8wfSfVgVaDi9ET3/7UgSZEQJFEqfiYGdnD6/IkAy2tUCRO5xNsXsOyVJ5A0CsDTTtvlEfGxf1UtQyt0BmRSGYLMTnDBStW1Oua2QfVcTKIJOdfEOyL/VWnn/f0RCJQiUkRo9OFmSywoFjgSC9Arejwsff5smEd5i/jTKk6rOoHgIMnAGxn+75BjF3vQ7usAJeEOlLzHB5puc5dKeCpn5rwxOHha1lfmr6kDs1ec5XhcgQKgvulYfjVQ= From 9c39e1e5a7fea32acf83883e0729659fc03b8551 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Wed, 8 May 2019 08:24:57 +0100 Subject: [PATCH 79/81] Use Python 3.7 for deployment --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index def3cb5..f60bf94 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,7 @@ jobs: - python: 3.7 dist: xenial - stage: PyPI release - python: 3.6 + python: 3.7 deploy: provider: pypi distributions: "sdist bdist_wheel" From 4eb547fcda4e76ebb5c52f713289be64885ed26a Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Mon, 26 Aug 2019 00:54:00 +0100 Subject: [PATCH 80/81] Bump version number to 0.7.2 --- probequest/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/probequest/version.py b/probequest/version.py index 8a37e2d..a815ff8 100644 --- a/probequest/version.py +++ b/probequest/version.py @@ -2,4 +2,4 @@ Actual version number of ProbeQuest. """ -VERSION = "0.7.1" +VERSION = "0.7.2" From 0f220df140db589da2cd5d08a173d87cb21ffcf1 Mon Sep 17 00:00:00 2001 From: Paul-Emmanuel Raoul Date: Mon, 26 Aug 2019 01:11:33 +0100 Subject: [PATCH 81/81] Update the changelog --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index df4c531..1b24ed9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +## v0.7.2 - Aug 26, 2019 + +### Improvements + +* Use the new [Scapy built-in asynchronous sniffer](https://scapy.readthedocs.io/en/latest/usage.html#asynchronous-sniffing) +* Introduce the new `Config` object containing the configuration of ProbeQuest + +### Fixes + +* Fix all linting and style errors + +### Misc. + +* Drop support for Python 3.3 + ## v0.7.1 - Mar 6, 2019 ### Fixes