import argparse import sys from pathlib import Path import joblib import numpy as np from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import StratifiedKFold, cross_validate from sklearn.metrics import classification_report, roc_auc_score sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from app.engine.ml_classifier import extract_features, FEATURE_NAMES def load_samples(data_dir: Path) -> tuple[np.ndarray, np.ndarray]: benign_dir = data_dir / "benign" malicious_dir = data_dir / "malicious" X_list = [] y_list = [] skipped = 0 for label, directory in [(0, benign_dir), (1, malicious_dir)]: if not directory.exists(): print(f"Directory not found: {directory}") continue for filepath in directory.iterdir(): if not filepath.is_file(): continue try: file_bytes = filepath.read_bytes() features = extract_features(file_bytes) if features is not None: X_list.append(features) y_list.append(label) else: skipped += 1 except Exception: skipped += 1 print(f"Loaded {len(X_list)} samples ({skipped} skipped)") return np.array(X_list), np.array(y_list) def train(data_dir: str, output_path: str, model_type: str = "rf") -> None: X, y = load_samples(Path(data_dir)) if len(X) < 20: print("Not enough samples for training. Need at least 20.") sys.exit(1) benign_count = int(np.sum(y == 0)) malicious_count = int(np.sum(y == 1)) print(f"Benign: {benign_count}, Malicious: {malicious_count}") if model_type == "catboost": try: from catboost import CatBoostClassifier model = CatBoostClassifier( iterations=500, learning_rate=0.05, depth=6, l2_leaf_reg=5, border_count=128, class_weights={0: 1.0, 1: 2.0}, eval_metric="Precision", random_seed=42, verbose=0, ) except ImportError: print("CatBoost not installed, falling back to RandomForest") model_type = "rf" if model_type == "rf": model = RandomForestClassifier( n_estimators=100, max_depth=10, random_state=42, ) cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) cv_results = cross_validate( model, X, y, cv=cv, scoring=["precision", "recall", "f1", "roc_auc"], return_train_score=False, ) print("\n5-Fold Cross-Validation Results:") print(f" Precision: {cv_results['test_precision'].mean():.4f} (+/- {cv_results['test_precision'].std():.4f})") print(f" Recall: {cv_results['test_recall'].mean():.4f} (+/- {cv_results['test_recall'].std():.4f})") print(f" F1: {cv_results['test_f1'].mean():.4f} (+/- {cv_results['test_f1'].std():.4f})") print(f" AUC-ROC: {cv_results['test_roc_auc'].mean():.4f} (+/- {cv_results['test_roc_auc'].std():.4f})") model.fit(X, y) output = Path(output_path) output.parent.mkdir(parents=True, exist_ok=True) joblib.dump(model, output) print(f"\nModel saved to {output}") if hasattr(model, "feature_importances_"): print("\nFeature Importances:") importances = model.feature_importances_ sorted_idx = np.argsort(importances)[::-1] for idx in sorted_idx: print(f" {FEATURE_NAMES[idx]:30s} {importances[idx]:.4f}") if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--data-dir", required=True) parser.add_argument("--output", default="app/models/rf_model.pkl") parser.add_argument("--model-type", choices=["rf", "catboost"], default="rf") args = parser.parse_args() train(args.data_dir, args.output, args.model_type)