"""
fic_cloud_gui.py
Prototype File Integrity Checker with:
- GUI (Tkinter)
- Baseline storage in MongoDB Atlas (encrypted fields)
- Real-time monitoring (watchdog)
- Email alerts on file change
- SHA-256 hashing
Before running, set environment variables:
MONGODB_URI, MONGODB_DB (optional, default 'fic_db'), MONGODB_COLLECTION (optional,
default 'baselines')
SMTP_SERVER, SMTP_PORT, EMAIL_USER, EMAIL_PASS, ALERT_RECIPIENT
"""
import os
import sys
import json
import time
import hashlib
import threading
import traceback
from datetime import datetime
from tkinter import Tk, Button, Label, filedialog, Listbox, Scrollbar, END,
simpledialog, messagebox, StringVar
from [Link] import Observer
from [Link] import FileSystemEventHandler
from pymongo import MongoClient, errors
from [Link] import hashes
from [Link].pbkdf2 import PBKDF2HMAC
from [Link] import Fernet
import base64
import smtplib
from [Link] import EmailMessage
from pathlib import Path
# -------------------------
# Configuration & helpers
# -------------------------
MONGODB_URI = [Link]("MONGODB_URI")
MONGODB_DB = [Link]("MONGODB_DB", "fic_db")
MONGODB_COLLECTION = [Link]("MONGODB_COLLECTION", "baselines")
SMTP_SERVER = [Link]("SMTP_SERVER")
SMTP_PORT = int([Link]("SMTP_PORT", "587"))
EMAIL_USER = [Link]("EMAIL_USER")
EMAIL_PASS = [Link]("EMAIL_PASS")
ALERT_RECIPIENT = [Link]("ALERT_RECIPIENT")
SALT = b"fic_project_salt" # in production, use a securely stored random salt
def derive_key_from_password(password: str) -> bytes:
"""Derive a Fernet key from a password via PBKDF2"""
password_bytes = [Link]()
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=SALT,
iterations=390000,
)
key = base64.urlsafe_b64encode([Link](password_bytes))
return key
def sha256_of_file(path: str, chunk_size=8192):
"""Compute SHA-256 of file path"""
h = hashlib.sha256()
with open(path, "rb") as f:
while True:
chunk = [Link](chunk_size)
if not chunk:
break
[Link](chunk)
return [Link]()
def human_time(ts=None):
return [Link](ts if ts else [Link]()).strftime("%Y-%m-%d %H:
%M:%S")
# -------------------------
# MongoDB wrapper
# -------------------------
class MongoBaselineStore:
def __init__(self, uri, db_name="fic_db", coll_name="baselines", fernet=None):
if not uri:
raise ValueError("MONGODB_URI environment variable not set")
[Link] = MongoClient(uri, serverSelectionTimeoutMS=5000)
try:
# quick ping
[Link]("ping")
except [Link] as e:
raise RuntimeError(f"Cannot connect to MongoDB: {e}")
[Link] = [Link][db_name]
[Link] = [Link][coll_name]
[Link] = fernet
def upsert_file(self, path, hash_val, mtime):
doc = {
"path": path,
"hash": hash_val if not [Link] else
[Link](hash_val.encode()).decode(),
"mtime": mtime,
"updated_at": [Link]()
}
[Link].update_one({"path": path}, {"$set": doc}, upsert=True)
def get_all(self):
out = {}
for d in [Link]({}):
try:
stored_hash = [Link]("hash")
if [Link]:
stored_hash =
[Link](stored_hash.encode()).decode()
except Exception:
stored_hash = "<decrypt_error>"
out[d["path"]] = {"hash": stored_hash, "mtime": [Link]("mtime")}
return out
def get(self, path):
d = [Link].find_one({"path": path})
if not d:
return None
stored_hash = [Link]("hash")
if [Link]:
try:
stored_hash = [Link](stored_hash.encode()).decode()
except Exception:
stored_hash = "<decrypt_error>"
return {"hash": stored_hash, "mtime": [Link]("mtime")}
def delete(self, path):
[Link].delete_one({"path": path})
# -------------------------
# Email alert
# -------------------------
def send_email_alert(subject: str, body: str):
if not SMTP_SERVER or not EMAIL_USER or not EMAIL_PASS or not ALERT_RECIPIENT:
print("Email not configured. Set SMTP_SERVER, EMAIL_USER, EMAIL_PASS,
ALERT_RECIPIENT env vars")
return
try:
msg = EmailMessage()
msg["From"] = EMAIL_USER
msg["To"] = ALERT_RECIPIENT
msg["Subject"] = subject
msg.set_content(body)
with [Link](SMTP_SERVER, SMTP_PORT) as server:
[Link]()
[Link](EMAIL_USER, EMAIL_PASS)
server.send_message(msg)
print("Alert email sent")
except Exception as e:
print("Failed to send email:", e)
# -------------------------
# Watchdog event handler
# -------------------------
class FICEventHandler(FileSystemEventHandler):
def __init__(self, controller):
super().__init__()
[Link] = controller
def on_created(self, event):
if not event.is_directory:
[Link].handle_file_event(event.src_path, "created")
def on_modified(self, event):
if not event.is_directory:
[Link].handle_file_event(event.src_path, "modified")
def on_deleted(self, event):
if not event.is_directory:
[Link].handle_file_deleted(event.src_path)
def on_moved(self, event):
if not event.is_directory:
[Link].handle_file_event(event.dest_path, "moved")
# -------------------------
# Controller (core logic)
# -------------------------
class FICController:
def __init__(self, fernet, db_store, gui_callback=None):
[Link] = fernet
[Link] = db_store
self.gui_callback = gui_callback
self.monitored_dir = None
[Link] = None
self.monitor_thread = None
[Link] = False
def set_monitored_dir(self, d):
self.monitored_dir = d
def create_baseline(self):
if not self.monitored_dir:
raise RuntimeError("Monitored directory not set")
files = []
for root, dirs, filenames in [Link](self.monitored_dir):
for fname in filenames:
path = [Link](root, fname)
try:
h = sha256_of_file(path)
mtime = [Link](path)
[Link].upsert_file(path, h, mtime)
[Link]((path, h))
except Exception as e:
print("Skipping", path, e)
if self.gui_callback:
self.gui_callback("Baseline created with %d files" % len(files))
return files
def verify_all(self):
# compares current files under monitored_dir with DB
if not self.monitored_dir:
raise RuntimeError("Monitored directory not set")
db_all = [Link].get_all()
results = []
visited = set()
for root, dirs, filenames in [Link](self.monitored_dir):
for fname in filenames:
path = [Link](root, fname)
[Link](path)
try:
h = sha256_of_file(path)
rec = db_all.get(path)
if not rec:
[Link]((path, "NEW"))
[Link].upsert_file(path, h, [Link](path))
else:
if rec["hash"] != h:
[Link]((path, "MODIFIED"))
# update DB to new value
[Link].upsert_file(path, h, [Link](path))
else:
[Link]((path, "UNCHANGED"))
except Exception as e:
[Link]((path, "ERROR"))
# deleted files
for path in db_all.keys():
if path not in visited:
[Link]((path, "DELETED"))
# keep/deletion policy: do nothing or remove; here we keep record
for audit
if self.gui_callback:
self.gui_callback("Verification complete. Results: %d items" %
len(results))
return results
def handle_file_event(self, path, event_type):
try:
# small delay to allow write to complete
[Link](0.1)
if not [Link](path): # may have been deleted quickly
return
h = sha256_of_file(path)
rec = [Link](path)
if not rec:
# new file
[Link].upsert_file(path, h, [Link](path))
msg = f"NEW file detected: {path} at {human_time()}"
print(msg)
send_email_alert("FIC Alert - New file", msg)
if self.gui_callback:
self.gui_callback(msg)
else:
if rec["hash"] != h:
# modified
[Link].upsert_file(path, h, [Link](path))
msg = f"MODIFIED file detected: {path} at {human_time()}"
print(msg)
send_email_alert("FIC Alert - Modified file", msg)
if self.gui_callback:
self.gui_callback(msg)
else:
# unchanged (maybe metadata change)
pass
except Exception:
print("Error handling event for", path)
traceback.print_exc()
def handle_file_deleted(self, path):
msg = f"DELETED file detected: {path} at {human_time()}"
print(msg)
# Optionally keep record in DB or mark deleted
if self.gui_callback:
self.gui_callback(msg)
send_email_alert("FIC Alert - Deleted file", msg)
def start_monitor(self):
if not self.monitored_dir:
raise RuntimeError("No directory set to monitor")
if [Link]:
return
event_handler = FICEventHandler(self)
[Link] = Observer()
[Link](event_handler, self.monitored_dir, recursive=True)
[Link]()
[Link] = True
if self.gui_callback:
self.gui_callback(f"Started monitoring {self.monitored_dir}")
def stop_monitor(self):
if not [Link]:
return
[Link]()
[Link](timeout=2)
[Link] = False
if self.gui_callback:
self.gui_callback("Stopped monitoring")
# -------------------------
# GUI (Tkinter)
# -------------------------
class FICGUI:
def __init__(self, root, controller):
[Link] = root
[Link] = controller
[Link]("Cloud File Integrity Checker - Prototype")
self.status_var = StringVar()
self.status_var.set("Idle")
Label(root, text="Selected folder:").grid(row=0, column=0, sticky="w",
padx=5, pady=5)
self.lbl_folder = Label(root, text="None", wraplength=600, anchor="w")
self.lbl_folder.grid(row=0, column=1, columnspan=3, sticky="w", padx=5,
pady=5)
Button(root, text="Select Folder", command=self.select_folder).grid(row=1,
column=0, padx=5, pady=5)
Button(root, text="Create Baseline",
command=self.create_baseline).grid(row=1, column=1, padx=5, pady=5)
Button(root, text="Verify Now", command=self.verify_now).grid(row=1,
column=2, padx=5, pady=5)
self.btn_monitor = Button(root, text="Start Monitoring",
command=self.toggle_monitor)
self.btn_monitor.grid(row=1, column=3, padx=5, pady=5)
Label(root, text="Events / Status").grid(row=2, column=0, sticky="w",
padx=5)
[Link] = Listbox(root, width=100, height=15)
[Link](row=3, column=0, columnspan=4, padx=5, pady=5)
scrollbar = Scrollbar(root, orient="vertical", command=[Link])
[Link](row=3, column=4, sticky="ns")
[Link](yscrollcommand=[Link])
Label(root, textvariable=self.status_var).grid(row=4, column=0,
columnspan=4, sticky="w", padx=5, pady=5)
def log(self, text):
[Link](END, f"[{human_time()}] {text}")
[Link].yview_moveto(1)
def select_folder(self):
d = [Link]()
if d:
self.lbl_folder.config(text=d)
[Link].set_monitored_dir(d)
[Link](f"Folder selected: {d}")
def create_baseline(self):
def bg_job():
self.status_var.set("Creating baseline...")
try:
files = [Link].create_baseline()
[Link](f"Baseline saved. {len(files)} files recorded.")
except Exception as e:
[Link](f"Baseline failed: {e}")
self.status_var.set("Idle")
[Link](target=bg_job, daemon=True).start()
def verify_now(self):
def bg_job():
self.status_var.set("Verifying...")
try:
results = [Link].verify_all()
# summarize
counts = {}
for _, status in results:
counts[status] = [Link](status, 0) + 1
summary = ", ".join([f"{k}:{v}" for k, v in [Link]()])
[Link](f"Verify done. {summary}")
except Exception as e:
[Link](f"Verify failed: {e}")
self.status_var.set("Idle")
[Link](target=bg_job, daemon=True).start()
def toggle_monitor(self):
if not [Link]:
try:
[Link].start_monitor()
self.btn_monitor.config(text="Stop Monitoring")
except Exception as e:
[Link](f"Start monitor failed: {e}")
else:
[Link].stop_monitor()
self.btn_monitor.config(text="Start Monitoring")
# -------------------------
# Main application
# -------------------------
def main():
# Get user-provided password for encrypting baseline fields
root = Tk()
[Link]() # hide while asking password
password = [Link]("Encryption Password", "Enter encryption
password for baseline storage", show="*")
if password is None:
[Link]("Abort", "Password required. Exiting.")
[Link]()
return
key = derive_key_from_password(password)
fernet = Fernet(key)
# connect to MongoDB
try:
store = MongoBaselineStore(MONGODB_URI, MONGODB_DB, MONGODB_COLLECTION,
fernet=fernet)
except Exception as e:
[Link]("DB Error", f"Failed to connect to MongoDB Atlas:
{e}")
return
# create controller
controller = FICController(fernet, store, gui_callback=lambda s: [Link](s))
# start GUI
[Link]()
gui = FICGUI(root, controller)
# inject gui into controller callback (circular setup)
controller.gui_callback = [Link]
# show instructions log
[Link]("Welcome. Select a folder and create baseline. Monitoring will alert on
changes.")
[Link]("WM_DELETE_WINDOW", lambda: on_close(root, controller))
[Link]()
def on_close(root, controller):
try:
controller.stop_monitor()
except Exception:
pass
[Link]()
[Link]()
if __name__ == "__main__":
main()