Browse Source

Merge branch 'unittest_collection' into unittest_master

Jens Keim 6 years ago
parent
commit
d1e6e9735e

+ 3 - 14
code/Attack/FTPWinaXeExploit.py

@@ -1,4 +1,5 @@
 import logging
+import ID2TLib.Utility
 
 from random import randint
 from lea import Lea
@@ -8,7 +9,7 @@ from Attack import BaseAttack
 from Attack.AttackParameters import Parameter as Param
 from Attack.AttackParameters import ParameterTypes
 from ID2TLib.Utility import update_timestamp, generate_source_port_from_platform, get_rnd_x86_nop, forbidden_chars,\
-    get_rnd_bytes, get_bytes_from_file
+    get_rnd_bytes , check_payload_len
 
 logging.getLogger("scapy.runtime").setLevel(logging.ERROR)
 # noinspection PyPep8
@@ -110,18 +111,6 @@ class FTPWinaXeExploit(BaseAttack.BaseAttack):
 
             return mss_value, ttl_value, win_value
 
-        def check_payload_len(payload_len: int, limit: int):
-            """
-            Checks if the len of the payload exceeds a given limit
-            :param payload_len: The length of the payload
-            :param limit: The limit of the length of the payload which is allowed
-            """
-
-            if payload_len > limit:
-                print("\nCustom payload too long: ", payload_len, " bytes. Should be a maximum of ", limit, " bytes.")
-                exit(1)
-
-
         pps = self.get_param_value(Param.PACKETS_PER_SECOND)
 
         # Timestamp
@@ -205,7 +194,7 @@ class FTPWinaXeExploit(BaseAttack.BaseAttack):
             if custom_payload_file == '':
                 payload = get_rnd_bytes(custom_payload_limit, forbidden_chars)
             else:
-                payload = get_bytes_from_file(custom_payload_file)
+                payload = ID2TLib.Utility.get_bytes_from_file(custom_payload_file)
                 check_payload_len(len(payload), custom_payload_limit)
                 payload += get_rnd_x86_nop(custom_payload_limit - len(payload), False, forbidden_chars)
         else:

+ 2 - 5
code/Attack/PortscanAttack.py

@@ -5,6 +5,7 @@ from random import shuffle, randint, choice
 from lea import Lea
 from scapy.layers.inet import IP, Ether, TCP
 
+from definitions import ROOT_DIR
 from Attack import BaseAttack
 from Attack.AttackParameters import Parameter as Param
 from Attack.AttackParameters import ParameterTypes
@@ -89,7 +90,7 @@ class PortscanAttack(BaseAttack.BaseAttack):
         :return: Ports numbers to be used as default destination ports or default open ports in the port scan.
         """
         ports_dst = []
-        spamreader = csv.reader(open('resources/nmap-services-tcp.csv', 'rt'), delimiter=',')
+        spamreader = csv.reader(open(ROOT_DIR + '/../resources/nmap-services-tcp.csv', 'rt'), delimiter=',')
         for count in range(ports_num):
             # escape first row (header)
             next(spamreader)
@@ -109,10 +110,6 @@ class PortscanAttack(BaseAttack.BaseAttack):
         return port_dst_shuffled
 
     def generate_attack_pcap(self):
-
-
-
-
         mac_source = self.get_param_value(Param.MAC_SOURCE)
         mac_destination = self.get_param_value(Param.MAC_DESTINATION)
         pps = self.get_param_value(Param.PACKETS_PER_SECOND)

+ 4 - 3
code/Attack/SMBScanAttack.py

@@ -10,8 +10,9 @@ from Attack import BaseAttack
 from Attack.AttackParameters import Parameter as Param
 from Attack.AttackParameters import ParameterTypes
 from ID2TLib.SMB2 import *
-from ID2TLib.Utility import update_timestamp, get_interval_pps, get_rnd_os, get_ip_range,\
+from ID2TLib.Utility import update_timestamp, get_interval_pps, get_ip_range,\
     generate_source_port_from_platform, get_filetime_format
+import ID2TLib.Utility
 from ID2TLib.SMBLib import smb_port, smb_versions, smb_dialects, get_smb_version, get_smb_platform_data,\
     invalid_smb_version
 
@@ -89,9 +90,9 @@ class SMBScanAttack(BaseAttack.BaseAttack):
 
         rnd_ip_count = self.statistics.get_ip_address_count()/2
         self.add_param_value(Param.HOSTING_IP, self.statistics.get_random_ip_address(rnd_ip_count))
-        self.host_os = get_rnd_os()
+        self.host_os = ID2TLib.Utility.get_rnd_os()
         self.add_param_value(Param.HOSTING_VERSION, get_smb_version(platform=self.host_os))
-        self.add_param_value(Param.SOURCE_PLATFORM, get_rnd_os())
+        self.add_param_value(Param.SOURCE_PLATFORM, ID2TLib.Utility.get_rnd_os())
         self.add_param_value(Param.PROTOCOL_VERSION, "1")
         self.add_param_value(Param.IP_DESTINATION_END, "0.0.0.0")
 

+ 2 - 1
code/Attack/SalityBotnet.py

@@ -4,6 +4,7 @@ from random import randint
 from scapy.utils import RawPcapReader
 from scapy.layers.inet import Ether
 
+from definitions import ROOT_DIR
 from Attack import BaseAttack
 from Attack.AttackParameters import Parameter as Param
 from Attack.AttackParameters import ParameterTypes
@@ -14,7 +15,7 @@ logging.getLogger("scapy.runtime").setLevel(logging.ERROR)
 
 
 class SalityBotnet(BaseAttack.BaseAttack):
-    template_attack_pcap_path = "resources/sality_botnet.pcap"
+    template_attack_pcap_path = ROOT_DIR + "/../resources/sality_botnet.pcap"
 
     def __init__(self):
         """

