diff --git a/Backend/__pycache__/app.cpython-39.pyc b/Backend/__pycache__/app.cpython-39.pyc new file mode 100644 index 0000000..616148c Binary files /dev/null and b/Backend/__pycache__/app.cpython-39.pyc differ diff --git a/Backend/__pycache__/autoencoder.cpython-313.pyc b/Backend/__pycache__/autoencoder.cpython-313.pyc new file mode 100644 index 0000000..f195b63 Binary files /dev/null and b/Backend/__pycache__/autoencoder.cpython-313.pyc differ diff --git a/Backend/__pycache__/autoencoder_custom.cpython-313.pyc b/Backend/__pycache__/autoencoder_custom.cpython-313.pyc new file mode 100644 index 0000000..dd4b894 Binary files /dev/null and b/Backend/__pycache__/autoencoder_custom.cpython-313.pyc differ diff --git a/Backend/__pycache__/isolation_forest_cicids.cpython-313.pyc b/Backend/__pycache__/isolation_forest_cicids.cpython-313.pyc new file mode 100644 index 0000000..755c213 Binary files /dev/null and b/Backend/__pycache__/isolation_forest_cicids.cpython-313.pyc differ diff --git a/Backend/__pycache__/isolation_forest_custom.cpython-313.pyc b/Backend/__pycache__/isolation_forest_custom.cpython-313.pyc new file mode 100644 index 0000000..06f023c Binary files /dev/null and b/Backend/__pycache__/isolation_forest_custom.cpython-313.pyc differ diff --git a/Backend/app.py b/Backend/app.py new file mode 100644 index 0000000..b40fa1b --- /dev/null +++ b/Backend/app.py @@ -0,0 +1,661 @@ +from flask import Flask, jsonify, send_file, request +from flask_cors import CORS +import threading +import multiprocessing as mp +import gc +import traceback +import json +import time +import os +import platform +import psutil +import traceback +try: + import winreg +except ImportError: + winreg = None +from datetime import datetime +from reportlab.pdfgen import canvas +from reportlab.lib.pagesizes import A4 +from reportlab.lib.units import cm + +from isolation_forest_cicids import run_isolation_forest +from autoencoder import run_autoencoder +from isolation_forest_custom import run_isolation_forest_custom +from autoencoder_custom import run_autoencoder_custom +import signal +import sys +import shutil +from flask import send_from_directory + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + +TEMP_DIR = os.path.join(BASE_DIR, "temp") +REPORT_DIR = os.path.join(BASE_DIR, "reports") +PROGRESS_PATH = os.path.join(BASE_DIR, "progress.json") +FRONTEND_DIR = os.path.join(BASE_DIR, "..", "Frontend") +HTML_DIR = os.path.join(FRONTEND_DIR, "HTML Files") +JS_DIR = os.path.join(FRONTEND_DIR, "JS") +IMG_DIR = os.path.join(FRONTEND_DIR, "img") + +os.makedirs(TEMP_DIR, exist_ok=True) +os.makedirs(REPORT_DIR, exist_ok=True) + +app = Flask(__name__) +CORS(app) + +def force_memory_cleanup(): + gc.collect() + try: + import ctypes + ctypes.CDLL("libc.so.6").malloc_trim(0) + except Exception: + pass + +@app.route("/", methods=["GET"]) +def serve_index(): + return send_from_directory(HTML_DIR, "index.html") + +@app.route("/upload", methods=["GET"]) +def serve_upload(): + return send_from_directory(HTML_DIR, "upload.html") + +@app.route("/default", methods=["GET"]) +def serve_default(): + return send_from_directory(HTML_DIR, "default.html") + +@app.route("/JS/", methods=["GET"]) +def serve_js(filename): + return send_from_directory(JS_DIR, filename) + +@app.route("/img/", methods=["GET"]) +def serve_img(filename): + return send_from_directory(IMG_DIR, filename) + +model_result_iforest = None +model_result_autoencoder = None +model_result_iforest_custom = None +model_result_autoencoder_custom = None + +state_lock = threading.Lock() +is_running = False +current_job = None +worker_process = None + +RESULT_IFOREST = os.path.join(TEMP_DIR, "result_iforest.json") +RESULT_AUTOENCODER = os.path.join(TEMP_DIR, "result_autoencoder.json") +RESULT_IFOREST_CUSTOM = os.path.join(TEMP_DIR, "result_iforest_custom.json") +RESULT_AUTOENCODER_CUSTOM = os.path.join(TEMP_DIR, "result_autoencoder_custom.json") + +def handle_exit(signum, frame): + cleanup() + sys.exit(0) + +signal.signal(signal.SIGINT, handle_exit) +signal.signal(signal.SIGTERM, handle_exit) + + +def clear_directory(path): + if not os.path.exists(path): + return + + for item in os.listdir(path): + item_path = os.path.join(path, item) + try: + if os.path.isfile(item_path) or os.path.islink(item_path): + os.unlink(item_path) + elif os.path.isdir(item_path): + shutil.rmtree(item_path) + except Exception as e: + print(f"Failed to delete {item_path}: {e}") + +def cleanup(): + + clear_directory(TEMP_DIR) + clear_directory(REPORT_DIR) + + +def update_progress(value): + with open(PROGRESS_PATH, "w") as f: + json.dump({"progress": value}, f) + +def read_json_file(path, default=None): + try: + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + except Exception: + return default + + +def write_json_file(path, data): + with open(path, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2) + + +def get_result_path(job_name): + mapping = { + "iforest": RESULT_IFOREST, + "autoencoder": RESULT_AUTOENCODER, + "iforest_custom": RESULT_IFOREST_CUSTOM, + "autoencoder_custom": RESULT_AUTOENCODER_CUSTOM, + } + return mapping[job_name] + + +def get_job_runner(job_name): + if job_name == "iforest": + return lambda: run_isolation_forest(plot=False, table=False) + elif job_name == "autoencoder": + return lambda: run_autoencoder(plot=False, table=False) + elif job_name == "iforest_custom": + return run_isolation_forest_custom + elif job_name == "autoencoder_custom": + return run_autoencoder_custom + else: + raise ValueError(f"Unknown job name: {job_name}") + + +def worker_entry(job_name): + result_path = get_result_path(job_name) + start = time.time() + + try: + update_progress(1) + + runner = get_job_runner(job_name) + result = runner() + + end = time.time() + if isinstance(result, dict): + result["runtime"] = round(end - start, 2) + + write_json_file(result_path, { + "ready": True, + "data": result + }) + + update_progress(100) + + except Exception as e: + write_json_file(result_path, { + "ready": True, + "data": { + "error": True, + "message": str(e), + "details": traceback.format_exc() + } + }) + update_progress(0) + +def refresh_worker_state(): + global is_running, worker_process, current_job + + with state_lock: + proc = worker_process + running = is_running + + if running and proc is not None and not proc.is_alive(): + try: + proc.join(timeout=0.2) + except Exception: + pass + + try: + proc.close() + except Exception: + pass + + with state_lock: + is_running = False + worker_process = None + current_job = None + + force_memory_cleanup() + + + +def start_background_job(job_name): + global is_running, current_job, worker_process + result_path = get_result_path(job_name) + + refresh_worker_state() + + with state_lock: + if is_running: + return False + + is_running = True + current_job = job_name + + if os.path.exists(result_path): + try: + os.remove(result_path) + except Exception: + pass + + update_progress(0) + + proc = mp.Process(target=worker_entry, args=(job_name,)) + proc.start() + + with state_lock: + worker_process = proc + + return True + + +def read_progress(): + try: + with open(PROGRESS_PATH, "r") as f: + return json.load(f).get("progress", 0) + except Exception: + return 0 + +update_progress(0) + + +@app.route("/start-iforest", methods=["POST"]) +def start_iforest(): + started = start_background_job("iforest") + if not started: + return jsonify({ + "status": "already_running", + "message": "Another algorithm is already running" + }), 409 + return jsonify({"status": "started"}) + + +@app.route("/start-iforest-custom", methods=["POST"]) +def start_iforest_custom(): + started = start_background_job("iforest_custom") + if not started: + return jsonify({ + "status": "already_running", + "message": "Another algorithm is already running" + }), 409 + return jsonify({"status": "started"}) + +@app.route("/start-autoencoder-custom", methods=["POST"]) +def start_autoencoder_custom(): + started = start_background_job("autoencoder_custom") + if not started: + return jsonify({ + "status": "already_running", + "message": "Another algorithm is already running" + }), 409 + return jsonify({"status": "started"}) + + +@app.route("/start-autoencoder", methods=["POST"]) +def start_autoencoder(): + started = start_background_job("autoencoder") + if not started: + return jsonify({ + "status": "already_running", + "message": "Another algorithm is already running" + }), 409 + return jsonify({"status": "started"}) + + +@app.route("/get-progress", methods=["GET"]) +def get_progress(): + refresh_worker_state() + return jsonify(read_json_file(PROGRESS_PATH, {"progress": 0})) + + +@app.route("/get-status", methods=["GET"]) +def get_status(): + refresh_worker_state() + + with state_lock: + running = is_running + job = current_job + + progress_data = read_json_file(PROGRESS_PATH, {"progress": 0}) + + return jsonify({ + "running": running, + "current_job": job, + "progress": progress_data.get("progress", 0) + }) + + +@app.route("/get-isolation-forest-result", methods=["GET"]) +def get_iforest_result(): + refresh_worker_state() + data = read_json_file(RESULT_IFOREST) + if not data: + return jsonify({"ready": False}) + return jsonify(data) + +@app.route("/get-isolation-forest-result-custom", methods=["GET"]) +def get_iforest_result_custom(): + refresh_worker_state() + data = read_json_file(RESULT_IFOREST_CUSTOM) + if not data: + return jsonify({"ready": False}) + return jsonify(data) + + +@app.route("/get-autoencoder-result", methods=["GET"]) +def get_auto_result(): + refresh_worker_state() + data = read_json_file(RESULT_AUTOENCODER) + if not data: + return jsonify({"ready": False}) + return jsonify(data) + +@app.route("/get-autoencoder-result-custom", methods=["GET"]) +def get_autoencoder_result_custom(): + refresh_worker_state() + data = read_json_file(RESULT_AUTOENCODER_CUSTOM) + if not data: + return jsonify({"ready": False}) + return jsonify(data) + + + +def generate_pdf_report(results, algorithm, params): + filename = os.path.join(REPORT_DIR, "report.pdf") + c = canvas.Canvas(filename, pagesize=A4) + + w, h = A4 + y = h - 2*cm + + + c.setFont("Helvetica-Bold", 24) + c.drawCentredString(w/2, y, f"{algorithm} Report") + y -= 1.4*cm + + c.setFont("Helvetica", 11) + c.drawCentredString( + w/2, y, + f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + y -= 1.6*cm + + + c.setFont("Helvetica-Bold", 15) + c.drawString(2*cm, y, "Algorithm Parameters:") + y -= 0.9*cm + + c.setFont("Helvetica", 11) + t = c.beginText(2.2*cm, y) + t.setLeading(13) + for k, v in params.items(): + t.textLine(f"{k}: {v}") + c.drawText(t) + y = t.getY() - 1.1*cm + + + c.setFont("Helvetica-Bold", 15) + c.drawString(2*cm, y, "Execution Summary:") + y -= 0.9*cm + + c.setFont("Helvetica", 11) + t = c.beginText(2.2*cm, y) + t.setLeading(13) + t.textLine(f"Runtime: {results['runtime']} seconds") + t.textLine(f"Normal Traffic: {int(results['normal_count'])}") + t.textLine(f"Detected Attacks: {int(results['attack_count'])}") + t.textLine(f"Accuracy: {results['accuracy']:.4f}") + c.drawText(t) + y = t.getY() - 1.3*cm + + + sys = get_system_info() + + c.setFont("Helvetica-Bold", 15) + c.drawString(2 * cm, y, "System Information") + y -= 0.9 * cm + + c.setFont("Helvetica", 11) + info = c.beginText(2.2 * cm, y) + info.setLeading(13) + info.textLine(f"CPU: {sys['cpu']}") + info.textLine(f"Physical Cores: {sys['cpu_physical_cores']}") + info.textLine(f"Logical Cores: {sys['cpu_total_cores']}") + info.textLine(f"GPU: {sys['gpu_name']}") + info.textLine(f"OS: {sys['os']}") + info.textLine(f"Python Version: {sys['python_version']}") + info.textLine(f"System RAM: {sys['ram_total_gb']} GB") + c.drawText(info) + y = info.getY() - 1.3 * cm + + + c.setFont("Helvetica-Bold", 15) + c.drawString(2 * cm, y, "Memory Usage:") + y -= 0.9 * cm + + c.setFont("Helvetica", 11) + t = c.beginText(2.2 * cm, y) + t.setLeading(13) + t.textLine(f"RAM Before: {results['ram_before']} MB") + t.textLine(f"Peak RAM: {results['ram_peak']} MB") + t.textLine(f"RAM After: {results['ram_after']} MB") + t.textLine(f"Total Increase: {results['ram_increase']} MB") + c.drawText(t) + y = t.getY() - 1.1 * cm + + + c.setFont("Helvetica-Bold", 15) + c.drawString(2*cm, y, "Top 5 Most Significant Anomalies:") + y -= 1*cm + + c.setFont("Helvetica", 11) + for anomaly in results["top_anomalies"]: + t = c.beginText(2.4*cm, y) + t.setLeading(13) + for k, v in anomaly.items(): + t.textLine(f"{k}: {v}") + c.drawText(t) + y = t.getY() - 0.9*cm + + if y < 3*cm: + c.showPage() + y = h - 2*cm + c.setFont("Helvetica", 11) + + c.save() + return filename + +@app.route("/download-pdf", methods=["POST"]) +def download_pdf(): + data = request.get_json() + algorithm = data["algorithm"] + results = data["results"] + + raw_algorithm = data["algorithm"] + algorithm_name = raw_algorithm.lower() + + if "isolation forest" in algorithm_name: + algo_title = "Isolation Forest" + params = { + "n_estimators": 600, + "max_samples": 0.3, + "contamination": results.get("contamination"), + "max_features": 0.7 + } + + elif "autoencoder" in algorithm_name: + algo_title = "Autoencoder" + params = { + "epochs": 20, + "batch_size": 64, + "latent_dimension": 16, + "validation_split": 0.1 + } + + else: + params = {} + + pdf_path = generate_pdf_report(results, algo_title, params) + return send_file(pdf_path, as_attachment=True) + +@app.route("/get-system-info", methods=["GET"]) +def system_info_endpoint(): + return jsonify(get_system_info()) + +def get_cpu_name(): + try: + key = winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, + r"HARDWARE\DESCRIPTION\System\CentralProcessor\0" + ) + cpu_name, _ = winreg.QueryValueEx(key, "ProcessorNameString") + winreg.CloseKey(key) + return cpu_name.strip() + except: + return platform.processor() or "Unknown CPU" + +def get_system_info(): + return { + "cpu": get_cpu_name(), + "cpu_physical_cores": psutil.cpu_count(logical=False), + "cpu_total_cores": psutil.cpu_count(logical=True), + "gpu_available": False, + "gpu_name": "None", + "os": platform.platform(), + "python_version": platform.python_version(), + "ram_total_gb": round(psutil.virtual_memory().total / (1024**3), 2), + "ram_used_mb": round(psutil.virtual_memory().used / (1024**2), 2), + } + +@app.route("/download-json", methods=["POST"]) +def download_json(): + data = request.get_json() + + algorithm = data["algorithm"] + results = data["results"] + runtime = data["runtime"] + + raw_algorithm = data["algorithm"] + algorithm_name = raw_algorithm.lower() + + if "isolation forest" in algorithm_name: + algo_title = "Isolation Forest" + params = { + "n_estimators": 600, + "max_samples": 0.3, + "contamination": results.get("contamination"), + "max_features": 0.7 + } + + elif "autoencoder" in algorithm_name: + algo_title = "Autoencoder" + params = { + "epochs": 20, + "batch_size": 64, + "latent_dimension": 16, + "validation_split": 0.1 + } + + system_info = get_system_info() + + ram_info = { + "ram_before": results["ram_before"], + "ram_peak": results["ram_peak"], + "ram_after": results["ram_after"], + "ram_increase": results["ram_increase"] + } + + anomalies = results.get("top_anomalies", []) + + json_report = { + "algorithm": algorithm, + "generated": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "algorithm_parameters": params, + + "execution_summary": { + "runtime_seconds": runtime, + "normal_traffic": results["normal_count"], + "detected_attacks": results["attack_count"], + "accuracy": results["accuracy"] + }, + + "system_information": system_info, + + "memory_usage": ram_info, + + "top_5_anomalies": anomalies + } + + json_path = os.path.join(REPORT_DIR, "report.json") + with open(json_path, "w", encoding="utf-8") as f: + json.dump(json_report, f, indent=4) + + return send_file(json_path, as_attachment=True) + + +@app.route("/upload-dataset", methods=["POST"]) +def upload_dataset(): + refresh_worker_state() + + with state_lock: + if is_running: + return jsonify({ + "error": "Cannot upload a new dataset while an algorithm is running" + }), 409 + + if "file" not in request.files: + return jsonify({"error": "No file provided"}), 400 + + file = request.files["file"] + + if file.filename == "": + return jsonify({"error": "Empty filename"}), 400 + + if not file.filename.lower().endswith(".csv"): + return jsonify({"error": "Only CSV files are allowed"}), 400 + + csv_path = os.path.join(TEMP_DIR, "upload.csv") + tmp_path = os.path.join(TEMP_DIR, "upload.tmp.csv") + + try: + if os.path.exists(tmp_path): + os.remove(tmp_path) + + file.save(tmp_path) + os.replace(tmp_path, csv_path) + + return jsonify({"status": "uploaded", "path": csv_path}) + finally: + try: + file.close() + except Exception: + pass + + try: + if hasattr(file, "stream") and file.stream: + file.stream.close() + except Exception: + pass + + force_memory_cleanup() + +@app.route("/save-config", methods=["POST"]) +def save_config(): + refresh_worker_state() + + with state_lock: + if is_running: + return jsonify({ + "error": "Cannot save config while an algorithm is running" + }), 409 + + data = request.get_json() + + if data is None: + return jsonify({"error": "No JSON provided"}), 400 + + config_path = os.path.join(TEMP_DIR, "config.json") + + with open(config_path, "w", encoding="utf-8") as f: + json.dump(data, f, indent=4) + + force_memory_cleanup() + return jsonify({"status": "config_saved"}) + +if __name__ == "__main__": + mp.freeze_support() + app.run(host="0.0.0.0", port=5000, debug=False, use_reloader=False) diff --git a/Backend/autoencoder.py b/Backend/autoencoder.py new file mode 100644 index 0000000..0e37b52 --- /dev/null +++ b/Backend/autoencoder.py @@ -0,0 +1,223 @@ +import warnings +warnings.filterwarnings("ignore") +import psutil +import pandas as pd +import numpy as np +import json +import gc +from sklearn.preprocessing import StandardScaler +from sklearn.metrics import ( + confusion_matrix, + accuracy_score, + precision_recall_fscore_support, +) + +from tabulate import tabulate + + +import os + +def _force_memory_cleanup(): + gc.collect() + try: + import ctypes + ctypes.CDLL("libc.so.6").malloc_trim(0) + except Exception: + pass + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +PROGRESS_PATH = os.path.join(BASE_DIR, "progress.json") +DATASET_PATH = os.path.join(BASE_DIR, "dataset", "cicids2017_cleaned.csv") + + +def update_progress(value): + with open(PROGRESS_PATH, "w") as f: + json.dump({"progress": value}, f) + + +def run_autoencoder(csv_path=DATASET_PATH, plot=False, table=False): + + import tensorflow as tf + + from tensorflow.keras import Model + from tensorflow.keras.layers import Dense, Input, Dropout + from tensorflow.keras.optimizers import Adam + from tensorflow.keras.callbacks import EarlyStopping + from tensorflow.keras import backend as K + + process = psutil.Process() + ram_before = process.memory_info().rss + ram_peak = ram_before + update_progress(1) + current_ram = process.memory_info().rss + ram_peak = max(ram_peak, current_ram) + + df = pd.read_csv(csv_path) + update_progress(10) + current_ram = process.memory_info().rss + ram_peak = max(ram_peak, current_ram) + + df["fraud"] = df["Attack Type"].apply( + lambda x: 0 if "normal" in str(x).lower() else 1 + ) + true_labels = df["fraud"] + + normal_count = int((true_labels == 0).sum()) + attack_count = int((true_labels == 1).sum()) + + features = [c for c in df.columns if c not in ["Attack Type", "fraud"]] + scaler = StandardScaler() + + X_raw = df[features].to_numpy(dtype=np.float32, copy=True) + y = df["fraud"].to_numpy(dtype=np.int8, copy=True) + + X_scaled = scaler.fit_transform(X_raw).astype(np.float32, copy=False) + + X_train_normal = X_scaled[y == 0] + X_test = X_scaled + y_test = y + + CODE_DIM = 16 + INPUT_SHAPE = X_scaled.shape[1] + + inp = Input(shape=(INPUT_SHAPE,)) + x = Dense(128, activation="relu")(inp) + x = Dropout(0.1)(x) + x = Dense(64, activation="relu")(x) + x = Dense(16, activation="relu")(x) + code = Dense(CODE_DIM, activation="relu")(x) + x = Dense(16, activation="relu")(code) + x = Dense(64, activation="relu")(x) + x = Dense(128, activation="relu")(x) + out = Dense(INPUT_SHAPE, activation="linear")(x) + + autoencoder = Model(inp, out) + autoencoder.compile(loss="mae", optimizer=Adam(learning_rate=0.001)) + update_progress(40) + current_ram = process.memory_info().rss + ram_peak = max(ram_peak, current_ram) + + earlystopping = EarlyStopping( + monitor="val_loss", patience=5, restore_best_weights=True + ) + + history = autoencoder.fit( + X_train_normal, + X_train_normal, + epochs=20, + batch_size=64, + validation_split=0.1, + callbacks=[earlystopping], + shuffle=True, + verbose=1, + ) + update_progress(60) + current_ram = process.memory_info().rss + ram_peak = max(ram_peak, current_ram) + + reconstructions = autoencoder.predict(X_test, verbose=0) + reconstruction_error = np.mean(np.abs(reconstructions - X_test), axis=1) + + recons_df = pd.DataFrame( + {"error": reconstruction_error, "y_true": y_test} + ).reset_index(drop=True) + + threshold = np.percentile(recons_df["error"], 60) + recons_df["y_pred"] = (recons_df["error"] > threshold).astype(int) + update_progress(80) + current_ram = process.memory_info().rss + ram_peak = max(ram_peak, current_ram) + + cm = confusion_matrix(recons_df["y_true"], recons_df["y_pred"]) + accuracy = accuracy_score(recons_df["y_true"], recons_df["y_pred"]) + precision, recall, f1, _ = precision_recall_fscore_support( + recons_df["y_true"], + recons_df["y_pred"], + average=None, + labels=[0, 1], + ) + update_progress(90) + current_ram = process.memory_info().rss + ram_peak = max(ram_peak, current_ram) + + if table: + table_data = [ + ["Normal (0)", f"{precision[0]:.4f}", f"{recall[0]:.4f}", f"{f1[0]:.4f}"], + ["Attack (1)", f"{precision[1]:.4f}", f"{recall[1]:.4f}", f"{f1[1]:.4f}"], + ["Overall Accuracy", "-", "-", f"{accuracy:.4f}"], + ] + print( + tabulate( + table_data, + headers=["Class", "Precision", "Recall", "F1-Score"], + tablefmt="fancy_grid", + ) + ) + + results = { + "normal_count": float(normal_count), + "attack_count": float(attack_count), + "accuracy": float(accuracy), + "precision_normal": float(precision[0]), + "recall_normal": float(recall[0]), + "f1_normal": float(f1[0]), + "precision_attack": float(precision[1]), + "recall_attack": float(recall[1]), + "f1_attack": float(f1[1]), + } + + candidates = recons_df[ + (recons_df["y_pred"] == 1) & (recons_df["y_true"] == 1) + ].copy() + + + if len(candidates) < 5: + extra = recons_df[recons_df["y_pred"] == 1].copy() + candidates = pd.concat([candidates, extra]).drop_duplicates() + + if len(candidates) < 5: + candidates = recons_df.copy() + + candidates = candidates.sort_values("error", ascending=False).head(5) + idx = candidates.index + + df_top = df.iloc[idx].copy() + df_top["reconstruction_error"] = candidates["error"].values + + important_cols = [ + "Attack Type", + "Destination Port", + "Flow Duration", + "Total Fwd Packets", + "Flow Packets/s", + "Packet Length Mean", + ] + cols_exist = [c for c in important_cols if c in df_top.columns] + + top_anomalies = df_top[cols_exist + ["reconstruction_error"]].to_dict( + orient="records" + ) + + results["top_anomalies"] = top_anomalies + + del X_raw, X_scaled, X_train_normal, X_test, y_test, y + del reconstructions, reconstruction_error, recons_df, candidates, df_top + del autoencoder, history, scaler, df + + K.clear_session() + tf.keras.backend.clear_session(free_memory=True) + _force_memory_cleanup() + + ram_after = process.memory_info().rss + results["ram_before"] = round(ram_before / (1024 ** 2), 2) + results["ram_peak"] = round(ram_peak / (1024 ** 2), 2) + results["ram_after"] = round(ram_after / (1024 ** 2), 2) + results["ram_increase"] = round((ram_peak - ram_before) / (1024 ** 2), 2) + + update_progress(100) + + return results + +if __name__ == "__main__": + res = run_autoencoder(plot=True, table=True) + print(res) diff --git a/Backend/autoencoder_custom.py b/Backend/autoencoder_custom.py new file mode 100644 index 0000000..e0085fe --- /dev/null +++ b/Backend/autoencoder_custom.py @@ -0,0 +1,240 @@ +# autoencoder_custom.py +import warnings +warnings.filterwarnings("ignore") +import json +import pandas as pd +import numpy as np +import psutil +from sklearn.preprocessing import StandardScaler +from sklearn.metrics import ( + confusion_matrix, + accuracy_score, + precision_recall_fscore_support, +) +import os +import gc + + +def _force_memory_cleanup(): + gc.collect() + try: + import ctypes + ctypes.CDLL("libc.so.6").malloc_trim(0) + except Exception: + pass + + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +TEMP_DIR = os.path.join(BASE_DIR, "temp") + + +def update_progress(value, progress_path="progress.json"): + with open(progress_path, "w") as f: + json.dump({"progress": value}, f) + + +def _update_ram_peak(process, ram_peak): + current_ram = process.memory_info().rss + return max(ram_peak, current_ram) + + +def run_autoencoder_custom(csv_path=None, config_path=None, progress_path=None): + if csv_path is None: + csv_path = os.path.join(TEMP_DIR, "upload.csv") + if config_path is None: + config_path = os.path.join(TEMP_DIR, "config.json") + if progress_path is None: + progress_path = os.path.join(BASE_DIR, "progress.json") + + process = psutil.Process() + _force_memory_cleanup() + ram_before = process.memory_info().rss + ram_peak = ram_before + + update_progress(5, progress_path) + + import tensorflow as tf + from tensorflow.keras import Model + from tensorflow.keras.layers import Dense, Input, Dropout + from tensorflow.keras.optimizers import Adam + from tensorflow.keras.callbacks import EarlyStopping + from tensorflow.keras import backend as K + + ram_peak = _update_ram_peak(process, ram_peak) + + with open(config_path, "r", encoding="utf-8") as f: + config = json.load(f) + + ram_peak = _update_ram_peak(process, ram_peak) + + label_col = config["labeling"]["label_column"] + normal_value = config["labeling"]["normal_value"] + features = config["features"]["selected_columns"] + params = config.get("algorithm", {}).get("parameters", {}) + + needed_cols = list(dict.fromkeys(features + [label_col])) + df = pd.read_csv(csv_path, usecols=needed_cols) + + update_progress(15, progress_path) + ram_peak = _update_ram_peak(process, ram_peak) + + col = df[label_col] + df["__label"] = col.apply(lambda x: 0 if x == normal_value else 1) + + update_progress(25, progress_path) + ram_peak = _update_ram_peak(process, ram_peak) + + normal_count = int((df["__label"] == 0).sum()) + attack_count = int((df["__label"] == 1).sum()) + + X_raw = df[features].to_numpy(dtype=np.float32, copy=True) + y = df["__label"].to_numpy(dtype=np.int8, copy=True) + ram_peak = _update_ram_peak(process, ram_peak) + + scaler = StandardScaler() + X_scaled = scaler.fit_transform(X_raw).astype(np.float32, copy=False) + ram_peak = _update_ram_peak(process, ram_peak) + + X_train_normal = X_scaled[y == 0] + X_test = X_scaled + y_test = y + ram_peak = _update_ram_peak(process, ram_peak) + + INPUT_SHAPE = X_scaled.shape[1] + CODE_DIM = params.get("latent_dim", 16) + + inp = Input(shape=(INPUT_SHAPE,)) + x = Dense(128, activation="relu")(inp) + x = Dropout(0.1)(x) + x = Dense(64, activation="relu")(x) + x = Dense(16, activation="relu")(x) + code = Dense(CODE_DIM, activation="relu")(x) + x = Dense(16, activation="relu")(code) + x = Dense(64, activation="relu")(x) + x = Dense(128, activation="relu")(x) + out = Dense(INPUT_SHAPE, activation="linear")(x) + + autoencoder = Model(inp, out) + autoencoder.compile( + loss="mae", + optimizer=Adam(learning_rate=params.get("learning_rate", 0.001)) + ) + + update_progress(45, progress_path) + ram_peak = _update_ram_peak(process, ram_peak) + + earlystopping = EarlyStopping( + monitor="val_loss", patience=5, restore_best_weights=True + ) + + history = autoencoder.fit( + X_train_normal, + X_train_normal, + epochs=params.get("epochs", 20), + batch_size=params.get("batch_size", 64), + validation_split=params.get("validation_split", 0.1), + callbacks=[earlystopping], + shuffle=True, + verbose=1, + ) + + update_progress(75, progress_path) + ram_peak = _update_ram_peak(process, ram_peak) + + reconstructions = autoencoder.predict(X_test, verbose=0) + ram_peak = _update_ram_peak(process, ram_peak) + + reconstruction_error = np.mean(np.abs(reconstructions - X_test), axis=1) + ram_peak = _update_ram_peak(process, ram_peak) + + recons_df = pd.DataFrame( + {"error": reconstruction_error, "y_true": y_test} + ).reset_index(drop=True) + ram_peak = _update_ram_peak(process, ram_peak) + + threshold = np.percentile(recons_df["error"], params.get("threshold_percentile", 60)) + recons_df["y_pred"] = (recons_df["error"] > threshold).astype(int) + + update_progress(85, progress_path) + ram_peak = _update_ram_peak(process, ram_peak) + + cm = confusion_matrix(recons_df["y_true"], recons_df["y_pred"]) + accuracy = accuracy_score(recons_df["y_true"], recons_df["y_pred"]) + precision, recall, f1, _ = precision_recall_fscore_support( + recons_df["y_true"], + recons_df["y_pred"], + average=None, + labels=[0, 1], + ) + + results = { + "normal_count": float(normal_count), + "attack_count": float(attack_count), + "accuracy": float(accuracy), + "precision_normal": float(precision[0]), + "recall_normal": float(recall[0]), + "f1_normal": float(f1[0]), + "precision_attack": float(precision[1]), + "recall_attack": float(recall[1]), + "f1_attack": float(f1[1]), + } + + display_cols = list(dict.fromkeys(features + [label_col])) + top_df = df[display_cols].copy() + top_df["reconstruction_error"] = recons_df["error"].to_numpy() + top_df["predicted"] = recons_df["y_pred"].to_numpy() + top_df["true_label"] = y_test + ram_peak = _update_ram_peak(process, ram_peak) + + real_attacks = top_df[top_df["true_label"] == 1] + real_attacks_sorted = real_attacks.sort_values("reconstruction_error", ascending=False) + top_real_attacks = real_attacks_sorted.head(5) + ram_peak = _update_ram_peak(process, ram_peak) + + if len(top_real_attacks) > 0: + cols = [c for c in display_cols if c in top_real_attacks.columns] + ["reconstruction_error"] + top_anomalies = ( + top_real_attacks[cols] + .rename(columns={"reconstruction_error": "score"}) + .round(4) + .to_dict(orient="records") + ) + else: + top_anomalies = [] + + results["top_anomalies"] = top_anomalies + ram_peak = _update_ram_peak(process, ram_peak) + + del X_raw, X_scaled, X_train_normal, X_test, y_test, y + del reconstructions, reconstruction_error, recons_df + del top_df, real_attacks, real_attacks_sorted, top_real_attacks + del autoencoder, history, scaler, df, cm + + try: + K.clear_session() + except Exception: + pass + + try: + tf.keras.backend.clear_session(free_memory=True) + except TypeError: + tf.keras.backend.clear_session() + except Exception: + pass + + _force_memory_cleanup() + + ram_after = process.memory_info().rss + results["ram_before"] = round(ram_before / (1024 ** 2), 2) + results["ram_peak"] = round(ram_peak / (1024 ** 2), 2) + results["ram_after"] = round(ram_after / (1024 ** 2), 2) + results["ram_increase"] = round((ram_peak - ram_before) / (1024 ** 2), 2) + + update_progress(100, progress_path) + + return results + + +if __name__ == "__main__": + res = run_autoencoder_custom() + print(json.dumps(res, indent=2)) \ No newline at end of file diff --git a/Backend/dataset/cicids2017_cleaned.csv b/Backend/dataset/cicids2017_cleaned.csv new file mode 100644 index 0000000..32e5303 Binary files /dev/null and b/Backend/dataset/cicids2017_cleaned.csv differ diff --git a/Backend/isolation_forest_cicids.py b/Backend/isolation_forest_cicids.py new file mode 100644 index 0000000..0c45cf9 --- /dev/null +++ b/Backend/isolation_forest_cicids.py @@ -0,0 +1,211 @@ +import pandas as pd +import numpy as np +import json +import psutil +from sklearn.ensemble import IsolationForest +from sklearn.preprocessing import RobustScaler +from sklearn.metrics import confusion_matrix, accuracy_score, precision_recall_fscore_support +from tabulate import tabulate +import matplotlib.pyplot as plt +import seaborn as sns + +import gc + +import os +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +PROGRESS_PATH = os.path.join(BASE_DIR, "progress.json") +DATASET_PATH = os.path.join(BASE_DIR, "dataset", "cicids2017_cleaned.csv") + +def _force_memory_cleanup(): + gc.collect() + try: + import ctypes + ctypes.CDLL("libc.so.6").malloc_trim(0) + except Exception: + pass + + +def update_progress(value): + with open(PROGRESS_PATH, "w") as f: + json.dump({"progress": value}, f) + + +def run_isolation_forest(csv_path=DATASET_PATH, plot=False, table=False): + process = psutil.Process() + ram_before = process.memory_info().rss + ram_peak = ram_before + + update_progress(1) + current_ram = process.memory_info().rss + ram_peak = max(ram_peak, current_ram) + + df = pd.read_csv(csv_path) + update_progress(10) + current_ram = process.memory_info().rss + ram_peak = max(ram_peak, current_ram) + + df["fraud"] = df["Attack Type"].apply( + lambda x: 0 if "normal" in str(x).lower() else 1 + ) + true_labels = df["fraud"] + update_progress(20) + current_ram = process.memory_info().rss + ram_peak = max(ram_peak, current_ram) + + normal_count = int((true_labels == 0).sum()) + attack_count = int((true_labels == 1).sum()) + + attack_fraction = true_labels.mean() + contamination = min(attack_fraction * 2.5, 0.49) + + X = df.select_dtypes(include=[np.number]) + X = X.loc[:, X.std() > 0.01] + X = X.to_numpy(dtype=np.float32, copy=True) + update_progress(30) + current_ram = process.memory_info().rss + ram_peak = max(ram_peak, current_ram) + + scaler = RobustScaler() + X_scaled = scaler.fit_transform(X).astype(np.float32, copy=False) + update_progress(40) + current_ram = process.memory_info().rss + ram_peak = max(ram_peak, current_ram) + + model = IsolationForest( + n_estimators=600, + max_samples=0.3, + contamination=contamination, + max_features=0.7, + bootstrap=False, + random_state=42, + n_jobs=1, + ) + model.fit(X_scaled) + update_progress(70) + current_ram = process.memory_info().rss + ram_peak = max(ram_peak, current_ram) + + preds = model.predict(X_scaled) + df["pred_label"] = np.where(preds == 1, 0, 1) + df["anomaly_score"] = model.decision_function(X_scaled) + update_progress(85) + current_ram = process.memory_info().rss + ram_peak = max(ram_peak, current_ram) + + cm = confusion_matrix(true_labels, df["pred_label"]) + accuracy = accuracy_score(true_labels, df["pred_label"]) + precision, recall, f1, _ = precision_recall_fscore_support( + true_labels, df["pred_label"], average=None, labels=[0, 1] + ) + update_progress(95) + current_ram = process.memory_info().rss + ram_peak = max(ram_peak, current_ram) + + if table: + table_data = [ + ["Normal (0)", f"{precision[0]:.4f}", f"{recall[0]:.4f}", f"{f1[0]:.4f}"], + ["Attack (1)", f"{precision[1]:.4f}", f"{recall[1]:.4f}", f"{f1[1]:.4f}"], + ["Overall Accuracy", "-", "-", f"{accuracy:.4f}"], + ] + print( + tabulate( + table_data, + headers=["Class", "Precision", "Recall", "F1-Score"], + tablefmt="fancy_grid", + ) + ) + + if plot: + plt.figure(figsize=(10, 5)) + scatter = plt.scatter( + range(len(df)), + df["anomaly_score"], + c=df["pred_label"], + cmap="coolwarm", + s=10, + ) + plt.xlabel("Instance") + plt.ylabel("Anomaly Score") + plt.title("Anomaly Score Distribution (Isolation Forest)") + handles, labels = scatter.legend_elements() + plt.legend(handles, ["Normal", "Anomaly"], title="Predicted") + plt.show() + + plt.figure(figsize=(5, 4)) + sns.heatmap( + cm, + annot=True, + fmt="d", + cmap="Blues", + xticklabels=["Normal", "Attack"], + yticklabels=["Normal", "Attack"], + ) + plt.xlabel("Predicted Label") + plt.ylabel("True Label") + plt.title("Confusion Matrix (Isolation Forest)") + plt.show() + + results = { + "normal_count": float(normal_count), + "attack_count": float(attack_count), + "contamination": float(contamination), + "accuracy": float(accuracy), + "precision_normal": float(precision[0]), + "recall_normal": float(recall[0]), + "f1_normal": float(f1[0]), + "precision_attack": float(precision[1]), + "recall_attack": float(recall[1]), + "f1_attack": float(f1[1]), + } + + + + candidates = df[(df["pred_label"] == 1) & (df["fraud"] == 1)].copy() + + + if len(candidates) < 5: + extra = df[df["pred_label"] == 1].copy() + candidates = pd.concat([candidates, extra]).drop_duplicates() + + if len(candidates) < 5: + candidates = df.copy() + + candidates = candidates.sort_values("anomaly_score").head(5) + + important_cols = [ + "Attack Type", + "Destination Port", + "Flow Duration", + "Total Fwd Packets", + "Flow Packets/s", + "Packet Length Mean", + ] + + cols_exist = [c for c in important_cols if c in candidates.columns] + + top_anomalies = candidates[cols_exist + ["anomaly_score"]].rename( + columns={"anomaly_score": "score"} + ).to_dict(orient="records") + + results["top_anomalies"] = top_anomalies + + current_ram = process.memory_info().rss + ram_peak = max(ram_peak, current_ram) + + + del X, X_scaled, preds, model, scaler, candidates, df, cm, true_labels + _force_memory_cleanup() + + ram_after = process.memory_info().rss + + results["ram_before"] = round(ram_before / (1024 ** 2), 2) + results["ram_peak"] = round(ram_peak / (1024 ** 2), 2) + results["ram_after"] = round(ram_after / (1024 ** 2), 2) + results["ram_increase"] = round((ram_peak - ram_before) / (1024 ** 2), 2) + + return results + + +if __name__ == "__main__": + res = run_isolation_forest(plot=True, table=True) + print(res) diff --git a/Backend/isolation_forest_custom.py b/Backend/isolation_forest_custom.py new file mode 100644 index 0000000..0bae38b --- /dev/null +++ b/Backend/isolation_forest_custom.py @@ -0,0 +1,175 @@ +# isolation_forest_custom.py +import json +import pandas as pd +import numpy as np +import psutil + +from sklearn.ensemble import IsolationForest +from sklearn.preprocessing import RobustScaler +from sklearn.metrics import accuracy_score, precision_recall_fscore_support + +import os + +import gc + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +TEMP_DIR = os.path.join(BASE_DIR, "temp") + +def _force_memory_cleanup(): + gc.collect() + try: + import ctypes + ctypes.CDLL("libc.so.6").malloc_trim(0) + except Exception: + pass + +def update_progress(value, progress_path="progress.json"): + with open(progress_path, "w") as f: + json.dump({"progress": value}, f) + + +def run_isolation_forest_custom( + csv_path=None, + config_path=None, + progress_path=None +): + if csv_path is None: + csv_path = os.path.join(TEMP_DIR, "upload.csv") + if config_path is None: + config_path = os.path.join(TEMP_DIR, "config.json") + if progress_path is None: + progress_path = os.path.join(BASE_DIR, "progress.json") + + update_progress(1, progress_path) + process = psutil.Process() + ram_before = process.memory_info().rss + ram_peak = ram_before + + update_progress(5, progress_path) + + with open(config_path, "r", encoding="utf-8") as f: + config = json.load(f) + + label_col = config["labeling"]["label_column"] + normal_value = config["labeling"]["normal_value"] + features = config["features"]["selected_columns"] + + params = config.get("algorithm", {}).get("parameters", {}) + + needed_cols = list(dict.fromkeys(features + [label_col])) + df = pd.read_csv(csv_path, usecols=needed_cols) + update_progress(15, progress_path) + + current_ram = process.memory_info().rss + ram_peak = max(ram_peak, current_ram) + + + col = df[label_col] + + + df["__label"] = col.apply(lambda x: 0 if x == normal_value else 1) + + update_progress(25, progress_path) + current_ram = process.memory_info().rss + ram_peak = max(ram_peak, current_ram) + + normal_count = int((df["__label"] == 0).sum()) + attack_count = int((df["__label"] == 1).sum()) + + X = df[features].to_numpy(dtype=np.float32, copy=True) + y = df["__label"].to_numpy(dtype=np.int8, copy=True) + + scaler = RobustScaler() + X_scaled = scaler.fit_transform(X).astype(np.float32, copy=False) + + + contamination = min(y.mean() * 2.5, 0.49) + + update_progress(45, progress_path) + current_ram = process.memory_info().rss + ram_peak = max(ram_peak, current_ram) + + + model = IsolationForest( + n_estimators=params.get("n_estimators", 600), + max_samples=params.get("max_samples", 0.3), + max_features=params.get("max_features", 0.7), + contamination=contamination, + random_state=params.get("random_state", 42), + n_jobs=1 + ) + + model.fit(X_scaled) + update_progress(75, progress_path) + + current_ram = process.memory_info().rss + ram_peak = max(ram_peak, current_ram) + + + preds = model.predict(X_scaled) + y_pred = np.where(preds == 1, 0, 1) + + accuracy = accuracy_score(y, y_pred) + precision, recall, f1, _ = precision_recall_fscore_support( + y, y_pred, average=None, labels=[0, 1] + ) + + update_progress(85, progress_path) + + + results = { + "normal_count": float(normal_count), + "attack_count": float(attack_count), + "contamination": float(contamination), + "accuracy": float(accuracy), + "precision_normal": float(precision[0]), + "recall_normal": float(recall[0]), + "f1_normal": float(f1[0]), + "precision_attack": float(precision[1]), + "recall_attack": float(recall[1]), + "f1_attack": float(f1[1]), + } + + anomaly_scores = -model.decision_function(X_scaled) + + display_cols = list(dict.fromkeys(features + [label_col])) + top_df = df[display_cols].copy() + top_df["anomaly_score"] = anomaly_scores + top_df["predicted"] = y_pred + top_df["true_label"] = y + + real_attacks = top_df[top_df["true_label"] == 1] + real_attacks_sorted = real_attacks.sort_values("anomaly_score", ascending=False) + top_real_attacks = real_attacks_sorted.head(5) + + if len(top_real_attacks) > 0: + cols = [c for c in display_cols if c in top_real_attacks.columns] + ["anomaly_score"] + top_anomalies = ( + top_real_attacks[cols] + .rename(columns={"anomaly_score": "score"}) + .round(4) + .to_dict(orient="records") + ) + else: + top_anomalies = [] + + results["top_anomalies"] = top_anomalies + + del X, X_scaled, preds, y_pred, anomaly_scores + del top_df, real_attacks, real_attacks_sorted, top_real_attacks + del model, scaler, df, y, col + _force_memory_cleanup() + + ram_after = process.memory_info().rss + + results["ram_before"] = round(ram_before / (1024 ** 2), 2) + results["ram_peak"] = round(ram_peak / (1024 ** 2), 2) + results["ram_after"] = round(ram_after / (1024 ** 2), 2) + results["ram_increase"] = round((ram_peak - ram_before) / (1024 ** 2), 2) + + return results + + +if __name__ == "__main__": + res = run_isolation_forest_custom() + print(json.dumps(res, indent=2)) \ No newline at end of file diff --git a/Backend/progress.json b/Backend/progress.json new file mode 100644 index 0000000..2cc70c8 --- /dev/null +++ b/Backend/progress.json @@ -0,0 +1 @@ +{"progress": 100} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..41a2073 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.11-slim-bullseye + +WORKDIR /app + +ENV PYTHONUNBUFFERED=1 +ENV PYTHONDONTWRITEBYTECODE=1 + +COPY requirements.txt . +RUN pip install --no-compile --no-cache-dir -r requirements.txt + +COPY . /app + +EXPOSE 5000 + +CMD ["python", "./Backend/app.py"] \ No newline at end of file diff --git a/Frontend/HTML Files/default.html b/Frontend/HTML Files/default.html new file mode 100644 index 0000000..1dc276b --- /dev/null +++ b/Frontend/HTML Files/default.html @@ -0,0 +1,136 @@ + + + + + + Demo demonstration! + + + + + + + + + + +
+
+ +

