import argparse import hashlib import json import os import struct import sys import urllib.request import zipfile from pathlib import Path import numpy as np import pandas as pd from sklearn.model_selection import train_test_split sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from app.engine.ml_classifier import extract_features, extract_features_from_ember_record, FEATURE_NAMES DATASET_DIR = Path(__file__).resolve().parent.parent / "datasets" RAW_DIR = DATASET_DIR / "raw" PROCESSED_DIR = DATASET_DIR / "processed" EMBER_URL = "https://ember.elastic.co/ember_dataset_2024.tar.bz2" BODMAS_INFO_URL = "https://whyisyoung.github.io/BODMAS/" BENIGN_SCAN_DIRS = [ Path(r"C:\Windows\System32"), Path(r"C:\Program Files"), Path(r"C:\Program Files (x86)"), ] PE_EXTENSIONS = {".exe", ".dll", ".sys", ".ocx", ".scr", ".cpl", ".drv"} def ensure_dirs(): for d in [RAW_DIR, PROCESSED_DIR, RAW_DIR / "benign", RAW_DIR / "malicious"]: d.mkdir(parents=True, exist_ok=True) def is_pe_file(filepath: Path) -> bool: try: with open(filepath, "rb") as f: magic = f.read(2) return magic == b"MZ" except (PermissionError, OSError): return False def collect_benign_samples(max_samples: int = 50000) -> list[Path]: print(f"\n[1/4] Collecting benign PE samples from system directories...") collected = [] seen_hashes = set() for scan_dir in BENIGN_SCAN_DIRS: if not scan_dir.exists(): print(f" Skipping {scan_dir} (not found)") continue print(f" Scanning {scan_dir}...") try: for root, dirs, files in os.walk(scan_dir): dirs[:] = [d for d in dirs if d not in {"WinSxS", "Temp", "temp"}] for fname in files: if len(collected) >= max_samples: break fpath = Path(root) / fname if fpath.suffix.lower() not in PE_EXTENSIONS: continue if not is_pe_file(fpath): continue try: file_hash = hashlib.sha256(fpath.read_bytes()).hexdigest() if file_hash in seen_hashes: continue seen_hashes.add(file_hash) collected.append(fpath) except (PermissionError, OSError): continue if len(collected) >= max_samples: break except (PermissionError, OSError): continue print(f" Collected {len(collected)} unique benign PE files") return collected def download_ember(force: bool = False) -> Path | None: ember_dir = RAW_DIR / "ember2024" if ember_dir.exists() and not force and any(ember_dir.iterdir()): print(f"\n[2/4] EMBER2024 already present at {ember_dir}") return ember_dir print(f"\n[2/4] EMBER2024 Dataset Download") print(f" The EMBER2024 dataset (~2GB) must be downloaded from:") print(f" {EMBER_URL}") print(f" ") print(f" To download automatically, run:") print(f" python scripts/prepare_dataset.py --download-ember") print(f" ") print(f" Or manually download and extract to: {ember_dir}") ember_dir.mkdir(parents=True, exist_ok=True) if force: tar_path = RAW_DIR / "ember_dataset_2024.tar.bz2" print(f" Downloading EMBER2024 to {tar_path}...") try: urllib.request.urlretrieve(EMBER_URL, tar_path) print(f" Download complete. Extracting...") import tarfile with tarfile.open(tar_path, "r:bz2") as tar: tar.extractall(path=ember_dir) print(f" Extraction complete.") return ember_dir except Exception as e: print(f" Download failed: {e}") print(f" Please download manually from {EMBER_URL}") return None return None def load_ember2024_from_zip( zip_path: Path, split: str = "train", max_per_week: int = 5000, val_weeks: int = 8, ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray] | None: if not zip_path.exists(): print(f" EMBER2024 zip not found: {zip_path}") return None prefix = f"Win64_{split}/" print(f" Loading EMBER2024 from {zip_path.name} (split={split}, max_per_week={max_per_week})") with zipfile.ZipFile(zip_path) as zf: week_files = sorted(n for n in zf.namelist() if n.startswith(prefix) and n.endswith(".jsonl")) print(f" Found {len(week_files)} weekly files") if split == "train" and val_weeks > 0: train_files = week_files[:-val_weeks] val_files = week_files[-val_weeks:] else: train_files = week_files val_files = [] def _load_weeks(files: list[str], label_str: str) -> tuple[list, list]: X_list, y_list = [], [] for i, fname in enumerate(files): week_X, week_y = [], [] with zf.open(fname) as f: for line in f: record = json.loads(line) lbl = record.get("label", -1) if lbl == -1: continue feat = extract_features_from_ember_record(record) if feat is not None: week_X.append(feat) week_y.append(lbl) if len(week_X) >= max_per_week: break X_list.extend(week_X) y_list.extend(week_y) benign = week_y.count(0) mal = week_y.count(1) print(f" {label_str} week {i+1}/{len(files)}: {len(week_y)} samples (b={benign}, m={mal})") return X_list, y_list print(f" Loading {len(train_files)} train weeks...") X_tr, y_tr = _load_weeks(train_files, "train") X_val, y_val = [], [] if val_files: print(f" Loading {len(val_files)} val weeks...") X_val, y_val = _load_weeks(val_files, "val") if not X_tr: print(" No training samples extracted.") return None X_train = np.array(X_tr, dtype=np.float64) y_train = np.array(y_tr, dtype=np.int64) X_val_arr = np.array(X_val, dtype=np.float64) if X_val else np.empty((0, len(FEATURE_NAMES))) y_val_arr = np.array(y_val, dtype=np.int64) if y_val else np.empty(0, dtype=np.int64) b_tr = int(np.sum(y_train == 0)) m_tr = int(np.sum(y_train == 1)) print(f" Train: {len(y_train)} total (benign={b_tr}, malicious={m_tr})") if len(y_val_arr): b_v = int(np.sum(y_val_arr == 0)) m_v = int(np.sum(y_val_arr == 1)) print(f" Val: {len(y_val_arr)} total (benign={b_v}, malicious={m_v})") return X_train, y_train, X_val_arr, y_val_arr def extract_pe_features(file_paths: list[Path], label: int) -> tuple[list[np.ndarray], list[int]]: X_list = [] y_list = [] skipped = 0 for fpath in file_paths: try: file_bytes = fpath.read_bytes() features = extract_features(file_bytes) if features is not None: X_list.append(features) y_list.append(label) else: skipped += 1 except (PermissionError, OSError, Exception): skipped += 1 print(f" Extracted features from {len(X_list)} files ({skipped} skipped)") return X_list, y_list def generate_synthetic_vibeware(count: int = 5000, seed: int = 42) -> tuple[np.ndarray, np.ndarray]: print(f"\n[3/4] Generating {count} synthetic vibeware feature vectors...") rng = np.random.RandomState(seed) X_list = [] for _ in range(count): variant = rng.choice(["nim", "zig", "rust", "go"]) if variant == "nim": sections = rng.choice([3, 4, 5]) max_entropy = rng.uniform(6.8, 7.6) mean_entropy = rng.uniform(5.2, 6.8) import_count = rng.randint(15, 80) dll_count = rng.randint(3, 12) elif variant == "zig": sections = rng.choice([2, 3, 4]) max_entropy = rng.uniform(6.5, 7.4) mean_entropy = rng.uniform(5.0, 6.5) import_count = rng.randint(10, 50) dll_count = rng.randint(2, 8) elif variant == "rust": sections = rng.choice([4, 5, 6, 7]) max_entropy = rng.uniform(6.2, 7.2) mean_entropy = rng.uniform(4.8, 6.2) import_count = rng.randint(30, 150) dll_count = rng.randint(5, 15) else: sections = rng.choice([3, 4, 5, 6]) max_entropy = rng.uniform(6.0, 7.0) mean_entropy = rng.uniform(4.5, 6.0) import_count = rng.randint(20, 100) dll_count = rng.randint(4, 10) features = np.array([ sections, rng.choice([224, 240]), rng.choice([0x0000, 0x0040, 0x0020, 0x8000]), rng.randint(1000, 50000), rng.randint(50000, 1500000), rng.randint(0, 80000), rng.randint(0x1000, 0x80000), rng.choice([0x00400000, 0x10000000, 0x140000000]), max_entropy, mean_entropy, rng.randint(0, 1024), rng.randint(50000, 3000000), import_count, dll_count, rng.choice([0, 1], p=[0.7, 0.3]), ], dtype=np.float64) X_list.append(features) X = np.array(X_list) y = np.ones(count, dtype=np.int64) print(f" Generated {count} synthetic vibeware samples (Nim/Zig/Rust/Go profiles)") return X, y def split_and_save( X: np.ndarray, y: np.ndarray, output_dir: Path, train_ratio: float = 0.7, val_ratio: float = 0.15, test_ratio: float = 0.15, seed: int = 42, ): print(f"\n[4/4] Splitting dataset ({len(X)} samples)...") print(f" Ratios: train={train_ratio}, val={val_ratio}, test={test_ratio}") X_train, X_temp, y_train, y_temp = train_test_split( X, y, test_size=(val_ratio + test_ratio), random_state=seed, stratify=y ) relative_test = test_ratio / (val_ratio + test_ratio) X_val, X_test, y_val, y_test = train_test_split( X_temp, y_temp, test_size=relative_test, random_state=seed, stratify=y_temp ) output_dir.mkdir(parents=True, exist_ok=True) for name, X_split, y_split in [("train", X_train, y_train), ("val", X_val, y_val), ("test", X_test, y_test)]: np.save(output_dir / f"X_{name}.npy", X_split) np.save(output_dir / f"y_{name}.npy", y_split) df = pd.DataFrame(X_split, columns=FEATURE_NAMES) df["label"] = y_split df.to_parquet(output_dir / f"{name}.parquet", index=False) df.to_csv(output_dir / f"{name}.csv", index=False) benign_count = int(np.sum(y_split == 0)) malicious_count = int(np.sum(y_split == 1)) print(f" {name:5s}: {len(y_split):6d} samples (benign={benign_count}, malicious={malicious_count})") manifest = { "total_samples": int(len(y)), "train_samples": int(len(y_train)), "val_samples": int(len(y_val)), "test_samples": int(len(y_test)), "feature_names": FEATURE_NAMES, "num_features": len(FEATURE_NAMES), "class_distribution": { "benign": int(np.sum(y == 0)), "malicious": int(np.sum(y == 1)), }, "splits": { "train_ratio": train_ratio, "val_ratio": val_ratio, "test_ratio": test_ratio, }, } with open(output_dir / "manifest.json", "w") as f: json.dump(manifest, f, indent=2) print(f"\n Dataset saved to {output_dir}") print(f" Files: X_{{train,val,test}}.npy, y_{{train,val,test}}.npy") print(f" Also: {{train,val,test}}.parquet, {{train,val,test}}.csv") print(f" Manifest: manifest.json") def main(): parser = argparse.ArgumentParser(description="VibeCheck Dataset Preparation Pipeline") parser.add_argument("--ember-zip", type=str, default=None, help="Path to EMBER2024 archive.zip (default: datasets/raw/ember2024/archive.zip)") parser.add_argument("--max-per-week", type=int, default=5000, help="Max samples to load per weekly JSONL file (default: 5000)") parser.add_argument("--val-weeks", type=int, default=8, help="Number of trailing train weeks to use as validation (default: 8)") parser.add_argument("--max-benign", type=int, default=50000, help="Max benign PE samples to collect from system directories") parser.add_argument("--synthetic-count", type=int, default=0, help="Synthetic vibeware samples to append (default: 0 when using EMBER)") parser.add_argument("--bodmas-dir", type=str, default=None, help="Path to BODMAS dataset directory") parser.add_argument("--output-dir", type=str, default=None, help="Output directory for processed splits") parser.add_argument("--seed", type=int, default=42) parser.add_argument("--skip-benign-scan", action="store_true", help="Skip scanning system directories for benign samples") args = parser.parse_args() ensure_dirs() output_dir = Path(args.output_dir) if args.output_dir else PROCESSED_DIR output_dir.mkdir(parents=True, exist_ok=True) zip_path = Path(args.ember_zip) if args.ember_zip else RAW_DIR / "ember2024" / "archive.zip" ember_result = load_ember2024_from_zip( zip_path, split="train", max_per_week=args.max_per_week, val_weeks=args.val_weeks, ) if ember_result is None: print("ERROR: EMBER2024 zip not found or failed to load.") print(f" Expected: {zip_path}") return X_train, y_train, X_val, y_val = ember_result print(f"\n[2] Loading EMBER2024 test split...") test_result = load_ember2024_from_zip( zip_path, split="test", max_per_week=args.max_per_week, val_weeks=0, ) if test_result is not None: X_test, y_test = test_result[0], test_result[1] else: print(" No test split found, using 15% of train as test.") from sklearn.model_selection import train_test_split as tts X_train, X_test, y_train, y_test = tts( X_train, y_train, test_size=0.15, random_state=args.seed, stratify=y_train ) if not args.skip_benign_scan: print(f"\n[3] Collecting benign system PEs (up to {args.max_benign})...") benign_paths = collect_benign_samples(max_samples=args.max_benign) if benign_paths: X_sys, y_sys = extract_pe_features(benign_paths, label=0) if X_sys: X_sys_arr = np.array(X_sys, dtype=np.float64) X_train = np.vstack([X_train, X_sys_arr]) y_train = np.concatenate([y_train, np.zeros(len(X_sys_arr), dtype=np.int64)]) print(f" Added {len(X_sys_arr)} real benign PEs to training set") else: print("\n[3] Skipping benign system scan") if args.bodmas_dir: bodmas_dir = Path(args.bodmas_dir) if bodmas_dir.exists(): print(f"\n Loading BODMAS from {bodmas_dir}...") bodmas_files = list(bodmas_dir.rglob("*.exe")) + list(bodmas_dir.rglob("*.dll")) if bodmas_files: X_b, y_b = extract_pe_features(bodmas_files, label=1) if X_b: X_train = np.vstack([X_train, np.array(X_b, dtype=np.float64)]) y_train = np.concatenate([y_train, np.array(y_b, dtype=np.int64)]) if args.synthetic_count > 0: print(f"\n[4] Generating {args.synthetic_count} synthetic vibeware samples...") X_synth, y_synth = generate_synthetic_vibeware(count=args.synthetic_count, seed=args.seed) X_train = np.vstack([X_train, X_synth]) y_train = np.concatenate([y_train, y_synth]) print(f"\n Final splits:") for name, X, y in [("train", X_train, y_train), ("val", X_val, y_val), ("test", X_test, y_test)]: b = int(np.sum(y == 0)) m = int(np.sum(y == 1)) print(f" {name:5s}: {len(y):7d} samples (benign={b}, malicious={m})") spw = np.sum(y_train == 0) / max(np.sum(y_train == 1), 1) print(f" scale_pos_weight = {spw:.4f}") for name, X, y in [("train", X_train, y_train), ("val", X_val, y_val), ("test", X_test, y_test)]: np.save(output_dir / f"X_{name}.npy", X) np.save(output_dir / f"y_{name}.npy", y) df = pd.DataFrame(X, columns=FEATURE_NAMES) df["label"] = y df.to_parquet(output_dir / f"{name}.parquet", index=False) manifest = { "total_samples": int(len(y_train) + len(y_val) + len(y_test)), "train_samples": int(len(y_train)), "val_samples": int(len(y_val)), "test_samples": int(len(y_test)), "feature_names": FEATURE_NAMES, "num_features": len(FEATURE_NAMES), "source": "EMBER2024", } with open(output_dir / "manifest.json", "w") as f: json.dump(manifest, f, indent=2) print(f"\n Dataset saved to {output_dir}") if __name__ == "__main__": main()