+ 2 - 1
code/ID2TLib/StatsDatabase.py

@@ -169,7 +169,7 @@ class StatsDatabase:
         """
         # Definition of SQL queries associated to named queries
         named_queries = {
-            "most_used.ipaddress": "SELECT ipAddress FROM ip_statistics WHERE (pktsSent+pktsReceived) == (SELECT MAX(pktsSent+pktsReceived) from ip_statistics) LIMIT 1",
+            "most_used.ipaddress": "SELECT ipAddress FROM ip_statistics WHERE (pktsSent+pktsReceived) == (SELECT MAX(pktsSent+pktsReceived) from ip_statistics) ORDER BY ipAddress ASC LIMIT 1",
             "most_used.macaddress": "SELECT * FROM (SELECT macAddress, COUNT(*) as occ from ip_mac GROUP BY macAddress ORDER BY occ DESC) WHERE occ=(SELECT COUNT(*) as occ from ip_mac GROUP BY macAddress ORDER BY occ DESC LIMIT 1)",
             "most_used.portnumber": "SELECT portNumber, COUNT(portNumber) as cntPort FROM ip_ports GROUP BY portNumber HAVING cntPort=(SELECT MAX(cntPort) from (SELECT portNumber, COUNT(portNumber) as cntPort FROM ip_ports GROUP BY portNumber))",
             "most_used.protocolname": "SELECT protocolName, COUNT(protocolCount) as countProtocols FROM ip_protocols GROUP BY protocolName HAVING countProtocols=(SELECT COUNT(protocolCount) as cnt FROM ip_protocols GROUP BY protocolName ORDER BY cnt DESC LIMIT 1)",
@@ -177,6 +177,7 @@ class StatsDatabase:
             "most_used.mssvalue": "SELECT mssValue FROM tcp_mss GROUP BY mssValue ORDER BY SUM(mssCount) DESC LIMIT 1",
             "most_used.winsize": "SELECT winSize FROM tcp_win GROUP BY winSize ORDER BY SUM(winCount) DESC LIMIT 1",
             "most_used.ipclass": "SELECT ipClass FROM ip_statistics GROUP BY ipClass ORDER BY COUNT(*) DESC LIMIT 1",
+            #FIXME ORDER BY ASC ? check queries for os dependency!!
             "least_used.ipaddress": "SELECT ipAddress FROM ip_statistics WHERE (pktsSent+pktsReceived) == (SELECT MIN(pktsSent+pktsReceived) from ip_statistics)",
             "least_used.macaddress": "SELECT * FROM (SELECT macAddress, COUNT(*) as occ from ip_mac GROUP BY macAddress ORDER BY occ ASC) WHERE occ=(SELECT COUNT(*) as occ from ip_mac GROUP BY macAddress ORDER BY occ ASC LIMIT 1)",
             "least_used.portnumber": "SELECT portNumber, COUNT(portNumber) as cntPort FROM ip_ports GROUP BY portNumber HAVING cntPort=(SELECT MIN(cntPort) from (SELECT portNumber, COUNT(portNumber) as cntPort FROM ip_ports GROUP BY portNumber))",

+ 11 - 0
code/ID2TLib/Utility.py

@@ -220,6 +220,17 @@ def get_rnd_bytes(count=1, ignore=None):
     return result
 
 
+def check_payload_len(payload_len: int, limit: int):
+    """
+    Checks if the len of the payload exceeds a given limit
+    :param payload_len: The length of the payload
+    :param limit: The limit of the length of the payload which is allowed
+    """
+
+    if payload_len > limit:
+        print("\nCustom payload too long: ", payload_len, " bytes. Should be a maximum of ", limit, " bytes.")
+        exit(1)
+
 def get_bytes_from_file(filepath):
     """
     Converts the content of a file into its byte representation