CICIDS Demo

+ +

+ Here, you can test the algoritmhs on + CICIDS dataset. +

+ +

+ Choose your desired algorithm: +

+ +
+ + + + + +
+ +
+ +
+ + + +
+
+ + + + + + + + diff --git a/Frontend/HTML Files/index.html b/Frontend/HTML Files/index.html new file mode 100644 index 0000000..1b963c6 --- /dev/null +++ b/Frontend/HTML Files/index.html @@ -0,0 +1,72 @@ + + + + + Welcome to this testing demo website! + + + + + + + +
+
+

Welcome!

+

+ This site serves as a demonstration of my Bachelor's Thesis project focused on + Intrusion Detection Systems (IDS). + It showcases the testing and comparison of Isolation Forest and + Autoencoder algorithms on the popular + CICIDS dataset. +

+

What is this project about?

+

+ The goal of this demo is to demonstrate how machine learning models can be used to: +

+
    +
  • Detect anomalies in network traffic
  • +
  • Identify potential cybersecurity threats
  • +
  • Process and analyze datasets in real time
  • +
  • Provide a modern and clear visual output
  • +
+

What algorithms are included?

+
    +
  • Isolation Forest – anomaly detection based on random isolation
  • +
  • Autoencoder – neural network that reconstructs normal traffic and flags anomalies
  • +
