1
0
Fork 0
mirror of https://github.com/dbarzin/pandora-box.git synced 2025-07-18 21:09:41 +02:00
pandora-box/pandora-box.py

1034 lines
29 KiB
Python
Raw Normal View History

2022-06-11 18:25:10 +02:00
#!/usr/bin/python3
2023-02-14 20:41:49 +01:00
#
2023-02-12 18:10:04 +01:00
# This file is part of the Pandora-box distribution.
# https://github.com/dbarzin/pandora-box
2022-07-13 15:26:00 +02:00
# Copyright (c) 2022 Didier Barzin.
2023-02-12 18:10:04 +01:00
#
2023-02-14 20:41:49 +01:00
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
2022-07-13 15:26:00 +02:00
# the Free Software Foundation, version 3.
#
2023-02-14 20:41:49 +01:00
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
2022-07-13 15:26:00 +02:00
# General Public License for more details.
#
2023-02-14 20:41:49 +01:00
# You should have received a copy of the GNU General Public License
2022-07-13 15:26:00 +02:00
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
2022-06-11 21:50:01 +02:00
2023-02-15 13:22:59 +01:00
"""The Pandora-Box Module."""
2022-06-16 13:24:36 +02:00
import configparser
2023-02-14 20:57:56 +01:00
import curses
2023-03-04 17:51:50 +01:00
import datetime
2025-04-18 13:06:32 +02:00
import glob
2023-03-04 17:51:50 +01:00
import logging
import os
import queue
import shutil
import socket
2023-03-05 12:58:15 +01:00
import sys
import time
import threading
2025-04-17 11:00:04 +02:00
import subprocess
2025-04-17 15:20:06 +02:00
from pathlib import Path
2023-02-14 20:57:56 +01:00
import pypandora
2025-04-17 15:20:06 +02:00
import psutil
import pyudev
2022-06-11 20:06:30 +02:00
2023-03-04 17:51:50 +01:00
# -----------------------------------------------------------
# Threading variables
# -----------------------------------------------------------
threads = []
2025-04-17 15:20:06 +02:00
EXIT_FLAG = False
queueLock = threading.Lock()
workQueue = None
2023-03-04 17:51:50 +01:00
# -----------------------------------------------------------
# Config variables
# -----------------------------------------------------------
2025-04-17 15:20:06 +02:00
isFakeScan = None
hasUSBAutoMount = None
2023-03-04 17:51:50 +01:00
pandora_root_url = None
2025-04-17 15:20:06 +02:00
hasQuarantine = None
2023-03-04 17:51:50 +01:00
quarantine_folder = None
has_curses = None
2023-03-04 18:52:40 +01:00
maxThreads = None
2023-03-08 08:26:25 +01:00
boxname = socket.gethostname()
2025-04-22 11:43:00 +02:00
maxFileSize = None
2023-03-04 17:51:50 +01:00
# -----------------------------------------------------------
# Curses
# -----------------------------------------------------------
screen = None
status_win = None
progress_win = None
title_win = None
log_win = None
# Pandora logo
logo = None
# -----------------------------------------------------------
# Interval box variables
# -----------------------------------------------------------
device = None
mount_point = None
infected_files = None
# -----------------------------------------------------------
2025-04-17 15:20:06 +02:00
2024-01-19 07:30:25 +01:00
class scanThread(threading.Thread):
2023-03-04 17:51:50 +01:00
"""Scanning thread"""
2024-01-19 07:30:25 +01:00
2023-03-04 21:12:26 +01:00
def __init__(self):
2023-03-04 17:51:50 +01:00
threading.Thread.__init__(self)
2023-03-04 21:12:26 +01:00
self.pandora = pypandora.PyPandora(root_url=pandora_root_url)
2023-03-04 17:51:50 +01:00
def run(self):
2025-04-17 15:20:06 +02:00
while not EXIT_FLAG:
queueLock.acquire()
if not workQueue.empty():
file = workQueue.get()
queueLock.release()
2023-03-04 21:12:26 +01:00
self.scan(file)
2023-02-15 13:22:59 +01:00
else:
2025-04-17 15:20:06 +02:00
queueLock.release()
2025-04-16 16:56:07 +02:00
time.sleep(1)
2023-03-04 17:51:50 +01:00
2023-03-04 21:12:26 +01:00
def scan(self, file):
2025-04-22 11:43:00 +02:00
global infected_files, scanned, file_count, f_used, maxFileSize
2025-04-17 20:00:40 +02:00
logging.info(f'{"Start scan."}')
2023-03-04 17:51:50 +01:00
try:
# get file information
file_name = os.path.basename(file)
file_size = os.path.getsize(file)
# log the scan has started
2025-04-16 16:56:07 +02:00
logging.info(
2025-04-17 15:20:06 +02:00
f"Scan {file_name} "
f"[{human_readable_size(file_size)}] "
f"Thread-{id} "
)
2023-03-04 19:28:00 +01:00
2024-01-19 07:30:25 +01:00
start_time = time.time()
2025-04-17 15:20:06 +02:00
if isFakeScan:
2023-03-04 17:51:50 +01:00
status = "SKIPPED"
2025-04-17 20:00:40 +02:00
logging.info(f'{"Fake scan - skipped."}')
2023-02-25 11:19:37 +01:00
else:
2023-03-04 17:51:50 +01:00
# do not scan files bigger than 1G
2025-04-22 11:43:00 +02:00
if file_size > maxFileSize :
2023-03-04 17:51:50 +01:00
status = "TOO BIG"
2025-04-17 20:00:40 +02:00
logging.info(f'{"File too big."}')
2023-03-03 21:15:32 +01:00
else:
2025-04-17 15:20:06 +02:00
worker = self.pandora.submit_from_disk(
file, seed_expire=6000)
if ("taskId" not in worker) or ("seed" not in worker):
logging.error(f"task_status={worker}")
2025-04-07 15:29:22 +02:00
status = "ERROR"
return
2023-03-04 19:13:16 +01:00
2025-04-08 09:25:25 +02:00
time.sleep(1)
2023-03-04 17:51:50 +01:00
loop = 0
2023-03-04 19:13:16 +01:00
2023-03-04 17:51:50 +01:00
while loop < (1024 * 256):
2025-04-17 15:20:06 +02:00
res = self.pandora.task_status(
worker["taskId"], worker["seed"])
logging.info(f"task_status={res}")
2025-04-07 15:29:22 +02:00
# Handle response from Pandora
2025-04-17 15:20:06 +02:00
if "status" in res:
status = res["status"]
2025-04-07 15:29:22 +02:00
if status != "WAITING":
break
2025-04-17 15:20:06 +02:00
else:
2025-04-07 15:29:22 +02:00
status = "ERROR"
return
2023-03-04 17:51:50 +01:00
# wait a little
2023-03-04 19:28:00 +01:00
pass
2025-04-08 09:25:25 +02:00
time.sleep(1)
2023-03-04 17:51:50 +01:00
loop += 1
2024-01-19 07:30:25 +01:00
end_time = time.time()
2023-03-04 17:51:50 +01:00
# log the result
2025-04-17 15:20:06 +02:00
log(
f"Scan {file_name} "
f"[{human_readable_size(file_size)}] "
"-> "
f"{status} ({(end_time - start_time):.1f}s)"
)
2023-03-04 17:51:50 +01:00
logging.info(
2023-03-08 08:26:25 +01:00
f'boxname="{boxname}", '
2023-03-04 17:51:50 +01:00
f'file="{file_name}", '
f'size="{file_size}", '
f'status="{status}"", '
2024-01-19 07:30:25 +01:00
f'duration="{int(end_time - start_time)}"'
)
2023-03-04 17:51:50 +01:00
# Get lock
2025-04-17 15:20:06 +02:00
queueLock.acquire()
2023-03-04 17:51:50 +01:00
scanned += file_size
file_count += 1
if status == "ALERT":
# add file to list
infected_files.append(file)
# Release lock
2025-04-17 15:20:06 +02:00
queueLock.release()
2023-03-04 17:51:50 +01:00
# update status bar
2025-04-16 17:04:16 +02:00
update_bar(scanned * 100 // f_used)
2023-03-04 17:51:50 +01:00
2025-04-17 15:20:06 +02:00
if hasQuarantine and status == "ALERT":
2023-03-04 17:51:50 +01:00
if not os.path.isdir(qfolder):
os.mkdir(qfolder)
shutil.copyfile(file, os.path.join(qfolder, file_name))
2023-02-15 13:22:59 +01:00
2023-02-25 11:19:37 +01:00
except Exception as ex:
2023-03-04 17:51:50 +01:00
log(f"Unexpected error: {str(ex)}", flush=True)
2025-04-17 15:20:06 +02:00
logging.info(
f'boxname="{boxname}", '
f'error="{str(ex)}"',
exc_info=True)
2023-03-04 17:51:50 +01:00
2025-04-17 20:00:40 +02:00
logging.info(f'{"Start done."}')
2023-03-04 17:51:50 +01:00
2025-04-17 15:20:06 +02:00
2023-03-04 17:51:50 +01:00
# ----------------------------------------------------------
2024-01-19 07:30:25 +01:00
2023-03-04 17:51:50 +01:00
def config():
2025-04-17 15:20:06 +02:00
global isFakeScan, hasUSBAutoMount, pandora_root_url
2025-04-22 12:50:37 +02:00
global hasQuarantine, quarantine_folder, has_curses, maxThreads, maxFileSize
2023-03-04 17:51:50 +01:00
""" read configuration file """
# intantiate a ConfirParser
config_parser = configparser.ConfigParser()
# read the config file
2024-01-19 07:30:25 +01:00
config_parser.read("pandora-box.ini")
2023-03-04 17:51:50 +01:00
# set values
2025-04-17 15:20:06 +02:00
isFakeScan = config_parser["DEFAULT"]["FAKE_SCAN"].lower() == "true"
hasUSBAutoMount = config_parser["DEFAULT"]["USB_AUTO_MOUNT"].lower(
) == "true"
2024-01-19 07:30:25 +01:00
pandora_root_url = config_parser["DEFAULT"]["PANDORA_ROOT_URL"]
2023-03-04 17:51:50 +01:00
# Quarantine
2025-04-17 15:20:06 +02:00
hasQuarantine = config_parser["DEFAULT"]["QUARANTINE"].lower() == "true"
2024-01-19 07:30:25 +01:00
quarantine_folder = config_parser["DEFAULT"]["QUARANTINE_FOLDER"]
2023-03-04 17:51:50 +01:00
# Curses
2024-01-19 07:30:25 +01:00
has_curses = config_parser["DEFAULT"]["CURSES"].lower() == "true"
2023-03-04 18:52:40 +01:00
# MaxThreads
2024-01-19 07:30:25 +01:00
maxThreads = int(config_parser["DEFAULT"]["THREADS"])
2025-04-22 11:43:00 +02:00
# MaxFileSize
maxFileSize = int(config_parser["DEFAULT"]["MAX_FILE_SIZE"])
2023-03-04 17:51:50 +01:00
# ----------------------------------------------------------
2024-01-19 07:30:25 +01:00
2023-03-04 17:51:50 +01:00
def human_readable_size(size, decimal_places=1):
2024-01-19 07:30:25 +01:00
"""Convert size to human readble string"""
for unit in ["B", "KB", "MB", "GB", "TB"]:
2023-03-04 17:51:50 +01:00
if size < 1024.0:
return f"{size:.{decimal_places}f}{unit}"
size /= 1024.0
return None
# -----------------------------------------------------------
# Image Screen
# -----------------------------------------------------------
2024-01-19 07:30:25 +01:00
2023-03-04 17:51:50 +01:00
def display_image(status):
2024-01-19 07:30:25 +01:00
"""Display image on screen"""
2023-03-04 17:51:50 +01:00
if not has_curses:
if status == "WAIT":
image = "images/key*.png"
elif status == "WORK":
image = "images/wait*.png"
elif status == "OK":
image = "images/ok.png"
elif status == "BAD":
image = "images/bad.png"
elif status == "ERROR":
image = "images/error.png"
2023-02-15 18:03:18 +01:00
else:
2023-03-04 17:51:50 +01:00
return
# hide old image
os.system("killall -s 9 fim 2>/dev/null")
# display image
if "*" in image:
# slide show
2025-04-17 15:20:06 +02:00
os.system(
f"fim -qa -c 'while(1){{display;sleep 1;next;}}' {image}"
"</dev/null 2>/dev/null >/dev/null &"
)
2023-02-15 18:03:18 +01:00
else:
2023-03-04 17:51:50 +01:00
# only one image
os.system(f"fim -qa {image} </dev/null 2>/dev/null >/dev/null &")
2023-02-15 13:22:59 +01:00
2023-03-04 17:51:50 +01:00
# -----------------------------------------------------------
# has_curses Screen
# -----------------------------------------------------------
def init_curses():
global screen
"""Initialise curses"""
if has_curses:
screen = curses.initscr()
screen.keypad(1)
2025-04-17 15:20:06 +02:00
curses.mousemask(curses.ALL_MOUSE_EVENTS |
curses.REPORT_MOUSE_POSITION)
2023-03-04 17:51:50 +01:00
curses.flushinp()
curses.noecho()
2023-03-05 12:33:52 +01:00
curses.curs_set(0)
2023-03-04 17:51:50 +01:00
else:
2023-03-05 12:58:15 +01:00
# hide blinking cursor
2023-03-05 13:12:19 +01:00
sys.stdout.write("\033[?1;0;0c")
2023-03-05 12:58:15 +01:00
sys.stdout.flush()
# display wait
2023-03-04 17:51:50 +01:00
display_image("WAIT")
def print_fslabel(label):
"""Print FS Label"""
if has_curses:
2025-04-17 15:20:06 +02:00
status_win.addstr(
1,
1,
f"Partition : {label!s:32}",
curses.color_pair(2))
2023-03-04 17:51:50 +01:00
status_win.refresh()
def print_size(label):
"""Print FS Size"""
if has_curses:
status_win.addstr(2, 1, f"Size : {label!s:32} ", curses.color_pair(2))
status_win.refresh()
def print_used(label):
"""Print FS Used Size"""
if has_curses:
status_win.addstr(3, 1, f"Used : {label!s:32} ", curses.color_pair(2))
status_win.refresh()
def print_fstype(label):
"""Print device FS type"""
if has_curses:
2025-04-17 15:20:06 +02:00
status_win.addstr(
1,
50,
f"Part / Type : {label!s:32}",
curses.color_pair(2))
2023-03-04 17:51:50 +01:00
status_win.refresh()
def print_model(label):
"""Print device model"""
if has_curses:
status_win.addstr(2, 50, f"Model : {label!s:32}", curses.color_pair(2))
status_win.refresh()
def print_serial(label):
"""Print device serail number"""
if has_curses:
2025-04-17 15:20:06 +02:00
status_win.addstr(
3,
50,
f"Serial : {label!s:32}",
curses.color_pair(2))
2023-03-04 17:51:50 +01:00
status_win.refresh()
def init_bar():
"""Initialise progress bar"""
global progress_win
if has_curses:
progress_win = curses.newwin(3, curses.COLS - 12, 17, 5)
progress_win.border(0)
progress_win.refresh()
def update_bar(progress, flush=False):
global last_update_time
"""Update progress bar"""
2024-01-19 07:30:25 +01:00
if flush or ((time.time() - last_update_time) >= 1):
2023-03-04 17:51:50 +01:00
last_update_time = time.time()
if has_curses:
if progress == 0:
progress_win.clear()
progress_win.border(0)
2025-04-16 16:56:07 +02:00
time.sleep(1)
2023-03-04 17:51:50 +01:00
progress_win.addstr(0, 1, "Progress:")
2023-02-15 13:22:59 +01:00
else:
2023-03-04 17:51:50 +01:00
pos = ((curses.COLS - 14) * progress) // 100
progress_win.addstr(1, 1, "#" * pos)
progress_win.addstr(0, 1, f"Progress: {progress}%")
progress_win.refresh()
def print_screen():
global status_win
"""Print main screen"""
if has_curses:
curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK)
curses.init_pair(2, curses.COLOR_BLUE, curses.COLOR_BLACK)
curses.init_pair(3, curses.COLOR_GREEN, curses.COLOR_BLACK)
title_win = curses.newwin(12, curses.COLS, 0, 0)
# title_win.border(0)
title_col = (curses.COLS - len(logo[0])) // 2
title_win.addstr(1, title_col, logo[0], curses.color_pair(1))
title_win.addstr(2, title_col, logo[1], curses.color_pair(1))
title_win.addstr(3, title_col, logo[2], curses.color_pair(1))
title_win.addstr(4, title_col, logo[3], curses.color_pair(1))
title_win.addstr(5, title_col, logo[4], curses.color_pair(1))
title_win.addstr(6, title_col, logo[5], curses.color_pair(1))
title_win.addstr(7, title_col, logo[6], curses.color_pair(1))
title_win.addstr(8, title_col, logo[7], curses.color_pair(1))
title_win.addstr(9, title_col, logo[8], curses.color_pair(1))
title_win.addstr(10, title_col, logo[9], curses.color_pair(1))
title_win.refresh()
status_win = curses.newwin(5, curses.COLS, 12, 0)
status_win.border(0)
status_win.addstr(0, 1, "USB Key Information")
print_fslabel("")
print_size("")
print_used("")
print_fstype("")
print_model("")
print_serial("")
init_bar()
update_bar(0, flush=True)
2024-01-19 07:30:25 +01:00
logging.info(f'boxname="{boxname}", ' "pandora-box-start")
2023-03-04 17:51:50 +01:00
def end_curses():
"""Closes curses"""
if has_curses:
curses.endwin()
curses.flushinp()
else:
# hide old image
os.system("killall -s 9 fim 2>/dev/null")
# -----------------------------------------------------------
# Logging windows
# -----------------------------------------------------------
2024-01-19 07:30:25 +01:00
2023-03-04 17:51:50 +01:00
def initlog():
"""Inititalize logging function"""
global log_win
if has_curses:
log_win = curses.newwin(curses.LINES - 20, curses.COLS, 20, 0)
log_win.border(0)
logging.basicConfig(
2025-04-17 15:20:06 +02:00
filename="/var/log/pandora-box.log",
level=logging.INFO,
format="%(asctime)s - %(message)s",
datefmt="%m/%d/%y %H:%M",
2023-03-04 17:51:50 +01:00
)
logs = []
last_update_time = 0
def log(msg, flush=False):
"""log a message with a new line"""
if has_curses:
# display log on screen
logs.append(msg)
if len(logs) > (curses.LINES - 22):
logs.pop(0)
log_update(flush)
def log_msg(msg):
"""update last message -> no new line"""
if has_curses:
# display log on screen
logs[-1] = msg
log_update()
def log_update(flush=False):
"""Update the log screen"""
global last_update_time
# do not refresh the screen too often
2024-01-19 07:30:25 +01:00
if flush or ((time.time() - last_update_time) >= 1):
2023-03-04 17:51:50 +01:00
last_update_time = time.time()
log_win.clear()
log_win.border(0)
for i in range(min(curses.LINES - 22, len(logs))):
2025-04-17 15:20:06 +02:00
log_win.addstr(i + 1,
1,
logs[i][: curses.COLS - 2],
curses.color_pair(3))
2023-03-04 17:51:50 +01:00
log_win.refresh()
# -----------------------------------------------------------
# Device
# -----------------------------------------------------------
2024-01-19 07:30:25 +01:00
2023-03-04 17:51:50 +01:00
def mount_device():
"""Mount USB device"""
global mount_point
2024-01-19 07:30:25 +01:00
log("Mount device", flush=True)
2025-04-17 15:20:06 +02:00
if hasUSBAutoMount:
2023-03-04 17:51:50 +01:00
mount_point = None
loop = 0
while (mount_point is None) and (loop < 15):
# need to sleep before devide is mounted
time.sleep(1)
for partition in psutil.disk_partitions():
if partition.device == device.device_node:
mount_point = partition.mountpoint
loop += 1
if mount_device is None:
2024-01-19 07:30:25 +01:00
log("No partition mounted", flush=True)
2023-03-04 17:51:50 +01:00
else:
mount_point = "/media/box"
if not os.path.exists("/media/box"):
log("folder /media/box does not exists", flush=True)
2025-04-17 10:57:02 +02:00
logging.error("folder /media/box does not exists")
2023-03-04 17:51:50 +01:00
return None
2025-04-17 10:57:02 +02:00
# Mount device
try:
2025-04-17 20:00:40 +02:00
subprocess.run(
2025-04-18 09:22:57 +02:00
["sudo",
2025-04-18 12:07:52 +02:00
"mount", "-o", "uid=1000,gid=1000,dmask=0000,fmask=0000",
2025-04-18 09:22:57 +02:00
device.device_node, "/media/box"],
2025-04-17 10:57:02 +02:00
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
2025-04-17 15:20:06 +02:00
check=True,
2025-04-17 10:57:02 +02:00
)
logging.info("Mount successful")
except subprocess.CalledProcessError as e:
logging.error(f"Mount failed: return code {e.returncode}")
return None
except FileNotFoundError:
2025-04-18 09:13:37 +02:00
logging.error("Command 'mount' not found")
2025-04-17 10:57:02 +02:00
return None
except Exception as e:
logging.error(f"Unexpected error: {e}")
return None
2023-03-04 17:51:50 +01:00
loop = 0
while loop < 10:
time.sleep(1)
2023-03-03 15:57:11 +01:00
try:
2023-03-04 17:51:50 +01:00
os.statvfs(mount_point)
except Exception as ex:
2025-04-17 14:55:41 +02:00
log(f"Mount - Unexpected error: {ex}", flush=True)
2023-03-04 17:51:50 +01:00
loop += 1
continue
break
def umount_device():
"""Unmount USB device"""
2025-04-17 15:20:06 +02:00
if hasUSBAutoMount:
2023-03-04 17:51:50 +01:00
log("Sync partitions", flush=True)
os.system("sync")
else:
2025-04-17 14:55:41 +02:00
try:
2025-04-17 20:00:40 +02:00
subprocess.run(
2025-04-18 09:13:37 +02:00
["sudo", "umount", "/media/box"],
2025-04-17 14:55:41 +02:00
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
2025-04-17 15:20:06 +02:00
check=True,
2025-04-17 14:55:41 +02:00
)
logging.info("Umount successful")
except subprocess.CalledProcessError as e:
2025-04-18 09:13:37 +02:00
logging.error(f"umount failed: return code {e.returncode}")
2025-04-17 14:55:41 +02:00
return None
except FileNotFoundError:
2025-04-18 09:13:37 +02:00
logging.error("Command 'umount' not found")
2025-04-17 14:55:41 +02:00
return None
except Exception as e:
logging.error(f"Umount - Unexpected error: {e}")
return None
2023-03-04 17:51:50 +01:00
def log_device_info(dev):
"""Log device information"""
logging.info(
2023-03-08 08:26:25 +01:00
f'boxname="{boxname}", '
f'device_name="{dev.get("DEVNAME")}, '
f'path_id="{dev.get("ID_PATH")}", '
f'bus system="{dev.get("ID_BUS")}", '
f'USB_driver="{dev.get("ID_USB_DRIVER")}", '
f'device_type="{dev.get("DEVTYPE")}", '
f'device_usage="{dev.get("ID_FS_USAGE")}", '
f'partition type="{dev.get("ID_PART_TABLE_TYPE")}", '
f'fs_type="{dev.get("ID_FS_TYPE")}", '
f'partition_label="{dev.get("ID_FS_LABEL")}", '
f'device_model="{dev.get("ID_MODEL")}", '
f'model_id="{dev.get("ID_MODEL_ID")}", '
2025-04-17 10:57:02 +02:00
f'serial_short="{dev.get("ID_SERIAL_SHORT")}", '
f'serial="{dev.get("ID_SERIAL")}"'
2024-01-19 07:30:25 +01:00
)
2023-03-04 17:51:50 +01:00
# -----------------------------------------------------------
# pandora
# -----------------------------------------------------------
2024-01-19 07:30:25 +01:00
2023-03-04 17:51:50 +01:00
def scan():
"""Scan devce with pypandora"""
global pandora, qfolder
2025-04-17 15:20:06 +02:00
global workQueue, EXIT_FLAG, threads, scanned
2025-04-16 17:06:07 +02:00
global mount_point, infected_files, file_count, f_used
2023-03-04 17:51:50 +01:00
# get device size
2025-04-18 12:07:52 +02:00
logging.info(f'start scan')
2023-03-04 17:51:50 +01:00
try:
statvfs = os.statvfs(mount_point)
except Exception as ex:
log(f"error={ex}", flush=True)
2025-04-17 15:20:06 +02:00
logging.info(
f'boxname="{boxname}", '
f'error="{str(ex)}"',
exc_info=True)
2023-03-04 17:51:50 +01:00
if not has_curses:
display_image("ERROR")
return "ERROR"
# Print device information
2025-04-16 16:56:07 +02:00
f_size = human_readable_size(statvfs.f_frsize * statvfs.f_blocks)
print_size(f_size)
2025-04-16 16:57:49 +02:00
logging.info(f'size="{f_size}"')
2023-03-04 17:51:50 +01:00
2025-04-16 16:56:07 +02:00
f_used = statvfs.f_frsize * (statvfs.f_blocks - statvfs.f_bfree)
2025-04-16 16:58:40 +02:00
print_used(human_readable_size(f_used))
2025-04-17 10:57:02 +02:00
logging.info(f'used="{human_readable_size(f_used)}"')
2023-03-04 17:51:50 +01:00
# scan device
infected_files = []
scanned = 0
file_count = 0
scan_start_time = time.time()
2025-04-17 15:20:06 +02:00
if hasQuarantine:
qfolder = os.path.join(
quarantine_folder, datetime.datetime.now().strftime("%y%m%d-%H%M")
)
2023-03-04 17:51:50 +01:00
# Instantice work quere
2025-04-17 15:20:06 +02:00
workQueue = queue.Queue(512)
2023-03-04 17:51:50 +01:00
# set exit condition to false
2025-04-17 15:20:06 +02:00
EXIT_FLAG = False
2023-03-04 17:51:50 +01:00
# Instanciate threads
2023-03-04 21:12:26 +01:00
for _ in range(maxThreads):
thread = scanThread()
2023-03-04 17:51:50 +01:00
thread.start()
threads.append(thread)
# Fill the work queue
for root, _, files in os.walk(mount_point):
for file in files:
2025-04-17 15:20:06 +02:00
while workQueue.full():
2025-04-16 16:56:07 +02:00
time.sleep(1)
2023-03-04 17:51:50 +01:00
pass
2025-04-17 15:20:06 +02:00
queueLock.acquire()
workQueue.put(os.path.join(root, file))
queueLock.release()
2023-03-04 17:51:50 +01:00
# Wait for queue to empty
2025-04-17 15:20:06 +02:00
while not workQueue.empty():
2025-04-16 16:56:07 +02:00
time.sleep(1)
2023-03-04 17:51:50 +01:00
pass
# Notify threads it's time to exit
2025-04-17 15:20:06 +02:00
EXIT_FLAG = True
2023-03-04 17:51:50 +01:00
# Wait for all threads to complete
for t in threads:
t.join()
update_bar(100, flush=True)
2024-01-19 07:30:25 +01:00
log(
"Scan done in %.1fs, %d files scanned, %d files infected"
% ((time.time() - scan_start_time), file_count, len(infected_files)),
flush=True,
)
2023-03-04 17:51:50 +01:00
logging.info(
2023-03-08 08:26:25 +01:00
f'boxname="{boxname}", '
2023-03-04 17:51:50 +01:00
f'duration="{int(time.time() - scan_start_time)}", '
f'files_scanned="{file_count}", '
2024-01-19 07:30:25 +01:00
f'files_infected="{len(infected_files)}"'
)
2023-03-04 17:51:50 +01:00
return "CLEAN"
# --------------------------------------
2025-04-17 15:20:06 +02:00
2023-03-04 17:51:50 +01:00
def wait():
"""Wait for insert of remove of USB device"""
2023-03-13 11:10:34 +01:00
# handle error - first unmount the device
umount_device()
2023-03-04 17:51:50 +01:00
# Loop
context = pyudev.Context()
monitor = pyudev.Monitor.from_netlink(context)
monitor.filter_by("block")
try:
for dev in iter(monitor.poll, None):
2025-04-17 15:20:06 +02:00
if dev.get(
"ID_FS_USAGE") == "filesystem" and dev.device_node[5:7] == "sd":
2023-03-04 17:51:50 +01:00
if dev.action == "add":
return device_inserted(dev)
if dev.action == "remove":
return device_removed()
except Exception as ex:
log(f"Unexpected error: {str(ex)}", flush=True)
2025-04-17 15:20:06 +02:00
logging.info(
f'boxname="{boxname}", '
f'error="{str(ex)}"',
exc_info=True)
2023-03-04 17:51:50 +01:00
return "STOP"
def device_inserted(dev):
global device
log("Device inserted", flush=True)
2024-01-19 07:30:25 +01:00
logging.info(f'boxname="{boxname}", ' "device-inserted")
2023-03-04 17:51:50 +01:00
device = dev
log_device_info(device)
if not has_curses:
display_image("WORK")
else:
# display device type
print_fslabel(device.get("ID_FS_LABEL"))
print_fstype(device.get("ID_FS_TYPE"))
2023-03-04 17:51:50 +01:00
print_model(device.get("ID_MODEL"))
print_serial(device.get("ID_SERIAL_SHORT"))
return "INSERTED"
def device_removed():
global device
log("Device removed", flush=True)
2024-01-19 07:30:25 +01:00
logging.info(f'boxname="{boxname}", ' "device-removed")
2023-03-04 17:51:50 +01:00
device = None
if not has_curses:
display_image("WAIT")
else:
print_fslabel("")
print_size("")
print_used("")
print_fstype("")
print_model("")
print_serial("")
update_bar(0, flush=True)
return "WAIT"
# --------------------------------------
2024-01-19 07:30:25 +01:00
2023-03-04 17:51:50 +01:00
def mount():
2024-01-19 07:30:25 +01:00
"""Mount device"""
2023-03-04 17:51:50 +01:00
global mount_point
mount_device()
2024-01-19 07:30:25 +01:00
log(f"Partition mounted at {mount_point}", flush=True)
2025-04-16 16:56:07 +02:00
logging.info(f"Partition mounted at {mount_point}")
2023-03-04 17:51:50 +01:00
if mount_point is None:
# no partition
if not has_curses:
display_image("WAIT")
2023-02-13 15:45:28 +01:00
return "WAIT"
2023-03-04 17:51:50 +01:00
try:
os.statvfs(mount_point)
except Exception as ex:
2023-03-04 20:42:57 +01:00
log(f"Unexpected error: {str(ex)}", flush=True)
2025-04-17 15:20:06 +02:00
logging.info(
f'boxname="{boxname}", '
f'error="{str(ex)}"',
exc_info=True)
2023-03-04 17:51:50 +01:00
if not has_curses:
display_image("WAIT")
2023-02-13 15:45:28 +01:00
return "WAIT"
2023-03-04 17:51:50 +01:00
return "SCAN"
2022-06-11 18:25:10 +02:00
2023-02-15 13:22:59 +01:00
2023-03-04 17:51:50 +01:00
# --------------------------------------
2024-01-19 07:30:25 +01:00
2023-03-04 17:51:50 +01:00
def error():
2024-01-19 07:30:25 +01:00
"""Display error message"""
2023-03-04 17:51:50 +01:00
if not has_curses:
display_image("ERROR")
return "WAIT"
2024-01-19 07:11:56 +01:00
2023-03-23 14:14:48 +01:00
# -----------------------------------------------------------
# Wait for mouse click or enter
# -----------------------------------------------------------
mouseEvent = threading.Event()
enterEvent = threading.Event()
mouseOrEnterCondition = threading.Condition()
2024-01-19 07:11:56 +01:00
2023-03-23 14:14:48 +01:00
def mouseClickThread():
mouse = open("/dev/input/mice", "rb")
os.set_blocking(mouse.fileno(), False)
down = False
while not enterEvent.is_set():
buf = mouse.read(3)
2025-04-17 15:20:06 +02:00
if buf is not None:
2024-01-19 07:30:25 +01:00
if (buf[0] & 0x1) == 1:
2023-03-23 14:14:48 +01:00
down = True
2024-01-19 07:30:25 +01:00
if ((buf[0] & 0x1) == 0) and down:
2023-03-23 14:14:48 +01:00
break
2025-04-16 16:56:07 +02:00
time.sleep(1)
2023-03-23 14:14:48 +01:00
mouse.close()
mouseEvent.set()
with mouseOrEnterCondition:
mouseOrEnterCondition.notify()
def enterKeyThread():
os.set_blocking(sys.stdin.fileno(), False)
while not mouseEvent.is_set():
input = sys.stdin.readline()
2024-01-19 07:30:25 +01:00
if len(input) > 0:
2023-03-23 14:14:48 +01:00
break
time.sleep(0.1)
enterEvent.set()
with mouseOrEnterCondition:
mouseOrEnterCondition.notify()
def waitMouseOrEnter():
with mouseOrEnterCondition:
threading.Thread(target=mouseClickThread, args=()).start()
threading.Thread(target=enterKeyThread, args=()).start()
mouseEvent.clear()
enterEvent.clear()
while not (mouseEvent.is_set() or enterEvent.is_set()):
mouseOrEnterCondition.wait()
2023-03-04 17:51:50 +01:00
2023-03-23 14:21:35 +01:00
2023-03-04 17:51:50 +01:00
# --------------------------------------
2024-01-19 07:30:25 +01:00
2023-03-04 17:51:50 +01:00
def clean():
"""Remove infected files"""
if len(infected_files) > 0:
# display message
2023-03-04 20:42:57 +01:00
log(f"{len(infected_files)} infected files detecetd:")
2025-04-17 15:20:06 +02:00
logging.info(
f'boxname="{boxname}", '
f"infeted_files={len(infected_files)}")
2023-03-04 17:51:50 +01:00
if not has_curses:
display_image("BAD")
else:
# print list of files
cnt = 0
for file in infected_files:
log(file)
cnt = cnt + 1
2023-03-08 17:08:46 +01:00
if cnt >= 10:
2024-01-19 07:30:25 +01:00
log("...")
2023-03-04 17:51:50 +01:00
break
# wait for clean
2024-01-19 07:30:25 +01:00
log("PRESS KEY TO CLEAN", flush=True)
2023-03-23 14:14:48 +01:00
waitMouseOrEnter()
2023-02-25 11:19:37 +01:00
2023-03-07 11:25:00 +01:00
# TODO: check device is still present
2023-03-04 17:51:50 +01:00
# Remove infected files
files_removed = 0
2023-03-04 22:45:49 +01:00
has_error = False
2023-03-04 17:51:50 +01:00
for file in infected_files:
try:
os.remove(file)
log(f"{file} removed")
2024-01-19 07:30:25 +01:00
logging.info(f'boxname="{boxname}", ' f'removed="{file}"')
2023-03-04 17:51:50 +01:00
files_removed += 1
except Exception as ex:
2023-03-04 21:56:50 +01:00
log(f"could not remove: {str(ex)}", flush=True)
2025-04-17 15:20:06 +02:00
logging.info(
f'boxname="{boxname}", '
f'not_removed="{file}, '
f'error="{str(ex)}"',
exc_info=True,
)
2023-03-04 22:45:49 +01:00
has_error = True
2023-03-07 11:25:00 +01:00
umount_device()
2025-04-17 15:20:06 +02:00
logging.info(
f'boxname="{boxname}", '
f'cleaned="{files_removed}/{len(infected_files)}"')
2023-03-07 13:48:31 +01:00
2023-03-07 11:25:00 +01:00
if not has_error:
if has_curses:
2024-01-19 07:30:25 +01:00
log("Device cleaned !", flush=True)
2023-03-04 22:45:49 +01:00
else:
2023-03-07 11:25:00 +01:00
display_image("OK")
else:
if has_curses:
2024-01-19 07:30:25 +01:00
log("Device not cleaned !", flush=True)
2023-03-07 11:25:00 +01:00
else:
2023-03-07 13:48:31 +01:00
display_image("WAIT")
2023-03-04 17:51:50 +01:00
else:
if not has_curses:
display_image("OK")
return "WAIT"
# --------------------------------------
2024-01-19 07:30:25 +01:00
2023-03-04 17:51:50 +01:00
def move_to_script_folder():
"""Move to pandora-box folder"""
abspath = os.path.abspath(__file__)
dname = os.path.dirname(abspath)
os.chdir(dname)
# --------------------------------------
2025-04-09 15:39:35 +02:00
def wait_for_workers():
2025-04-22 11:52:52 +02:00
log(f"Starting..............", flush=True)
time.sleep(10)
2025-04-09 15:39:35 +02:00
pandora = pypandora.PyPandora(root_url=pandora_root_url)
2025-04-18 13:06:32 +02:00
workers = pandora.get_enabled_workers()
while not pandora.is_up:
log(f"Waiting for Pandora", flush=True)
time.sleep(1)
for worker in workers:
log(f"Worker: {worker}", flush=True)
2025-04-17 15:20:06 +02:00
2025-04-09 15:39:35 +02:00
# --------------------------------------
2024-01-19 07:30:25 +01:00
2023-03-04 17:51:50 +01:00
def startup():
"""Start Pandora-box"""
global logo
# Move to script folder
move_to_script_folder()
# read config
config()
2023-03-05 12:58:15 +01:00
# Initialize curses
2023-03-04 17:51:50 +01:00
init_curses()
# Initilize log
initlog()
# Read logo
2024-01-19 07:30:25 +01:00
with open("pandora-box.txt", mode="r", encoding="utf-8") as file1:
2023-03-04 17:51:50 +01:00
logo = file1.readlines()
# Print logo screen
print_screen()
2025-04-18 13:06:32 +02:00
# Wait for workers to start
wait_for_workers()
# Now Ready
log("Ready.", flush=True)
2023-03-23 14:14:48 +01:00
2023-03-04 17:51:50 +01:00
return "WAIT"
# --------------------------------------
2024-01-19 07:30:25 +01:00
2023-03-04 17:51:50 +01:00
def loop(state):
"""Main event loop"""
match state:
case "START":
return startup()
case "WAIT":
return wait()
case "INSERTED":
return mount()
case "SCAN":
return scan()
case "CLEAN":
return clean()
case "ERROR":
return error()
case _:
return "STOP"
# --------------------------------------
2024-01-19 07:30:25 +01:00
def get_lock(process_name):
2024-01-19 07:30:25 +01:00
"""Get a lock to check that Pandora-box is not already running"""
# Without holding a reference to our socket somewhere it gets garbage
# collected when the function exits
get_lock._lock_socket = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
try:
# The null byte (\0) means the socket is created
# in the abstract namespace instead of being created
# on the file system itself.
# Works only in Linux
2024-01-19 07:30:25 +01:00
get_lock._lock_socket.bind("\0" + process_name)
except socket.error:
2024-01-19 07:30:25 +01:00
print("Pandora-box is already running !", file=sys.stderr)
os.execvp("/usr/bin/bash", ["/usr/bin/bash", "--norc"])
sys.exit()
2025-04-17 15:20:06 +02:00
2023-03-04 17:51:50 +01:00
# --------------------------------------
2025-04-17 15:20:06 +02:00
2023-03-08 17:08:46 +01:00
def main(_):
2023-02-15 14:51:46 +01:00
"""Main entry point"""
2024-06-30 11:26:19 +02:00
print("main")
2023-03-04 17:51:50 +01:00
try:
2025-04-09 14:22:47 +02:00
# Enter the mail loop
2023-03-04 17:51:50 +01:00
state = "START"
while state != "STOP":
state = loop(state)
2025-04-18 13:06:32 +02:00
#except Exception as ex:
# print({str(ex)})
# log(f"Unexpected error: {str(ex)}", flush=True)
# logging.info(
# f'boxname="{boxname}", '
# f'error="{str(ex)}"',
# exc_info=True)
2023-03-04 17:51:50 +01:00
finally:
end_curses()
2023-02-12 18:10:04 +01:00
2023-02-25 11:19:37 +01:00
2022-06-11 21:06:59 +02:00
if __name__ == "__main__":
2024-06-30 11:26:19 +02:00
print("Start")
2024-01-19 07:30:25 +01:00
get_lock("pandora-box")
2023-03-04 17:51:50 +01:00
curses.wrapper(main)
2024-06-30 11:26:19 +02:00
print("Done.")