+ 1 - 0
code/Test/Lib.py

@@ -7,6 +7,7 @@ from definitions import ROOT_DIR
 test_resource_dir = ROOT_DIR + "/../resources/test"
 test_pcap = ROOT_DIR + "/../resources/test/test.pcap"
 test_pcap_ips = ["192.168.189.143", "192.168.189.1"]
+test_pcap_empty = []
 
 """
 helper functions for generic_test

+ 49 - 5
code/Test/test_FTPWinaXeExploit.py

@@ -2,14 +2,20 @@ import unittest
 import unittest.mock as mock
 
 from Test.GenericTest import GenericTest
-from Test.Lib import get_bytes, get_x86_nop
+from Test.Lib import *
 
-sha_one_attacker_ftp = '941947ccc42ea10e724d2a20626882130d62fc5dbbe007095a90f67a943ab3bf'
+sha_ftp_basic = '941947ccc42ea10e724d2a20626882130d62fc5dbbe007095a90f67a943ab3bf'
+sha_ftp_most_used_ip = '941947ccc42ea10e724d2a20626882130d62fc5dbbe007095a90f67a943ab3bf'
+sha_ftp_mac = 'c2e83e62bb8a15402725faef47a53c6e5afa3dd82a17435d48000058976160cb'
+sha_ftp_random_ip_src = '41ae677b553064428905682f6a17447850cc4c1b617c337e046ee6e50f51217b'
+sha_not_empty_custom_payload_empty_file = '369d59174de5f01787ea623673f320e8342ddd6be9761edb607bf635f44a3749'
+sha_empty_custom_payload_not_empty_file = '9d3ec2451b05acc72b99b40309b714bc015b6d12b5477f6490cd2f9ba8f1ffa8'
+sha_valid_ip = '941947ccc42ea10e724d2a20626882130d62fc5dbbe007095a90f67a943ab3bf'
 
 """
 Name                             Stmts   Miss  Cover   Missing
 --------------------------------------------------------------------------------------------
