Files
Mark-VIII/tests/test_recon.py
T
2026-08-11 20:24:24 -07:00

600 lines
25 KiB
Python

import os
import shutil
import sqlite3
import sys
import tempfile
import unittest
from unittest import mock
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'payload', 'user', 'remote_access', 'pager-webui'))
import server
def setUpModule():
__import__('importlib').reload(server)
SCHEMA = '''
CREATE TABLE scan(id INTEGER PRIMARY KEY AUTOINCREMENT, uuid TEXT, time INT, name TEXT);
CREATE TABLE wifi_device(hash INT PRIMARY KEY, scan INT, mac TEXT, time INT, signal INT, freq INT, packets INT);
CREATE TABLE ssid(hash INT PRIMARY KEY, wifi_device INT, scan INT, type INT, bssid TEXT, ssid BLOB, hidden INT, time INT, signal INT, freq INT, channel INT, encryption INT);
CREATE TABLE handshake(hash INT PRIMARY KEY, scan INT, stahash INT, aphash INT, time INT, beacon BLOB, hs1 BLOB, hs2 BLOB, hs3 BLOB, hs4 BLOB);
CREATE TABLE hostap_basic(id INT PRIMARY KEY, scan INT, time INT, type TEXT, identity TEXT, password TEXT, verified INT NOT NULL DEFAULT 0);
CREATE TABLE hostap_chalresp(id INT PRIMARY KEY, scan INT, time INT, type TEXT, username TEXT, challenge BLOB, response BLOB, verified INT NOT NULL DEFAULT 0);
CREATE TABLE hostap_client(id INT PRIMARY KEY, scan INT, hash INT, mac TEXT, ssid BLOB, connected_time INT, disconnected_time INT);
CREATE TABLE hostap_handshake(id INT PRIMARY KEY, scan INT, time INT, type INT, mic BLOB, apmac BLOB, stamac BLOB, ssid BLOB, nonce BLOB, eapol BLOB);
'''
def make_db():
fd, db = tempfile.mkstemp(suffix='.db')
os.close(fd)
conn = sqlite3.connect(db)
conn.executescript(SCHEMA)
conn.execute("INSERT INTO scan (uuid, time, name) VALUES ('u1', 1786466531, 'pager')")
conn.execute("INSERT INTO scan (uuid, time, name) VALUES ('u2', 1786466848, 'pager')")
conn.execute("INSERT INTO wifi_device (hash, scan, mac, time, signal, freq, packets) VALUES (1, 1, 'AE77C0EB3141', 1786466531, -71, 2412, 5)")
conn.execute("INSERT INTO wifi_device (hash, scan, mac, time, signal, freq, packets) VALUES (2, 1, 'C89E43648080', 1786466532, -76, 5745, 9)")
conn.execute("INSERT INTO ssid (hash, wifi_device, scan, type, bssid, ssid, hidden, time, signal, freq, channel, encryption) "
"VALUES (10, 2, 1, 8, 'C89E43648080', X'416E646572736F6E2D35', 0, 1786466532, -76, 5745, 149, 0x400400108)")
conn.execute("INSERT INTO ssid (hash, wifi_device, scan, type, bssid, ssid, hidden, time, signal, freq, channel, encryption) "
"VALUES (11, 2, 1, 8, '506F9A010000', X'', 1, 1786466532, -64, 5745, 149, 0)")
conn.execute("INSERT INTO ssid (hash, wifi_device, scan, type, bssid, ssid, hidden, time, signal, freq, channel, encryption) "
"VALUES (12, 1, 1, 4, NULL, X'5A6E6574', NULL, 1786466531, -40, 2412, NULL, NULL)")
conn.execute("INSERT INTO handshake (hash, scan, stahash, aphash, time) VALUES (20, 1, 1, 2, 1786466600)")
conn.execute("INSERT INTO hostap_basic (scan, time, type, identity, password, verified) VALUES (1, 1786466601, 'WPA', 'bob', '', 0)")
conn.commit()
conn.close()
return db
class DecodersTest(unittest.TestCase):
def test_fmt_mac_colon_form(self):
self.assertEqual(server.fmt_mac('AE77C0EB3141'), 'AE:77:C0:EB:31:41')
def test_fmt_mac_noop(self):
self.assertEqual(server.fmt_mac('AE:77:C0:EB:31:41'), 'AE:77:C0:EB:31:41')
self.assertEqual(server.fmt_mac(''), '--')
self.assertEqual(server.fmt_mac(None), '--')
def test_norm_mac_12hex(self):
self.assertEqual(server._norm_mac('C89E43648080'), 'C8:9E:43:64:80:80')
def test_decode_ssid_bytes_and_str(self):
self.assertEqual(server.decode_ssid(b'Znet'), 'Znet')
self.assertEqual(server.decode_ssid('Znet'), 'Znet')
self.assertEqual(server.decode_ssid(None), '')
self.assertEqual(server.decode_ssid(b'\xff\xfeZnet'), '\ufffd\ufffdZnet')
def test_decode_ssid_cli_escapes(self):
# Device sqlite3 CLI -json emits \xNN escapes for non-UTF8 BLOBs.
self.assertEqual(server.decode_ssid('casaalicia\\x00.\\xde_;'), 'casaalicia\x00.\ufffd_;')
def test_decode_encryption_cases(self):
self.assertEqual(server.decode_encryption(0), 'Open')
self.assertEqual(server.decode_encryption(None), 'Open')
self.assertEqual(server.decode_encryption(2), 'WEP')
self.assertEqual(server.decode_encryption(0x04), 'WPA')
self.assertEqual(server.decode_encryption(0x08), 'WPA2')
self.assertEqual(server.decode_encryption(0x04 | 0x08), 'WPA2 WPA')
self.assertEqual(server.decode_encryption(0x400400108), 'WPA3 WPA2')
self.assertEqual(server.decode_encryption(0x20050004C), 'WPA2 WPA')
class ReconDataTest(unittest.TestCase):
def setUp(self):
self.db = make_db()
server.RECON_DB = self.db
def tearDown(self):
os.unlink(self.db)
def test_scans_list_uses_time_and_counts(self):
data = server.recon_scans_data()
self.assertEqual(len(data['scans']), 2)
newest = data['scans'][0]
self.assertEqual(newest['id'], 2)
self.assertEqual(newest['time'], 1786466848)
self.assertEqual(newest['name'], 'pager')
old = data['scans'][1]
self.assertEqual(old['devices'], 2)
self.assertEqual(old['aps'], 2)
self.assertEqual(old['handshakes'], 1)
self.assertNotIn('uuid', old)
def test_scan_detail_decodes_aps(self):
data = server.recon_scan_data(1)
self.assertEqual(data['scan']['id'], 1)
self.assertEqual(data['scan']['time'], 1786466531)
self.assertEqual(len(data['aps']), 2)
aps = {a['bssid']: a for a in data['aps']}
a = aps['C8:9E:43:64:80:80']
self.assertEqual(a['ssid'], 'Anderson-5')
self.assertEqual(a['channel'], 149)
self.assertEqual(a['signal'], -76)
self.assertEqual(a['encryption'], 'WPA3 WPA2')
self.assertFalse(a['hidden'])
hidden = aps['50:6F:9A:01:00:00']
self.assertTrue(hidden['hidden'])
self.assertEqual(hidden['encryption'], 'Open')
def test_scan_detail_clients_exclude_ap_macs(self):
data = server.recon_scan_data(1)
macs = [c['mac'] for c in data['clients']]
self.assertEqual(macs, ['AE:77:C0:EB:31:41'])
def test_scan_detail_handshakes_resolve_macs(self):
data = server.recon_scan_data(1)
self.assertEqual(len(data['handshakes']), 1)
hs = data['handshakes'][0]
self.assertEqual(hs['ap'], 'C8:9E:43:64:80:80')
self.assertEqual(hs['client'], 'AE:77:C0:EB:31:41')
self.assertEqual(hs['time'], 1786466600)
def test_scan_detail_missing_returns_none(self):
self.assertIsNone(server.recon_scan_data(999))
class FakeSock:
def __init__(self, resp=b''):
self.resp = resp
self.sent = b''
self.connected = None
def settimeout(self, t):
pass
def connect(self, addr):
self.connected = addr
def sendall(self, b):
self.sent += b
def recv(self, n):
chunk, self.resp = self.resp[:n], self.resp[n:]
return chunk
def close(self):
pass
class DaemonSockTest(unittest.TestCase):
def test_socket_call_posts_json_to_sock(self):
server.DAEMON_SOCK = '/tmp/api.sock'
fake = FakeSock(b'HTTP/1.1 200 OK\r\nContent-Length: 17\r\n\r\n{"success":true}')
with mock.patch.object(server.socket, 'socket', lambda *a, **k: fake):
status, data = server.daemon_sock_call('POST', '/api/pineap/recon/new', body={'x': 1})
self.assertEqual(status, 200)
self.assertEqual(data, {'success': True})
self.assertEqual(fake.connected, '/tmp/api.sock')
self.assertTrue(fake.sent.startswith(b'POST /api/pineap/recon/new HTTP/1.1'))
self.assertTrue(b'Content-Length: 8' in fake.sent)
self.assertTrue(fake.sent.endswith(b'{"x": 1}'))
def test_socket_call_connect_error(self):
def boom(*a, **k):
raise OSError('nope')
with mock.patch.object(server.socket, 'socket', boom):
status, data = server.daemon_sock_call('POST', '/api/pineap/recon/new')
self.assertEqual(status, 0)
self.assertIsNone(data)
def test_start_stop_handlers_call_socket(self):
calls = []
server.daemon_sock_call = lambda m, p, body=None: calls.append((m, p, body)) or (200, {'success': True})
server.h_recon_start(type('C', (), {'args': ()})())
server.h_recon_stop(type('C', (), {'args': ()})())
self.assertEqual(calls[0], ('POST', '/api/pineap/log/recon/start', {}))
self.assertEqual(calls[1], ('POST', '/api/pineap/log/recon/stop', {}))
def test_start_forwards_scan_time(self):
calls = []
server.daemon_sock_call = lambda m, p, body=None: calls.append((m, p, body)) or (200, {'success': True})
ctx = type('C', (), {'args': (), 'body': {'scan_time': 60}})()
status, data = server.h_recon_start(ctx)
self.assertEqual(status, 200)
self.assertEqual(calls[0], ('POST', '/api/pineap/log/recon/start', {'scan_time': 60}))
def test_start_defaults_empty_body(self):
calls = []
server.daemon_sock_call = lambda m, p, body=None: calls.append((m, p, body)) or (200, {'success': True})
server.h_recon_start(type('C', (), {'args': ()})())
self.assertEqual(calls[0], ('POST', '/api/pineap/log/recon/start', {}))
class ReconScanStateTest(unittest.TestCase):
"""The daemon ignores scan_time and scans continuously until 'stop'. The webui
must track the requested duration itself so timed scans actually end and the
toggle can reflect real scan state."""
def setUp(self):
self.db = make_db()
server.RECON_DB = self.db
server._recon_scan_state = {'active': False, 'started': 0, 'duration': 0}
def tearDown(self):
os.unlink(self.db)
def _start(self, scan_time=None, fail=False):
body = {}
if scan_time is not None:
body['scan_time'] = scan_time
server.daemon_sock_call = (lambda m, p, body=None: (502, {})) if fail \
else (lambda m, p, body=None: (200, {'success': True}))
return server.h_recon_start(type('C', (), {'args': (), 'body': body})())
def _stop(self, fail=False):
server.daemon_sock_call = (lambda m, p, body=None: (502, {})) if fail \
else (lambda m, p, body=None: (200, {'success': True}))
return server.h_recon_stop(type('C', (), {'args': ()})())
def _status(self):
server.daemon_sock_call = lambda m, p, body=None: (200, {'success': True})
return server.h_recon_status(type('C', (), {'args': ()})())
def test_timed_start_marks_scanning_with_remaining(self):
server.time.time = lambda: 1000.0
status, data = self._start(scan_time=30)
self.assertEqual(status, 200)
status, data = self._status()
self.assertTrue(data['scanning'])
self.assertEqual(data['scan_remaining'], 30)
def test_timed_scan_expires_when_duration_elapses(self):
base = [1000.0]
server.time.time = lambda: base[0]
self._start(scan_time=30)
base[0] = 1031.0
status, data = self._status()
self.assertFalse(data['scanning'])
self.assertEqual(data['scan_remaining'], 0)
def test_continuous_scan_has_no_remaining(self):
server.time.time = lambda: 1000.0
self._start(scan_time=0)
status, data = self._status()
self.assertTrue(data['scanning'])
self.assertIsNone(data['scan_remaining'])
def test_default_start_is_continuous(self):
server.time.time = lambda: 1000.0
self._start()
status, data = self._status()
self.assertTrue(data['scanning'])
self.assertIsNone(data['scan_remaining'])
def test_stop_clears_scanning(self):
server.time.time = lambda: 1000.0
self._start(scan_time=30)
self._stop()
status, data = self._status()
self.assertFalse(data['scanning'])
def test_start_failure_does_not_mark_scanning(self):
server.time.time = lambda: 1000.0
status, data = self._start(scan_time=30, fail=True)
self.assertEqual(status, 502)
status, data = self._status()
self.assertFalse(data['scanning'])
def test_watchdog_stops_expired_timed_scan(self):
calls = []
server.time.time = lambda: 1000.0
self._start(scan_time=10)
server.time.time = lambda: 1012.0
server.daemon_sock_call = lambda m, p, body=None: calls.append((m, p)) or (200, {'success': True})
server._recon_watchdog_tick()
self.assertEqual(calls, [('POST', '/api/pineap/log/recon/stop')])
self.assertFalse(server._recon_scan_state['active'])
def test_watchdog_leaves_active_scan_alone(self):
server.time.time = lambda: 1000.0
self._start(scan_time=30)
server.time.time = lambda: 1010.0
calls = []
server.daemon_sock_call = lambda m, p, body=None: calls.append((m, p)) or (200, {'success': True})
server._recon_watchdog_tick()
self.assertEqual(calls, [])
self.assertTrue(server._recon_scan_state['active'])
class ReconExtrasTest(unittest.TestCase):
def setUp(self):
self.db = make_db()
server.RECON_DB = self.db
def tearDown(self):
os.unlink(self.db)
def test_status_reports_last_scan_and_active(self):
server.time.time = lambda: 1786466532 + 100
status, data = server.h_recon_status(type('C', (), {'args': ()})())
self.assertEqual(status, 200)
self.assertEqual(data['last_scan'], 1786466848)
self.assertEqual(data['last_activity'], 1786466532)
self.assertTrue(data['active'])
server.time.time = lambda: 1786466532 + 1000
status, data = server.h_recon_status(type('C', (), {'args': ()})())
self.assertFalse(data['active'])
def test_delete_cascades(self):
server.recon_delete_scan(1)
rows = server._db_rows(self.db, 'SELECT count(*) AS c FROM wifi_device')
self.assertEqual(rows[0]['c'], 0)
rows = server._db_rows(self.db, 'SELECT count(*) AS c FROM ssid')
self.assertEqual(rows[0]['c'], 0)
rows = server._db_rows(self.db, 'SELECT count(*) AS c FROM handshake')
self.assertEqual(rows[0]['c'], 0)
rows = server._db_rows(self.db, 'SELECT count(*) AS c FROM scan')
self.assertEqual(rows[0]['c'], 1)
def test_delete_handler_404_for_missing(self):
status, data = server.h_recon_delete(type('C', (), {'args': ('999',)})())
self.assertEqual(status, 404)
def test_events_lists_db_rows(self):
status, data = server.h_recon_events(type('C', (), {'args': ()})())
self.assertEqual(status, 200)
kinds = [e['type'] for e in data['events']]
self.assertIn('auth attempt', kinds)
self.assertEqual(data['events'][0]['time'], 1786466601)
class ReconExamineTest(unittest.TestCase):
def test_examine_bssid_calls_hak5(self):
calls = []
server.hak5 = lambda *args, **kw: calls.append(args) or ''
ctx = type('C', (), {'args': (), 'body': {'bssid': 'AA:BB:CC:DD:EE:FF'}})()
status, data = server.h_recon_examine(ctx)
self.assertEqual(status, 200)
self.assertEqual(calls[0], ('PINEAPPLE_EXAMINE_BSSID', 'AA:BB:CC:DD:EE:FF'))
def test_examine_channel_calls_hak5(self):
calls = []
server.hak5 = lambda *args, **kw: calls.append(args) or ''
ctx = type('C', (), {'args': (), 'body': {'channel': 6}})()
status, data = server.h_recon_examine(ctx)
self.assertEqual(status, 200)
self.assertEqual(calls[0], ('PINEAPPLE_EXAMINE_CHANNEL', '6'))
def test_examine_requires_target(self):
server.hak5 = lambda *args, **kw: ''
ctx = type('C', (), {'args': (), 'body': {}})()
status, data = server.h_recon_examine(ctx)
self.assertEqual(status, 400)
class HandshakeFileTest(unittest.TestCase):
def test_file_download_decodes_name(self):
import shutil
d = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, d)
server.LOOT_HS_DIR = d
name = '00 11_scan.cap'
with open(os.path.join(d, name), 'wb') as f:
f.write(b'PCAPDATA')
status, payload = server.h_handshake_file(type('C', (), {'args': ('00%2011_scan.cap',)}))
self.assertEqual(status, 200)
self.assertEqual(payload.data, b'PCAPDATA')
self.assertEqual(payload.filename, name)
@unittest.skipIf(os.name == 'nt', 'colons are not valid in filenames on Windows')
def test_file_download_decodes_mac_colon_name(self):
import shutil
d = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, d)
server.LOOT_HS_DIR = d
name = '00:11:22:33:44:55_scan.cap'
with open(os.path.join(d, name), 'wb') as f:
f.write(b'PCAPDATA')
encoded = '00%3A11%3A22%3A33%3A44%3A55_scan.cap'
status, payload = server.h_handshake_file(type('C', (), {'args': (encoded,)}))
self.assertEqual(status, 200)
self.assertEqual(payload.data, b'PCAPDATA')
self.assertEqual(payload.filename, name)
def test_file_download_404_missing(self):
import shutil
d = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, d)
server.LOOT_HS_DIR = d
status, payload = server.h_handshake_file(type('C', (), {'args': ('nope.cap',)}))
self.assertEqual(status, 404)
class CliFallbackTest(unittest.TestCase):
def setUp(self):
self.db = make_db()
server.RECON_DB = self.db
self._orig_sqlite3 = server.sqlite3
self._orig_device_run = server.device_run
server.sqlite3 = None
server.device_run = lambda args, timeout=20: (0, self._run(args), '')
def _run(self, args):
# emulate `sqlite3 -json <db> <sql>` over an in-memory copy
import sqlite3 as s3
import subprocess
conn = s3.connect(self.db)
try:
cur = conn.execute(args[-1])
conn.commit()
if cur.description:
import json
rows = cur.fetchall()
cols = [d[0] for d in cur.description]
objs = []
for row in rows:
obj = {}
for c, v in zip(cols, row):
if isinstance(v, bytes):
try:
v = v.decode('utf-8')
except Exception:
v = ''.join('\\x%02x' % b for b in v)
obj[c] = v
objs.append(obj)
return json.dumps(objs)
return ''
finally:
conn.close()
def tearDown(self):
server.sqlite3 = self._orig_sqlite3
server.device_run = self._orig_device_run
os.unlink(self.db)
def test_delete_cascade_via_cli(self):
server.recon_delete_scan(1)
rows = server._db_rows(self.db, 'SELECT count(*) AS c FROM scan')
self.assertEqual(rows[0]['c'], 1)
def test_cli_ssid_escapes_decoded(self):
# emulate the CLI emitting \xNN escapes for a non-UTF8 blob
server.device_run = lambda args, timeout=20: (0, '[{"ssid": "casaalicia\\\\x00.\\\\xde_"}]', '')
server.RECON_DB = '/nonexistent.db'
self.assertEqual(server.decode_ssid('casaalicia\\x00.\\xde_'), 'casaalicia\x00.\ufffd_')
def make_hs_db():
db = make_db()
conn = sqlite3.connect(db)
conn.execute(
"INSERT INTO handshake (hash, scan, stahash, aphash, time, beacon, hs1, hs2, hs3, hs4) "
"VALUES (21, 1, 1, 2, 1786466650, X'424541434F4E', X'01', X'02', X'03', X'04')")
conn.commit()
conn.close()
return db
class ParseHsFilenameTest(unittest.TestCase):
def test_parse_full_pcap(self):
p = server.parse_hs_filename('1786466650_C8:9E:43:64:80:80_AE:77:C0:EB:31:41_handshake.pcap')
self.assertEqual(p['ts'], 1786466650)
self.assertEqual(p['ap'], 'C8:9E:43:64:80:80')
self.assertEqual(p['client'], 'AE:77:C0:EB:31:41')
self.assertEqual(p['kind'], 'full')
self.assertEqual(p['ext'], 'pcap')
def test_parse_partial_and_incomplete(self):
p = server.parse_hs_filename('1_C8-9E-43-64-80-80_AE-77-C0-EB-31-41_handshake_partial.22000')
self.assertEqual(p['kind'], 'partial')
self.assertEqual(p['ext'], '22000')
p = server.parse_hs_filename('1_C8:9E:43:64:80:80_AE:77:C0:EB:31:41_handshake_incomplete.pcap')
self.assertEqual(p['kind'], 'incomplete')
self.assertEqual(p['ext'], 'pcap')
def test_parse_dash_macs_and_no_ts(self):
p = server.parse_hs_filename('C8-9E-43-64-80-80_AE-77-C0-EB-31-41_handshake.pcap')
self.assertIsNone(p['ts'])
self.assertEqual(p['ap'], 'C8:9E:43:64:80:80')
self.assertEqual(p['client'], 'AE:77:C0:EB:31:41')
def test_parse_unrecognized(self):
self.assertIsNone(server.parse_hs_filename('random.cap'))
self.assertIsNone(server.parse_hs_filename('notes.txt'))
self.assertIsNone(server.parse_hs_filename(''))
self.assertIsNone(server.parse_hs_filename('123_mac1_mac2_handshake'))
class HandshakesDataTest(unittest.TestCase):
def setUp(self):
self.db = make_hs_db()
self.dir = tempfile.mkdtemp()
self.addCleanup(server.__dict__.update, {'RECON_DB': server.RECON_DB, 'LOOT_HS_DIR': server.LOOT_HS_DIR})
server.RECON_DB = self.db
server.LOOT_HS_DIR = self.dir
def tearDown(self):
shutil.rmtree(self.dir)
os.unlink(self.db)
def _write(self, name, ts):
path = os.path.join(self.dir, name)
open(path, 'w').close()
os.utime(path, (ts, ts))
def test_empty_dir_skips_db(self):
with mock.patch.object(server, '_db_rows', side_effect=AssertionError('db should not be touched')):
data = server.handshakes_data()
self.assertEqual(data, {'files': [], 'handshakes': []})
def test_correlation_composes_full_record(self):
self._write('1786466650_C8-9E-43-64-80-80_AE-77-C0-EB-31-41_handshake.pcap', 1786466650)
data = server.handshakes_data()
self.assertEqual(len(data['files']), 1)
hs = data['handshakes'][0]
self.assertEqual(hs['mac'], 'C8:9E:43:64:80:80')
self.assertEqual(hs['client'], 'AE:77:C0:EB:31:41')
self.assertEqual(hs['source'], 'Recon')
self.assertEqual(hs['type'], 'full')
self.assertEqual(hs['extension'], 'pcap')
self.assertEqual(hs['timestamp'], 1786466650)
self.assertTrue(hs['in_db'])
self.assertEqual(hs['part_mask'], 15)
self.assertTrue(hs['beacon'])
self.assertEqual(hs['name'], '1786466650_C8-9E-43-64-80-80_AE-77-C0-EB-31-41_handshake.pcap')
self.assertTrue(hs['file_exists'])
self.assertEqual(hs['location'], os.path.join(server.LOOT_HS_DIR, hs['name']))
def test_file_not_in_db_has_question_mark_fields(self):
self._write('1786467000_AA-BB-CC-DD-EE-FF_00-11-22-33-44-55_handshake.pcap', 1786467000)
hs = server.handshakes_data()['handshakes'][0]
self.assertFalse(hs['in_db'])
self.assertEqual(hs['part_mask'], 0)
self.assertFalse(hs['beacon'])
self.assertEqual(hs['timestamp'], 1786467000)
def test_unparseable_file_still_listed_with_placeholders(self):
self._write('random.cap', 1786467005)
hs = server.handshakes_data()['handshakes'][0]
self.assertEqual(hs['mac'], '--')
self.assertEqual(hs['client'], '--')
self.assertFalse(hs['in_db'])
self.assertEqual(hs['extension'], 'cap')
class HandshakeRoutesTest(unittest.TestCase):
def setUp(self):
self.dir = tempfile.mkdtemp()
self.addCleanup(server.__dict__.update, {'LOOT_HS_DIR': server.LOOT_HS_DIR})
server.LOOT_HS_DIR = self.dir
def tearDown(self):
shutil.rmtree(self.dir)
def _write(self, name, data=b'data'):
with open(os.path.join(self.dir, name), 'wb') as f:
f.write(data)
def test_location_returns_loot_dir(self):
status, data = server.h_handshakes_location(type('C', (), {'args': ()})())
self.assertEqual(status, 200)
self.assertEqual(data['location'], self.dir)
def test_location_route_precedes_file_download(self):
h, args = server.ROUTER.dispatch('GET', '/api/pineap/handshakes/location')
self.assertIs(h, server.h_handshakes_location)
def test_delete_all_removes_files(self):
self._write('1_C8-9E-43-64-80-80_AE-77-C0-EB-31-41_handshake.pcap')
self._write('2_C8-9E-43-64-80-80_AE-77-C0-EB-31-41_handshake.22000')
status, data = server.h_handshakes_delete_all(type('C', (), {'args': ()})())
self.assertEqual(status, 200)
self.assertEqual(data['files'], [])
self.assertEqual(data['handshakes'], [])
self.assertEqual(os.listdir(self.dir), [])
def test_delete_all_empty_dir_is_ok(self):
status, data = server.h_handshakes_delete_all(type('C', (), {'args': ()})())
self.assertEqual(status, 200)
self.assertEqual(data['files'], [])
def test_delete_all_skips_dotfiles(self):
self._write('.hidden')
self._write('1_C8-9E-43-64-80-80_AE-77-C0-EB-31-41_handshake.pcap')
status, data = server.h_handshakes_delete_all(type('C', (), {'args': ()})())
self.assertEqual(status, 200)
self.assertEqual(data['files'], [])
self.assertEqual(data['handshakes'], [])
self.assertEqual(os.listdir(self.dir), ['.hidden'])