+

What can you do here?

+
    +
  • Upload your own dataset and test the algorithms
  • +
  • Run default tests on the CICIDS dataset
  • +
+

+ This demo is for educational and research purposes only! +

+
+
+ + + + + diff --git a/Frontend/HTML Files/upload.html b/Frontend/HTML Files/upload.html new file mode 100644 index 0000000..55a9569 --- /dev/null +++ b/Frontend/HTML Files/upload.html @@ -0,0 +1,264 @@ + + + + + Upload your files in order to begin testing! + + + + + + + + + + + + + +
+ +
+ + +
+

+ Upload your custom dataset here! +

+ +

+ Here, you can upload your own dataset and test it out on either + Isolation Forest or + Autoencoder. +

+ +

+ Please note, you can only use .CSV format when uploading. +

+ +
+ +
+
+ + + + + + + + + + + + + +
+
+ + +
+

© 2025 Michal Utlak

+
+ + + + + + + + + \ No newline at end of file diff --git a/Frontend/JS/frontend.js b/Frontend/JS/frontend.js new file mode 100644 index 0000000..fde0217 --- /dev/null +++ b/Frontend/JS/frontend.js @@ -0,0 +1,467 @@ +const btnIF = document.getElementById("btnIF"); +const btnAE = document.getElementById("btnAE"); +const startBtn = document.getElementById("startBtn"); + +let chosenAlgorithm = null; +let chosenAlgorithmName = ""; +let startTime = null; +let lastResults = null; +let progressInterval = null; +let backendErrorShown = false; + +const JOB_STORAGE_KEY = "default_active_job"; + +function selectAlgorithm(selected, other) { + selected.classList.add("scale-105", "border-2", "border-black"); + selected.classList.remove("opacity-50"); + + other.classList.remove("scale-105", "border-2", "border-black"); + other.classList.add("opacity-50"); +} + +function enableStartButton() { + startBtn.classList.remove("bg-gray-700", "text-gray-300", "cursor-not-allowed"); + startBtn.classList.add("bg-orange-500", "hover:bg-orange-400", "text-white", "cursor-pointer"); +} + +btnIF.addEventListener("click", () => { + selectAlgorithm(btnIF, btnAE); + chosenAlgorithm = "isolation_forest"; + chosenAlgorithmName = "Isolation Forest"; + enableStartButton(); +}); + +btnAE.addEventListener("click", () => { + selectAlgorithm(btnAE, btnIF); + chosenAlgorithm = "autoencoder"; + chosenAlgorithmName = "Autoencoder"; + enableStartButton(); +}); + +function getBackendJobName() { + return chosenAlgorithm === "isolation_forest" ? "iforest" : "autoencoder"; +} + +function getResultURL() { + return chosenAlgorithm === "isolation_forest" + ? "/get-isolation-forest-result" + : "/get-autoencoder-result"; +} + +function saveActiveJob() { + sessionStorage.setItem(JOB_STORAGE_KEY, JSON.stringify({ + algorithm: chosenAlgorithm, + algorithmName: chosenAlgorithmName, + backendJob: getBackendJobName(), + startedAt: Date.now() + })); +} + +function loadActiveJob() { + const raw = sessionStorage.getItem(JOB_STORAGE_KEY); + if (!raw) return null; + + try { + return JSON.parse(raw); + } catch { + sessionStorage.removeItem(JOB_STORAGE_KEY); + return null; + } +} + +function clearActiveJob() { + sessionStorage.removeItem(JOB_STORAGE_KEY); +} + +function applySavedAlgorithm(saved) { + chosenAlgorithm = saved.algorithm; + chosenAlgorithmName = saved.algorithmName; + startTime = saved.startedAt; + + if (chosenAlgorithm === "isolation_forest") { + selectAlgorithm(btnIF, btnAE); + } else if (chosenAlgorithm === "autoencoder") { + selectAlgorithm(btnAE, btnIF); + } + + enableStartButton(); +} + +function updateProgressUI(progress) { + document.getElementById("progressContainer").classList.remove("hidden"); + document.getElementById("progressBar").style.width = progress + "%"; + document.getElementById("progressLabel").textContent = progress + "%"; +} + +function showLoading(customText = null) { + document.getElementById("loadingOverlay").classList.remove("hidden"); + + document.getElementById("progressBar").style.width = "0%"; + document.getElementById("progressLabel").textContent = "0%"; + document.getElementById("progressContainer").classList.add("hidden"); + + const loadingText = document.querySelector("#loadingOverlay p"); + if (loadingText) { + loadingText.innerHTML = customText ?? `Starting ${chosenAlgorithmName}...
This may take a while.`; + } +} + +function hideLoading() { + document.getElementById("loadingOverlay").classList.add("hidden"); +} + +function showErrorPopup(message) { + const popup = document.createElement("div"); + + popup.innerHTML = ` +
+
+

Error

+

${message}

+ +
+
+ `; + + document.body.appendChild(popup); + document.getElementById("closeError").onclick = () => popup.remove(); +} + +function fetchFinalResults(retryCount = 0) { + fetch(getResultURL()) + .then(res => res.json()) + .then(json => { + if (!json.ready) { + if (retryCount < 10) { + setTimeout(() => fetchFinalResults(retryCount + 1), 1000); + return; + } + hideLoading(); + clearActiveJob(); + showErrorPopup("Final results are not ready."); + return; + } + + hideLoading(); + clearActiveJob(); + lastResults = json.data; + + if (json.data?.error) { + showErrorPopup(`Algorithm failed.
${json.data.message}`); + return; + } + + displayResults(json.data); + }) + .catch(err => { + hideLoading(); + clearActiveJob(); + showErrorPopup("Error fetching final results."); + }); +} + +function startProgressPolling() { + if (progressInterval) { + clearInterval(progressInterval); + } + + document.getElementById("progressContainer").classList.remove("hidden"); + + progressInterval = setInterval(async () => { + try { + const res = await fetch("/get-status"); + + if (!res.ok) { + throw new Error("Backend status fetch failed"); + } + + const data = await res.json(); + + updateProgressUI(data.progress || 0); + + if (!data.running) { + clearInterval(progressInterval); + progressInterval = null; + fetchFinalResults(); + } + } catch (err) { + clearInterval(progressInterval); + progressInterval = null; + hideLoading(); + clearActiveJob(); + + if (!backendErrorShown) { + backendErrorShown = true; + showErrorPopup("Connection to backend was lost.
Backend may have stopped."); + } + } + }, 1500); +} + +async function restoreRunningJob() { + const saved = loadActiveJob(); + if (!saved) return; + + try { + const res = await fetch("/get-status"); + const status = await res.json(); + + if (status.current_job !== saved.backendJob) { + clearActiveJob(); + return; + } + + applySavedAlgorithm(saved); + showLoading(`Resuming ${chosenAlgorithmName}...
This may take a while.`); + updateProgressUI(status.progress || 0); + + if (status.running) { + startProgressPolling(); + } else { + fetchFinalResults(); + } + } catch (err) { + hideLoading(); + clearActiveJob(); + + if (!backendErrorShown) { + backendErrorShown = true; + showErrorPopup("Connection to backend was lost.
Please start the backend and try again."); + } +} +} + +startBtn.addEventListener("click", () => { + if (!chosenAlgorithm) return; + + showLoading(); + backendErrorShown = false; + saveActiveJob(); + + let url = + chosenAlgorithm === "isolation_forest" + ? "/start-iforest" + : "/start-autoencoder"; + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 10000); + + fetch(url, { method: "POST", signal: controller.signal }) + .then(async res => { + clearTimeout(timeout); + + if (res.status === 409) { + hideLoading(); + clearActiveJob(); + showErrorPopup("Another algorithm is already running.
Please wait until it finishes."); + return; + } + + if (!res.ok) { + throw new Error("Server error"); + } + + const saved = loadActiveJob(); + startTime = saved?.startedAt ?? Date.now(); + startProgressPolling(); + }) + .catch(err => { + hideLoading(); + clearActiveJob(); + console.error(err); + showErrorPopup("Backend server is not running.
Start app.py and try again."); + }); +}); + +window.addEventListener("load", restoreRunningJob); + +function displayResults(data) { + const container = document.getElementById("resultContainer"); + container.classList.remove("hidden"); + + const runtime = ((Date.now() - startTime) / 1000).toFixed(2); + + // Dynamicky vytvoríme tabuľku pre Top 5 anomálie (ak existujú) + let topAnomaliesHTML = ''; + if (data.top_anomalies && data.top_anomalies.length > 0) { + const headers = Object.keys(data.top_anomalies[0]); + + topAnomaliesHTML = ` +
+

+ Top 5 detected anomalies +

+
+ + + + ${headers.map(h => ``).join('')} + + + + ${data.top_anomalies.map(row => ` + + ${headers.map(key => { + const val = row[key]; + return ``; + }).join('')} + `).join('')} + +
${h}
${typeof val === 'number' ? val.toFixed(4) : val}
+
+
`; + } + + container.innerHTML = ` +

+ Results +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ClassPrecisionRecallF1-Score
Normal traffic${data.precision_normal.toFixed(4)}${data.recall_normal.toFixed(4)}${data.f1_normal.toFixed(4)}
Anomaly${data.precision_attack.toFixed(4)}${data.recall_attack.toFixed(4)}${data.f1_attack.toFixed(4)}
Overall accuracy--${data.accuracy.toFixed(4)}
+
+ + +
+ +
+

Class distribution

+ +
+ + +
+

Performance

+
+
Normal traffic: ${Math.round(data.normal_count)}
+
Anomalies: ${Math.round(data.attack_count)}
+
Runtime: ${runtime} s
+
RAM before: ${data.ram_before} MB
+
RAM peak: ${data.ram_peak} MB
+
RAM after: ${data.ram_after} MB
+
RAM increase: +${data.ram_increase} MB
+
+
+
+ + ${topAnomaliesHTML} + + +
+ + +
+ `; + + loadPieChart(data); + + document.getElementById("downloadPDF").addEventListener("click", () => { + downloadPDF(data, runtime); + }); + document.getElementById("downloadJSON").addEventListener("click", () => { + downloadJSON(data, runtime); + }); +} + +function downloadPDF(results, runtime) { + fetch("/download-pdf", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + algorithm: chosenAlgorithmName, + results: results, + runtime: runtime + }) + }) + .then(response => response.blob()) + .then(blob => { + const url = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = "report.pdf"; + link.click(); + }); +} + +function downloadJSON(results, runtime) { + fetch("/download-json", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + algorithm: chosenAlgorithmName, + results: results, + runtime: runtime + }) + }) + .then(res => res.blob()) + .then(blob => { + const url = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = "report.json"; + link.click(); + }); +} + +function loadPieChart(data) { + const ctx = document.getElementById("pieChart"); + + new Chart(ctx, { + type: "pie", + data: { + labels: ["Normal Traffic", "Attacks"], + datasets: [{ + data: [data.normal_count || 0, data.attack_count || 0], + backgroundColor: ["#14b8a6", "#dc2626"], + }] + }, + options: { + plugins: { + legend: { + labels: { + color: "#ffffff" + } + } + } + } + }); +} \ No newline at end of file diff --git a/Frontend/JS/menu.js b/Frontend/JS/menu.js new file mode 100644 index 0000000..9865e1d --- /dev/null +++ b/Frontend/JS/menu.js @@ -0,0 +1,20 @@ +const btn = document.getElementById("menuBtn"); +const panel = document.getElementById("menuPanel"); + +btn.addEventListener("click", () => { + if (panel.classList.contains("hidden")) { + panel.classList.remove("hidden"); + setTimeout(() => { + panel.classList.remove("opacity-0", "translate-y-3"); + panel.classList.add("opacity-100", "translate-y-0"); + btn.innerText = "Menu ▾"; + }, 10); + } else { + panel.classList.add("opacity-0", "translate-y-3"); + panel.classList.remove("opacity-100", "translate-y-0"); + btn.innerText = "Menu ▸"; + setTimeout(() => { + panel.classList.add("hidden"); + }, 250); + } +}); diff --git a/Frontend/JS/upload.js b/Frontend/JS/upload.js new file mode 100644 index 0000000..d4ef782 --- /dev/null +++ b/Frontend/JS/upload.js @@ -0,0 +1,271 @@ + +function show(el) { el.classList.remove("hidden"); } +function hide(el) { el.classList.add("hidden"); } + +function escapeHtml(str) { + return String(str) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} + +function splitCsvLine(line) { + const out = []; + let cur = ""; + let inQuotes = false; + + for (let i = 0; i < line.length; i++) { + const ch = line[i]; + if (ch === '"') { + if (inQuotes && line[i + 1] === '"') { + cur += '"'; + i++; + } else { + inQuotes = !inQuotes; + } + } else if (ch === "," && !inQuotes) { + out.push(cur); + cur = ""; + } else { + cur += ch; + } + } + out.push(cur); + return out.map(v => v.trim()); +} + +const fileInput = document.getElementById("file_input"); + +const step2 = document.getElementById("step-2"); +const step3 = document.getElementById("step-3"); +const step4 = document.getElementById("step-4"); +const step5 = document.getElementById("step-5"); + +const labelSelect = document.getElementById("label_select"); +const labelError = document.getElementById("label_error"); + +const normalBox = document.getElementById("normal_values"); +const normalError = document.getElementById("normal_error"); + +const featureList = document.getElementById("feature_list"); +const featureCounter = document.getElementById("feature_counter"); +const featureError = document.getElementById("feature_error"); + +const btnStep2Next = document.getElementById("btn_step2_next"); +const btnStep3Next = document.getElementById("btn_step3_next"); +const btnRun = document.getElementById("btn_run"); + +const MAX_FEATURES = 5; + + +let headers = []; +let rows = []; +let labelIndex = -1; +let numericColumnIdx = []; + +fileInput.addEventListener("change", async () => { + + const formData = new FormData(); +formData.append("file", fileInput.files[0]); + +await fetch("/upload-dataset", { + method: "POST", + body: formData +}); + + if (!fileInput.files.length) return; + + const file = fileInput.files[0]; + const text = await file.text(); + + const lines = text.split(/\r?\n/).filter(l => l.trim() !== ""); + if (lines.length < 2) return; + + headers = splitCsvLine(lines[0]); + rows = lines.slice(1).map(splitCsvLine); + + labelSelect.innerHTML = ``; + headers.forEach((h, idx) => { + const opt = document.createElement("option"); + opt.value = idx; + opt.textContent = h; + labelSelect.appendChild(opt); + }); + + numericColumnIdx = detectNumericColumns(headers, rows, 30); + + show(step2); + hide(step3); + hide(step4); + hide(step5); + hide(labelError); +}); + +function detectNumericColumns(headers, rows, sampleCount) { + const result = []; + const samples = Math.min(sampleCount, rows.length); + + for (let c = 0; c < headers.length; c++) { + let valid = 0; + let total = 0; + + for (let r = 0; r < samples; r++) { + const v = rows[r][c]; + if (v === undefined || v === "") continue; + total++; + if (!isNaN(Number(v))) valid++; + } + + if (total > 0 && valid / total >= 0.9) { + result.push(c); + } + } + return result; +} + +btnStep2Next.addEventListener("click", () => { + if (labelSelect.value === "") { + show(labelError); + return; + } + + hide(labelError); + labelIndex = Number(labelSelect.value); + + const uniques = new Set(); + rows.forEach(r => { + const v = r[labelIndex]; + if (v !== undefined && v !== "") uniques.add(v.trim()); + }); + + normalBox.innerHTML = ""; + uniques.forEach(val => { + const label = document.createElement("label"); + label.className = + "flex items-center gap-3 bg-gray-900/40 border border-gray-700 rounded-lg px-4 py-2"; + + label.innerHTML = ` + + ${escapeHtml(val)} + `; + normalBox.appendChild(label); + }); + + normalBox.querySelectorAll("input").forEach(cb => { + cb.addEventListener("change", () => { + if (cb.checked) { + normalBox.querySelectorAll("input").forEach(o => { + if (o !== cb) o.checked = false; + }); + } + }); + }); + + show(step3); + hide(step4); + hide(step5); + hide(normalError); +}); + +btnStep3Next.addEventListener("click", () => { + const checked = normalBox.querySelectorAll("input:checked"); + if (checked.length !== 1) { + normalError.textContent = "Select exactly one NORMAL value."; + show(normalError); + return; + } + + hide(normalError); + + featureList.innerHTML = ""; + numericColumnIdx + .filter(idx => idx !== labelIndex) + .forEach(idx => { + const label = document.createElement("label"); + label.className = + "flex items-center justify-between bg-gray-900/40 border border-gray-700 rounded-lg px-4 py-2"; + + label.innerHTML = ` +
+ + ${escapeHtml(headers[idx])} +
+ numeric + `; + featureList.appendChild(label); + }); + + featureList.querySelectorAll("input").forEach(cb => + cb.addEventListener("change", updateCounter) + ); + + updateCounter(); + show(step4); + hide(featureError); +}); + +function updateCounter() { + const all = featureList.querySelectorAll("input"); + const selected = featureList.querySelectorAll("input:checked").length; + + featureCounter.textContent = `${selected} / ${MAX_FEATURES} selected`; + + all.forEach(cb => { + cb.disabled = selected >= MAX_FEATURES && !cb.checked; + cb.classList.toggle("opacity-60", cb.disabled); + }); +} + +btnRun.addEventListener("click", async () => { + + const labelColumnName = headers[labelIndex]; + let normalValue = normalBox.querySelector("input:checked").value; + + if (!isNaN(normalValue)) { + normalValue = Number(normalValue); + } + + const selectedFeatures = Array.from( + featureList.querySelectorAll("input:checked") + ).map(cb => headers[cb.dataset.col]); + + const config = { + dataset: { + file_path: "temp/upload.csv", + dataset_name: "custom_user_dataset" + }, + labeling: { + label_column: labelColumnName, + normal_value: normalValue, + normal_label: 0, + anomaly_label: 1 + }, + features: { + selected_columns: selectedFeatures, + expected_feature_count: selectedFeatures.length + }, + algorithm: { + name: "isolation_forest" + } + }; + + await fetch("/save-config", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(config) + }); + const selected = featureList.querySelectorAll("input:checked").length; + + if (selected !== MAX_FEATURES) { + featureError.textContent = `You must select exactly ${MAX_FEATURES} numeric features.`; + show(featureError); + return; + } + + hide(featureError); + + show(step5); +}); + diff --git a/Frontend/JS/upload_choosing_fetch.js b/Frontend/JS/upload_choosing_fetch.js new file mode 100644 index 0000000..f2b612f --- /dev/null +++ b/Frontend/JS/upload_choosing_fetch.js @@ -0,0 +1,516 @@ +const btnIF = document.getElementById("btnIF"); +const btnAE = document.getElementById("btnAE"); +const startBtn = document.getElementById("startBtn"); + +let chosenAlgorithm = null; +let chosenAlgorithmName = null; +let startTime = null; +let lastResults = null; +let progressInterval = null; +let backendErrorShown = false; + +const JOB_STORAGE_KEY = "custom_active_job"; + +function selectAlgorithm(selected, other) { + selected.classList.add("scale-105", "border-2", "border-black"); + selected.classList.remove("opacity-50"); + + other.classList.remove("scale-105", "border-2", "border-black"); + other.classList.add("opacity-50"); +} + +function enableStartButton() { + startBtn.classList.remove("bg-gray-600", "text-gray-300", "cursor-not-allowed"); + startBtn.classList.add("bg-orange-500", "hover:bg-orange-400", "text-white", "cursor-pointer"); +} + +btnIF.addEventListener("click", () => { + selectAlgorithm(btnIF, btnAE); + chosenAlgorithm = "iforest_custom"; + chosenAlgorithmName = "Isolation Forest (Custom)"; + enableStartButton(); +}); + +btnAE.addEventListener("click", () => { + selectAlgorithm(btnAE, btnIF); + chosenAlgorithm = "autoencoder_custom"; + chosenAlgorithmName = "Autoencoder (Custom)"; + enableStartButton(); +}); + +function getBackendJobName() { + return chosenAlgorithm; +} + +function getResultURL() { + if (chosenAlgorithm === "iforest_custom") { + return "/get-isolation-forest-result-custom"; + } + return "/get-autoencoder-result-custom"; +} + +function saveActiveJob() { + sessionStorage.setItem(JOB_STORAGE_KEY, JSON.stringify({ + algorithm: chosenAlgorithm, + algorithmName: chosenAlgorithmName, + backendJob: getBackendJobName(), + startedAt: Date.now() + })); +} + +function loadActiveJob() { + const raw = sessionStorage.getItem(JOB_STORAGE_KEY); + if (!raw) return null; + + try { + return JSON.parse(raw); + } catch { + sessionStorage.removeItem(JOB_STORAGE_KEY); + return null; + } +} + +function clearActiveJob() { + sessionStorage.removeItem(JOB_STORAGE_KEY); +} + +function applySavedAlgorithm(saved) { + chosenAlgorithm = saved.algorithm; + chosenAlgorithmName = saved.algorithmName; + startTime = saved.startedAt; + + if (chosenAlgorithm === "iforest_custom") { + selectAlgorithm(btnIF, btnAE); + } else if (chosenAlgorithm === "autoencoder_custom") { + selectAlgorithm(btnAE, btnIF); + } + + enableStartButton(); +} + +function restoreCustomRunLayout() { + const step1 = document.getElementById("step-1"); + const step2 = document.getElementById("step-2"); + const step3 = document.getElementById("step-3"); + const step4 = document.getElementById("step-4"); + const step5 = document.getElementById("step-5"); + + if (step1) step1.classList.add("hidden"); + if (step2) step2.classList.add("hidden"); + if (step3) step3.classList.add("hidden"); + if (step4) step4.classList.add("hidden"); + if (step5) step5.classList.remove("hidden"); +} + +function updateProgressUI(progress) { + document.getElementById("progressContainer").classList.remove("hidden"); + document.getElementById("progressBar").style.width = progress + "%"; + document.getElementById("progressLabel").textContent = progress + "%"; +} + +function showLoading(customText = null) { + document.getElementById("loadingOverlay").classList.remove("hidden"); + + document.getElementById("progressBar").style.width = "0%"; + document.getElementById("progressLabel").textContent = "0%"; + document.getElementById("progressContainer").classList.add("hidden"); + + const loadingText = document.getElementById("loadingText"); + if (loadingText) { + loadingText.innerHTML = customText ?? `Starting ${chosenAlgorithmName}...
This may take a while.`; + } +} + +function hideLoading() { + document.getElementById("loadingOverlay").classList.add("hidden"); +} + +function showErrorPopup(message) { + const popup = document.createElement("div"); + + popup.innerHTML = ` +
+
+

Error

+

${message}

+ +
+
+ `; + + document.body.appendChild(popup); + document.getElementById("closeError").onclick = () => popup.remove(); +} + +const originalFetch = window.fetch.bind(window); + +window.fetch = async function (...args) { + const res = await originalFetch(...args); + + let url = ""; + const req = args[0]; + + if (typeof req === "string") { + url = req; + } else if (req && typeof req.url === "string") { + url = req.url; + } + + if (res.status === 409) { + let message = "Another operation is already running."; + + try { + const data = await res.clone().json(); + if (data?.error) { + message = data.error; + } else if (data?.message) { + message = data.message; + } + } catch (_) {} + + if (url.includes("/upload-dataset")) { + showErrorPopup(message || "Cannot upload a new dataset while an algorithm is running."); + } else if (url.includes("/save-config")) { + showErrorPopup(message || "Cannot save config while an algorithm is running."); + } + } + + return res; +}; + +function fetchFinalResults(retryCount = 0) { + fetch(getResultURL()) + .then(res => res.json()) + .then(json => { + if (!json.ready) { + if (retryCount < 10) { + setTimeout(() => fetchFinalResults(retryCount + 1), 1000); + return; + } + hideLoading(); + clearActiveJob(); + showErrorPopup("Final results are not ready."); + return; + } + + hideLoading(); + clearActiveJob(); + restoreCustomRunLayout(); + lastResults = json.data; + + if (json.data?.error) { + showErrorPopup(`Algorithm failed.
${json.data.message}`); + return; + } + + displayResults(json.data); + }) + .catch(() => { + hideLoading(); + clearActiveJob(); + showErrorPopup("Error fetching final results."); + }); +} + +function startProgressPolling() { + if (progressInterval) { + clearInterval(progressInterval); + } + + document.getElementById("progressContainer").classList.remove("hidden"); + + progressInterval = setInterval(async () => { + try { + const res = await fetch("/get-status"); + + if (!res.ok) { + throw new Error("Backend status fetch failed"); + } + + const data = await res.json(); + + updateProgressUI(data.progress || 0); + + if (!data.running) { + clearInterval(progressInterval); + progressInterval = null; + fetchFinalResults(); + } + } catch (err) { + clearInterval(progressInterval); + progressInterval = null; + hideLoading(); + clearActiveJob(); + + if (!backendErrorShown) { + backendErrorShown = true; + showErrorPopup("Connection to backend was lost.
Backend may have stopped."); + } + } + }, 1500); +} + +async function restoreRunningJob() { + const saved = loadActiveJob(); + if (!saved) return; + + try { + const res = await fetch("/get-status"); + const status = await res.json(); + + if (status.current_job !== saved.backendJob) { + clearActiveJob(); + return; + } + + applySavedAlgorithm(saved); + restoreCustomRunLayout(); + showLoading(`Resuming ${chosenAlgorithmName}...
This may take a while.`); + updateProgressUI(status.progress || 0); + + if (status.running) { + startProgressPolling(); + } else { + fetchFinalResults(); + } + } catch (err) { + hideLoading(); + clearActiveJob(); + + if (!backendErrorShown) { + backendErrorShown = true; + showErrorPopup("Connection to backend was lost.
Please start the backend and try again."); + } + } +} + +startBtn.addEventListener("click", () => { + if (!chosenAlgorithm) return; + + showLoading(); + backendErrorShown = false; + saveActiveJob(); + + let url; + if (chosenAlgorithm === "iforest_custom") { + url = "/start-iforest-custom"; + } else if (chosenAlgorithm === "autoencoder_custom") { + url = "/start-autoencoder-custom"; + } + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 60000); + + fetch(url, { method: "POST", signal: controller.signal }) + .then(async res => { + clearTimeout(timeout); + + if (res.status === 409) { + hideLoading(); + clearActiveJob(); + showErrorPopup("Another algorithm is already running.
Please wait until it finishes."); + return; + } + + if (!res.ok) { + throw new Error("Server error"); + } + + const saved = loadActiveJob(); + startTime = saved?.startedAt ?? Date.now(); + startProgressPolling(); + }) + .catch(err => { + hideLoading(); + clearActiveJob(); + console.error(err); + showErrorPopup("Backend server is not running.
Start app.py and try again."); + }); +}); + +window.addEventListener("load", restoreRunningJob); + +function displayResults(data) { + const container = document.getElementById("resultContainer"); + container.classList.remove("hidden"); + + const runtime = ((Date.now() - startTime) / 1000).toFixed(2); + + let topAnomaliesHTML = ''; + if (data.top_anomalies && data.top_anomalies.length > 0) { + const headers = Object.keys(data.top_anomalies[0]); + + topAnomaliesHTML = ` +
+

+ Top 5 detected anomalies +

+
+ + + + ${headers.map(h => ``).join('')} + + + + ${data.top_anomalies.map(row => ` + + ${headers.map(key => { + const val = row[key]; + return ``; + }).join('')} + `).join('')} + +
${h}
${typeof val === 'number' ? val.toFixed(4) : val}
+
+
`; + } + + container.innerHTML = ` +

+ Results +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ClassPrecisionRecallF1-Score
Normal traffic${data.precision_normal?.toFixed(4) ?? '-'}${data.recall_normal?.toFixed(4) ?? '-'}${data.f1_normal?.toFixed(4) ?? '-'}
Anomaly${data.precision_attack?.toFixed(4) ?? '-'}${data.recall_attack?.toFixed(4) ?? '-'}${data.f1_attack?.toFixed(4) ?? '-'}
Overall accuracy--${data.accuracy?.toFixed(4) ?? '-'}
+
+ +
+
+

Class distribution

+ +
+ +
+

Performance

+
+
Normal traffic: ${Math.round(data.normal_count || 0)}
+
Anomalies: ${Math.round(data.attack_count || 0)}
+
Runtime: ${runtime} s
+
RAM before: ${data.ram_before || 0} MB
+
RAM peak: ${data.ram_peak || 0} MB
+
RAM after: ${data.ram_after || 0} MB
+
RAM increase: +${data.ram_increase || 0} MB
+
+
+
+ + ${topAnomaliesHTML} + +
+ + +
+ `; + + loadPieChart(data); + + document.getElementById("downloadPDF").addEventListener("click", () => { + downloadPDF(data, runtime); + }); + document.getElementById("downloadJSON").addEventListener("click", () => { + downloadJSON(data, runtime); + }); +} + +function downloadPDF(results, runtime) { + fetch("/download-pdf", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + algorithm: chosenAlgorithmName, + results: results, + runtime: runtime + }) + }) + .then(response => response.blob()) + .then(blob => { + const url = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = "report.pdf"; + link.click(); + }); +} + +function downloadJSON(results, runtime) { + fetch("/download-json", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + algorithm: chosenAlgorithmName, + results: results, + runtime: runtime + }) + }) + .then(res => res.blob()) + .then(blob => { + const url = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = "report.json"; + link.click(); + }); +} + +function loadPieChart(data) { + const ctx = document.getElementById("pieChart"); + + new Chart(ctx, { + type: "pie", + data: { + labels: ["Normal Traffic", "Attacks"], + datasets: [{ + data: [data.normal_count || 0, data.attack_count || 0], + backgroundColor: ["#14b8a6", "#dc2626"], + }] + }, + options: { + plugins: { + legend: { + labels: { + color: "#ffffff" + } + } + } + } + }); +} \ No newline at end of file diff --git a/Frontend/img/FEI_logo.svg b/Frontend/img/FEI_logo.svg new file mode 100644 index 0000000..2b3b8f2 --- /dev/null +++ b/Frontend/img/FEI_logo.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Frontend/img/kemt_logo.png b/Frontend/img/kemt_logo.png new file mode 100644 index 0000000..5c90277 Binary files /dev/null and b/Frontend/img/kemt_logo.png differ diff --git a/docker.sh b/docker.sh new file mode 100644 index 0000000..db63cfa --- /dev/null +++ b/docker.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +echo "Staviam kontajner" + +docker build -t aplikacia:dev . + +echo "Spustam kontajner" + +docker run --name utlak-aplikacia -p 5000:5000 aplikacia:dev diff --git a/progress.json b/progress.json new file mode 100644 index 0000000..2cc70c8 --- /dev/null +++ b/progress.json @@ -0,0 +1 @@ +{"progress": 100} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..dbb2dc3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,11 @@ +flask +flask-cors +tensorflow +pandas +numpy +scikit-learn +seaborn +matplotlib +psutil +tabulate +reportlab