-Attack/FTPWinaXeExploit.py         141     14    90%   55, 66, 71, 121-122, 148-150, 208-214
+Attack/FTPWinaXeExploit.py         141     14    99%    67
 """
 
 
@@ -17,8 +23,46 @@ class UnitTestFTPWinaXeExploit(GenericTest):
 
     @mock.patch('ID2TLib.Utility.get_rnd_bytes', side_effect=get_bytes)
     @mock.patch('ID2TLib.Utility.get_rnd_x86_nop', side_effect=get_x86_nop)
-    def test_one_attacker_ftp(self, mock_get_rnd_bytes, mock_get_rnd_x86_nop):
-        self.generic_test([['FTPWinaXeExploit']], sha_one_attacker_ftp)
+    def test_ftp_basic(self, mock_get_rnd_x86_nop, mock_get_rnd_bytes):
+        self.generic_test([['FTPWinaXeExploit']], sha_ftp_basic)
+
+    @mock.patch('ID2TLib.Utility.get_rnd_bytes', side_effect=get_bytes)
+    @mock.patch('ID2TLib.Utility.get_rnd_x86_nop', side_effect=get_x86_nop)
+    @mock.patch('ID2TLib.Statistics.Statistics.get_most_used_ip_address')
+    def test_ftp_most_used_ips(self,mock_most_used_ip_address, mock_get_rnd_x86_nop, mock_get_rnd_bytes):
+        mock_most_used_ip_address.return_value = test_pcap_ips
+        self.generic_test([['FTPWinaXeExploit']], sha_ftp_most_used_ip)
+
+    @mock.patch('ID2TLib.Utility.get_rnd_bytes', side_effect=get_bytes)
+    @mock.patch('ID2TLib.Utility.get_rnd_x86_nop', side_effect=get_x86_nop)
+    @mock.patch('ID2TLib.Statistics.Statistics.get_mac_address')
+    def test_ftp_mac(self, mock_mac_address, mock_get_rnd_x86_nop, mock_get_rnd_bytes):
+        mock_mac_address.return_value = test_pcap_empty
+        self.generic_test([['FTPWinaXeExploit']], sha_ftp_mac)
+
+    @mock.patch('ID2TLib.Utility.get_rnd_bytes', side_effect=get_bytes)
+    @mock.patch('ID2TLib.Utility.get_rnd_x86_nop', side_effect=get_x86_nop)
+    def test_ftp_random_ip_src(self, mock_get_rnd_x86_nop, mock_get_rnd_bytes):
+        self.generic_test([['FTPWinaXeExploit', 'ip.src.shuffle=1']], sha_ftp_random_ip_src)
+
+    @mock.patch('ID2TLib.Utility.get_rnd_bytes', side_effect=get_bytes)
+    @mock.patch('ID2TLib.Utility.get_rnd_x86_nop', side_effect=get_x86_nop)
+    def test_ftp_not_empty_custom_payload_empty_file(self, mock_get_rnd_x86_nop, mock_get_rnd_bytes):
+        self.generic_test([['FTPWinaXeExploit', 'custom.payload=1']], sha_not_empty_custom_payload_empty_file)
+
+    @mock.patch('ID2TLib.Utility.get_rnd_bytes', side_effect=get_bytes)
+    @mock.patch('ID2TLib.Utility.get_rnd_x86_nop', side_effect=get_x86_nop)
+    @mock.patch('ID2TLib.Utility.check_payload_len')
+    @mock.patch('ID2TLib.Utility.get_bytes_from_file', return_value=b'AAAAA')
+    def test_ftp_empty_custom_payload_not_empty_file(self, mock_bytes_from_file, mock_payload_len, mock_get_rnd_x86_nop,
+                                                     mock_get_rnd_bytes):
+        self.generic_test([['FTPWinaXeExploit', 'custom.payload.file=1']], sha_empty_custom_payload_not_empty_file)
+
+    @mock.patch('ID2TLib.Utility.get_rnd_bytes', side_effect=get_bytes)
+    @mock.patch('ID2TLib.Utility.get_rnd_x86_nop', side_effect=get_x86_nop)
+    @mock.patch('Attack.BaseAttack.BaseAttack.is_valid_ip_address', return_values=[False, True])
+    def test_ftp_invalid_ip(self, mock_valid_ip_check, mock_get_rnd_x86_nop, mock_get_rnd_bytes):
+        self.generic_test([['FTPWinaXeExploit']], sha_valid_ip)
 
 
 if __name__ == '__main__':

+ 58 - 0
code/Test/test_PortscanAttack.py

@@ -0,0 +1,58 @@
+import unittest
+import unittest.mock as mock
+
+from Test.GenericTest import GenericTest
+from Test.Lib import *
+
+sha_portscan_default = 'dd28509dcc55a722c57d6b462741581d7b48024cddb8b8c89fe138661fac2b07'
+sha_portscan_reverse_ports = '04f5cdab7ade15bde00f0fcf42278508da7104ac76eab543d9c4b1cbab4f67c7'
+sha_portscan_shuffle_dst_ports = 'a6ef8a714da52d7608a84f50fe9dc71a3714e8b78a62be07c4e3d5509fa03d95'
+sha_portscan_shuffle_src_ports = '218382e8feabea3c5a35834c9962034cdff6e0c90fafee899883a9a54bb38371'
+sha_portscan_mss_value_zero = 'c3847e0a3a5abf886506dc5402fbc9a3096db2fd1df16d276d6c60c6b4b4ca5f'
+sha_portscan_ttl_value_zero = 'c3847e0a3a5abf886506dc5402fbc9a3096db2fd1df16d276d6c60c6b4b4ca5f'
+sha_portscan_win_value_zero = 'c3847e0a3a5abf886506dc5402fbc9a3096db2fd1df16d276d6c60c6b4b4ca5f'
+sha_portscan_ip_src_random = 'c3939f30a40fa6e2164cc91dc4a7e823ca409492d44508e3edfc9d24748af0e5'
+sha_portscan_most_used_ip_in_list = 'c3939f30a40fa6e2164cc91dc4a7e823ca409492d44508e3edfc9d24748af0e5'
+"""
+CURRENT COVERAGE
+Name                             Stmts   Miss  Cover   Missing (lines)
+---------------------------------------------------------------------------
+Attack/PortscanAttack.py           146      6    96%   73, 108-109, 158, 211, 238
+"""
+# TODO: get 100% coverage
+
+
+class UnitTestPortscanAttack(GenericTest):
+
+    def test_portscan_default(self):
+        self.generic_test([['PortscanAttack']], sha_portscan_default)
+
+    def test_portscan_reverse_ports(self):
+        self.generic_test([['PortscanAttack', 'port.dst.order-desc=1']], sha_portscan_reverse_ports)
+
+    def test_portscan_shuffle_dst_ports(self):
+        self.generic_test([['PortscanAttack', 'port.dst.shuffle=1']], sha_portscan_shuffle_dst_ports)
+
+    def test_portscan_shuffle_src_ports(self):
+        self.generic_test([['PortscanAttack', 'port.src.shuffle=1']], sha_portscan_shuffle_src_ports)
+
+    @mock.patch('ID2TLib.Statistics.Statistics.get_mss_distribution', return_value='')
+    def test_portscan_mss_length_zero(self, mock_mss_dis):
+        self.generic_test([['PortscanAttack']], sha_portscan_mss_value_zero)
+
+    @mock.patch('ID2TLib.Statistics.Statistics.get_ttl_distribution', return_value='')
+    def test_portscan_ttl_length_zero(self, mock_ttl_dis):
+        self.generic_test([['PortscanAttack']], sha_portscan_ttl_value_zero)
+
+    @mock.patch('ID2TLib.Statistics.Statistics.get_win_distribution', return_value='')
+    def test_portscan_win_length_zero(self, mock_win_dis):
+        self.generic_test([['PortscanAttack']], sha_portscan_win_value_zero)
+
+    @mock.patch('ID2TLib.Statistics.Statistics.get_most_used_ip_address')
+    def test_portscan_most_used_ips(self, mock_most_used_ip_address):
+        mock_most_used_ip_address.return_value = test_pcap_ips
+        self.generic_test([['PortscanAttack']], sha_portscan_most_used_ip_in_list)
+
+
+if __name__ == '__main__':
+    unittest.main()

+ 83 - 0
code/Test/test_SMBScan.py

@@ -0,0 +1,83 @@
+import unittest
+import unittest.mock as mock
+
+from Test.GenericTest import GenericTest
+
+# FIXME: create new hashes if new test.pcap is used
+sha_default = '6650602f7ac54b0032504bba24c05a99ed09dcf094a0b6ea3172b95d805807f4'
+sha_one_victim_linux = '9da7ca3fe34f7a4f8d93d67b297afd198f0a4eb628171fbd25e15dc3d9bc97b5'
+sha_victim_range_winxp_hosting = '5d58804c68e1d94e12150283e4013c678f22fb819eb2207100f0341dacba88ec'
+sha_multiple_victims_macos = 'd39cd3dbdb85304d2629884118df070a78f9689ab7b3fd3a046c3706c3cd0f7e'
+sha_port_shuffle = 'd32d557c65c01f46ec3de769dc15d223ec13234016898f5ec7aaab1b9549801a'
+sha_dest_mac_only = 'af0140c0a2883927d429da82409f6bc091c9743e984111bda7c27d2bf99992ab'
+sha_ip_src_shuffle = 'c6ed7baf850ccc3f53551e9a93c0a397629eb064abae7deeafb05d84b2633b05'
+sha_smb2 = '8407a3316ba8dfb4ae610cedeeddfe4a7c0be1d420c2cad1c2750a213893618e'
+
+
+"""
+CURRENT COVERAGE
+Name                             Stmts   Miss  Cover   Missing (lines)
+---------------------------------------------------------------------------
+Attack/SMBScanAttack.py            239      9    96%   65, 73-74, 82, 193, 210-211, 284-285
+"""
+# TODO: get 100% coverage
+
+
+class UnitTestSMBScan(GenericTest):
+
+    def test_default(self):
+        with mock.patch("ID2TLib.Utility.get_rnd_os", return_value="win7"):
+            self.generic_test([['SMBScanAttack']], sha_default)
+
+    def test_one_victim_linux(self):
+        with mock.patch("ID2TLib.Utility.get_rnd_os", return_value="linux"):
+            self.generic_test([['SMBScanAttack', 'ip.src=192.168.178.1', 'ip.dst=192.168.178.10']],
+                              sha_one_victim_linux)
+
+    def test_victim_range_winxp_hosting(self):
+        with mock.patch("ID2TLib.Utility.get_rnd_os", return_value="winxp"):
+            self.generic_test([['SMBScanAttack', 'ip.src=192.168.178.1', 'ip.dst=192.168.178.5',
+                                'ip.dst.end=192.168.178.10', 'hosting.ip=192.168.178.5']],
+                              sha_victim_range_winxp_hosting)
+
+    def test_multiple_victims_macos(self):
+        with mock.patch("ID2TLib.Utility.get_rnd_os", return_value="macos"):
+            self.generic_test([['SMBScanAttack', 'ip.src=192.168.178.1',
+                                'ip.dst=192.168.178.10,192.168.178.15,192.168.178.20',
+                                'hosting.ip=192.168.178.15,192.168.178.20']], sha_multiple_victims_macos)
+
+    def test_invalid_smb_version(self):
+        with self.assertRaises(SystemExit):
+            self.generic_test([['SMBScanAttack', 'protocol.version=42']], 'somehash')
+
+    def test_invalid_smb_platform(self):
+        with self.assertRaises(SystemExit):
+            self.generic_test([['SMBScanAttack', 'hosting.version=1337']], 'somehash')
+
+    def test_port_shuffle(self):
+        with mock.patch("ID2TLib.Utility.get_rnd_os", return_value="win7"):
+            self.generic_test([['SMBScanAttack', 'ip.src=192.168.178.1', 'ip.dst=192.168.178.5',
+                                'ip.dst.end=192.168.178.10', 'hosting.ip=192.168.178.5', 'port.src.shuffle=false']],
+                              sha_port_shuffle)
+
+    def test_dest_mac_only(self):
+        with mock.patch("ID2TLib.Utility.get_rnd_os", return_value="win7"):
+            self.generic_test([['SMBScanAttack', 'ip.src=192.168.178.1',
+                                'mac.dst=00:0C:29:9C:70:64']], sha_dest_mac_only)
+
+    def test_src_ip_shuffle(self):
+        with mock.patch("ID2TLib.Utility.get_rnd_os", return_value="win7"):
+            self.generic_test([['SMBScanAttack', 'ip.src=192.168.178.1', 'ip.dst=192.168.178.5',
+                                'ip.dst.end=192.168.178.10', 'hosting.ip=192.168.178.5', 'ip.src.shuffle=True']],
+                              sha_ip_src_shuffle)
+
+    def test_smb2(self):
+
+        with mock.patch("ID2TLib.Utility.get_rnd_os", return_value="linux"):
+            self.generic_test([['SMBScanAttack', 'ip.src=192.168.178.1', 'ip.dst=192.168.178.5',
+                                'ip.dst.end=192.168.178.10', 'hosting.ip=192.168.178.5', 'protocol.version=2.1',
+                                'hosting.version=2.1']], sha_smb2)
+
+
+if __name__ == '__main__':
+    unittest.main()

+ 30 - 0
code/Test/test_SalityBotnet.py

@@ -0,0 +1,30 @@
+import unittest
+import unittest.mock as mock
+
+from Test.GenericTest import GenericTest
+from Test.Lib import test_pcap_ips
+
+sha_botnet_basic = 'bbe75f917933a9f7727d99137920a70a5f720cabc773da9e24acfd6cba45a87a'
+sha_botnet_most_used_ip_in_list ='8583e2563d2756347449aec4b1c7cf7bfc7c0a96db4885627dcf0afc9e59feff'
+
+"""
+CURRENT COVERAGE
+Name                             Stmts   Miss  Cover   Missing (lines)
+---------------------------------------------------------------------------
+Attack/SalityBotnet.py           77      0    100%
+"""
+
+
+class UnitTestSalityBotnet(GenericTest):
+
+    def test_botnet_basic(self):
+        self.generic_test([['SalityBotnet']], sha_botnet_basic)
+
+    @mock.patch('ID2TLib.Statistics.Statistics.get_most_used_ip_address')
+    def test_botnet_most_used_ips(self, mock_most_used_ip_address):
+        mock_most_used_ip_address.return_value = test_pcap_ips
+        self.generic_test([['SalityBotnet']], sha_botnet_most_used_ip_in_list)
+
+
+if __name__ == '__main__':
+    unittest.main()