Spaces:
Sleeping
Sleeping
added all files
Browse files- app.py +12 -0
- models/engine_a_model.pkl +3 -0
- models/engine_b_classes.json +1 -0
- models/engine_b_model.pth +3 -0
- requirements.txt +12 -0
- src/__init__.py +0 -0
- src/engine_a/__init__.py +0 -0
- src/engine_a/inference.py +207 -0
- src/engine_b/__init__.py +0 -0
- src/engine_b/inference.py +156 -0
- src/ui/__init__.py +0 -0
- src/ui/app.py +330 -0
app.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Entry point for Hugging Face Spaces deployment.
|
| 3 |
+
Hugging Face Spaces expects an `app.py` file in the root directory.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import gradio as gr
|
| 7 |
+
from src.ui.app import app
|
| 8 |
+
|
| 9 |
+
if __name__ == "__main__":
|
| 10 |
+
app.launch(
|
| 11 |
+
theme=gr.themes.Monochrome()
|
| 12 |
+
)
|
models/engine_a_model.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:13423e119e88a150020a013d044983c622336dac092bbda728f07bb86d4c02fc
|
| 3 |
+
size 788881
|
models/engine_b_classes.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
["Adialer.C", "Agent.FYI", "Allaple.A", "Allaple.L", "Alueron.gen!J", "Autorun.K", "C2LOP.P", "C2LOP.gen!g", "Dialplatform.B", "Dontovo.A", "Fakerean", "Instantaccess", "Lolyda.AA1", "Lolyda.AA2", "Lolyda.AA3", "Lolyda.AT", "Malex.gen!J", "Obfuscator.AD", "Rbot!gen", "Skintrim.N", "Swizzor", "VB.AT", "Wintrim.BX", "Yuner.A"]
|
models/engine_b_model.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3088ffb9aa49089c145a9d4c9b2162c7f907d18d7863caabaa7b38c2da15ff40
|
| 3 |
+
size 67534965
|
requirements.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fpdf2>=2.8.7
|
| 2 |
+
gradio>=6.15.2
|
| 3 |
+
lightgbm>=4.6.0
|
| 4 |
+
matplotlib>=3.10.9
|
| 5 |
+
numpy>=2.4.6
|
| 6 |
+
pandas>=3.0.3
|
| 7 |
+
pefile>=2024.8.26
|
| 8 |
+
pillow>=12.2.0
|
| 9 |
+
scikit-learn>=1.8.0
|
| 10 |
+
seaborn>=0.13.2
|
| 11 |
+
torch>=2.12
|
| 12 |
+
torchvision>=0.27
|
src/__init__.py
ADDED
|
File without changes
|
src/engine_a/__init__.py
ADDED
|
File without changes
|
src/engine_a/inference.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Inference script for Engine A (EMBER).
|
| 3 |
+
|
| 4 |
+
This module handles loading a pre-trained LightGBM structural classifier
|
| 5 |
+
and parsing uploaded Windows executable binaries (.exe, .dll) using pefile
|
| 6 |
+
to approximate EMBER features. It also supports loading mock JSON profiles.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
import pickle
|
| 11 |
+
import pefile
|
| 12 |
+
import numpy as np
|
| 13 |
+
import warnings
|
| 14 |
+
import json
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class EngineAInfer:
|
| 18 |
+
"""
|
| 19 |
+
Inference Engine for Static Structural Malware Detection.
|
| 20 |
+
|
| 21 |
+
Attributes:
|
| 22 |
+
model: The loaded LightGBM classifier.
|
| 23 |
+
feature_names (dict): Mapping of extracted array indices to human-readable PE header names.
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
def __init__(self, model_path="models/engine_a_model.pkl"):
|
| 27 |
+
"""
|
| 28 |
+
Initializes the inference engine by loading the specified LightGBM model.
|
| 29 |
+
|
| 30 |
+
Args:
|
| 31 |
+
model_path (str): Path to the pickled LightGBM model file.
|
| 32 |
+
"""
|
| 33 |
+
self.model = None
|
| 34 |
+
if os.path.exists(model_path):
|
| 35 |
+
with open(model_path, "rb") as f:
|
| 36 |
+
self.model = pickle.load(f)
|
| 37 |
+
else:
|
| 38 |
+
print(
|
| 39 |
+
f"Warning: {model_path} not found. Engine A will fail unless trained."
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
# Human readable names for the custom extracted pefile features
|
| 43 |
+
self.feature_names = {
|
| 44 |
+
0: "Machine Architecture",
|
| 45 |
+
1: "SizeOfOptionalHeader",
|
| 46 |
+
2: "Characteristics",
|
| 47 |
+
3: "MajorLinkerVersion",
|
| 48 |
+
4: "MinorLinkerVersion",
|
| 49 |
+
5: "SizeOfCode",
|
| 50 |
+
6: "SizeOfInitializedData",
|
| 51 |
+
7: "SizeOfUninitializedData",
|
| 52 |
+
8: "AddressOfEntryPoint",
|
| 53 |
+
9: "BaseOfCode",
|
| 54 |
+
10: "NumberOfSections",
|
| 55 |
+
11: "NumberOfImports",
|
| 56 |
+
12: "TotalImportedFunctions",
|
| 57 |
+
13: "NumberOfExports",
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
def extract_features(self, file_path):
|
| 61 |
+
"""
|
| 62 |
+
Approximates EMBER features using the pefile library.
|
| 63 |
+
|
| 64 |
+
Parses basic PE headers, sections, imports, and exports, and pads
|
| 65 |
+
the vector to the 2381 features expected by the EMBER dataset format.
|
| 66 |
+
|
| 67 |
+
Args:
|
| 68 |
+
file_path (str): Absolute or relative path to the executable file.
|
| 69 |
+
|
| 70 |
+
Returns:
|
| 71 |
+
np.ndarray: A 2D numpy array of shape (1, 2381) representing the features.
|
| 72 |
+
"""
|
| 73 |
+
features = np.zeros(2381, dtype=np.float32)
|
| 74 |
+
|
| 75 |
+
try:
|
| 76 |
+
pe = pefile.PE(file_path)
|
| 77 |
+
# Basic header features
|
| 78 |
+
features[0] = pe.FILE_HEADER.Machine
|
| 79 |
+
features[1] = pe.FILE_HEADER.SizeOfOptionalHeader
|
| 80 |
+
features[2] = pe.FILE_HEADER.Characteristics
|
| 81 |
+
features[3] = pe.OPTIONAL_HEADER.MajorLinkerVersion
|
| 82 |
+
features[4] = pe.OPTIONAL_HEADER.MinorLinkerVersion
|
| 83 |
+
features[5] = pe.OPTIONAL_HEADER.SizeOfCode
|
| 84 |
+
features[6] = pe.OPTIONAL_HEADER.SizeOfInitializedData
|
| 85 |
+
features[7] = pe.OPTIONAL_HEADER.SizeOfUninitializedData
|
| 86 |
+
features[8] = pe.OPTIONAL_HEADER.AddressOfEntryPoint
|
| 87 |
+
features[9] = pe.OPTIONAL_HEADER.BaseOfCode
|
| 88 |
+
|
| 89 |
+
# Number of sections
|
| 90 |
+
features[10] = pe.FILE_HEADER.NumberOfSections
|
| 91 |
+
|
| 92 |
+
# Imports
|
| 93 |
+
if hasattr(pe, "DIRECTORY_ENTRY_IMPORT"):
|
| 94 |
+
features[11] = len(pe.DIRECTORY_ENTRY_IMPORT)
|
| 95 |
+
num_imports = sum(
|
| 96 |
+
[len(entry.imports) for entry in pe.DIRECTORY_ENTRY_IMPORT]
|
| 97 |
+
)
|
| 98 |
+
features[12] = num_imports
|
| 99 |
+
|
| 100 |
+
# Exports
|
| 101 |
+
if hasattr(pe, "DIRECTORY_ENTRY_EXPORT"):
|
| 102 |
+
features[13] = len(pe.DIRECTORY_ENTRY_EXPORT.symbols)
|
| 103 |
+
|
| 104 |
+
except Exception as e:
|
| 105 |
+
print(f"PE parsing error (might not be a PE file): {e}")
|
| 106 |
+
|
| 107 |
+
return features.reshape(1, -1)
|
| 108 |
+
|
| 109 |
+
def predict(self, file_path):
|
| 110 |
+
"""
|
| 111 |
+
Predicts if a file is malicious based on its structural layout.
|
| 112 |
+
|
| 113 |
+
Also calculates SHAP (SHapley Additive exPlanations) values to provide
|
| 114 |
+
explainability regarding which structural features influenced the decision.
|
| 115 |
+
|
| 116 |
+
Args:
|
| 117 |
+
file_path (str): Path to the target file. Can be a Windows PE binary
|
| 118 |
+
or a simulated mock malware profile (.json).
|
| 119 |
+
|
| 120 |
+
Returns:
|
| 121 |
+
dict: Contains 'is_malware' (bool), 'malware_prob' (float), and
|
| 122 |
+
'top_features' (list of tuples detailing top contributing factors).
|
| 123 |
+
"""
|
| 124 |
+
if self.model is None:
|
| 125 |
+
raise Exception("Engine A model is not loaded. Train the model first.")
|
| 126 |
+
|
| 127 |
+
# Handle mock JSON profile logic
|
| 128 |
+
if file_path.endswith(".json"):
|
| 129 |
+
try:
|
| 130 |
+
with open(file_path, "r") as f:
|
| 131 |
+
data = json.load(f)
|
| 132 |
+
if data.get("is_mock_profile"):
|
| 133 |
+
features = np.array(data["ember_features"]).reshape(1, -1)
|
| 134 |
+
else:
|
| 135 |
+
features = self.extract_features(file_path)
|
| 136 |
+
except:
|
| 137 |
+
features = self.extract_features(file_path)
|
| 138 |
+
else:
|
| 139 |
+
features = self.extract_features(file_path)
|
| 140 |
+
|
| 141 |
+
# Suppress LGBM missing feature names warning
|
| 142 |
+
with warnings.catch_warnings():
|
| 143 |
+
warnings.simplefilter("ignore", UserWarning)
|
| 144 |
+
probs = self.model.predict_proba(features)[0]
|
| 145 |
+
|
| 146 |
+
# Use LightGBM pred_contrib=True to get SHAP values for explainability
|
| 147 |
+
contributions = self.model.predict(features, pred_contrib=True)[0]
|
| 148 |
+
|
| 149 |
+
malware_prob = float(probs[1]) if len(probs) > 1 else float(probs[0])
|
| 150 |
+
|
| 151 |
+
# Explainability Logic: Exclude the last element (base expected value)
|
| 152 |
+
feature_shap = contributions[:-1]
|
| 153 |
+
|
| 154 |
+
# If the file is malicious, we want the most positive SHAP values.
|
| 155 |
+
# If it's benign, we want the most negative SHAP values.
|
| 156 |
+
is_malicious = malware_prob > 0.5
|
| 157 |
+
|
| 158 |
+
if is_malicious:
|
| 159 |
+
# Sort descending for highest positive push
|
| 160 |
+
top_indices = np.argsort(feature_shap)[::-1][:3]
|
| 161 |
+
else:
|
| 162 |
+
# Sort ascending for lowest negative push
|
| 163 |
+
top_indices = np.argsort(feature_shap)[:3]
|
| 164 |
+
|
| 165 |
+
top_features = []
|
| 166 |
+
for idx in top_indices:
|
| 167 |
+
name = self._get_feature_name(idx)
|
| 168 |
+
contrib = feature_shap[idx]
|
| 169 |
+
top_features.append((name, float(contrib)))
|
| 170 |
+
|
| 171 |
+
return {
|
| 172 |
+
"is_malware": is_malicious,
|
| 173 |
+
"malware_prob": malware_prob,
|
| 174 |
+
"top_features": top_features,
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
def _get_feature_name(self, idx):
|
| 178 |
+
"""
|
| 179 |
+
Maps an EMBER numerical feature index to a human-readable category.
|
| 180 |
+
|
| 181 |
+
EMBER extracts 2381 features using LIEF. Because the parquet file strips
|
| 182 |
+
the column names, we rely on the known ranges of the EMBER feature specification
|
| 183 |
+
to explain what the model is looking at.
|
| 184 |
+
"""
|
| 185 |
+
if idx in self.feature_names and idx <= 13:
|
| 186 |
+
# Our custom exact mappings for the first 14 features
|
| 187 |
+
return f"{self.feature_names[idx]} (Index {idx})"
|
| 188 |
+
|
| 189 |
+
# Generalized EMBER feature mapping by vector location
|
| 190 |
+
if 0 <= idx <= 255:
|
| 191 |
+
return f"Raw Byte Histogram Analysis (Index {idx})"
|
| 192 |
+
elif 256 <= idx <= 511:
|
| 193 |
+
return f"Byte Entropy/Complexity Matrix (Index {idx})"
|
| 194 |
+
elif 512 <= idx <= 615:
|
| 195 |
+
return f"Embedded Strings Metadata [Paths/URLs/RegKeys] (Index {idx})"
|
| 196 |
+
elif 616 <= idx <= 626:
|
| 197 |
+
return f"General PE Structural Info (Index {idx})"
|
| 198 |
+
elif 627 <= idx <= 688:
|
| 199 |
+
return f"PE Header Anomaly (Index {idx})"
|
| 200 |
+
elif 689 <= idx <= 944:
|
| 201 |
+
return f"PE Sections Characteristics (Index {idx})"
|
| 202 |
+
elif 945 <= idx <= 2224:
|
| 203 |
+
return f"Imported Libraries/API Calls (Index {idx})"
|
| 204 |
+
elif 2225 <= idx <= 2381:
|
| 205 |
+
return f"Exported Functions/Data Directories (Index {idx})"
|
| 206 |
+
else:
|
| 207 |
+
return f"Deep Structural Feature (Index {idx})"
|
src/engine_b/__init__.py
ADDED
|
File without changes
|
src/engine_b/inference.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Inference script for Engine B (Malimg).
|
| 3 |
+
|
| 4 |
+
This module provides functionality to read raw binary files, convert them
|
| 5 |
+
to 2D grayscale image tensors, and classify their malware family using
|
| 6 |
+
a pre-trained Convolutional Neural Network (CNN).
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
import torch
|
| 11 |
+
from torchvision import transforms
|
| 12 |
+
from PIL import Image
|
| 13 |
+
import numpy as np
|
| 14 |
+
import json
|
| 15 |
+
from src.engine_b.train import MalwareCNN
|
| 16 |
+
import math
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def bytes_to_image(file_path):
|
| 20 |
+
"""
|
| 21 |
+
Reads a raw binary file and converts its bytes into a square grayscale image.
|
| 22 |
+
|
| 23 |
+
If the file is a mock JSON profile, it reads the byte array from the JSON.
|
| 24 |
+
Otherwise, it reads the raw bytes of the executable and reshapes them into
|
| 25 |
+
a square 2D matrix, padding the end with zeros if necessary.
|
| 26 |
+
|
| 27 |
+
Args:
|
| 28 |
+
file_path (str): Path to the target binary file.
|
| 29 |
+
|
| 30 |
+
Returns:
|
| 31 |
+
PIL.Image: A grayscale (mode 'L') Image object representing the binary.
|
| 32 |
+
"""
|
| 33 |
+
# Check if this is our safe mock profile
|
| 34 |
+
if file_path.endswith(".json"):
|
| 35 |
+
try:
|
| 36 |
+
with open(file_path, "r") as f:
|
| 37 |
+
data = json.load(f)
|
| 38 |
+
if data.get("is_mock_profile"):
|
| 39 |
+
byte_array = np.array(data["malimg_bytes"], dtype=np.uint8)
|
| 40 |
+
else:
|
| 41 |
+
with open(file_path, "rb") as f:
|
| 42 |
+
binary_data = f.read()
|
| 43 |
+
byte_array = np.frombuffer(binary_data, dtype=np.uint8)
|
| 44 |
+
except:
|
| 45 |
+
with open(file_path, "rb") as f:
|
| 46 |
+
binary_data = f.read()
|
| 47 |
+
byte_array = np.frombuffer(binary_data, dtype=np.uint8)
|
| 48 |
+
else:
|
| 49 |
+
# Read raw binary
|
| 50 |
+
with open(file_path, "rb") as f:
|
| 51 |
+
binary_data = f.read()
|
| 52 |
+
byte_array = np.frombuffer(binary_data, dtype=np.uint8)
|
| 53 |
+
|
| 54 |
+
# Calculate image dimensions (square)
|
| 55 |
+
length = len(byte_array)
|
| 56 |
+
if length == 0:
|
| 57 |
+
return Image.new("L", (128, 128), color=0)
|
| 58 |
+
|
| 59 |
+
width = int(math.ceil(math.sqrt(length)))
|
| 60 |
+
height = width
|
| 61 |
+
|
| 62 |
+
# Pad array to form a perfect square
|
| 63 |
+
padded_length = width * height
|
| 64 |
+
padded_array = np.pad(byte_array, (0, padded_length - length), mode="constant")
|
| 65 |
+
|
| 66 |
+
# Reshape and create PIL Image
|
| 67 |
+
image_2d = padded_array.reshape((height, width))
|
| 68 |
+
img = Image.fromarray(image_2d, mode="L")
|
| 69 |
+
return img
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
class EngineBInfer:
|
| 73 |
+
"""
|
| 74 |
+
Inference Engine for Visual Malware Family Classification.
|
| 75 |
+
|
| 76 |
+
Attributes:
|
| 77 |
+
device (torch.device): CPU or CUDA device for inference.
|
| 78 |
+
classes (list): Ordered list of malware family class names.
|
| 79 |
+
model (MalwareCNN): The loaded PyTorch CNN.
|
| 80 |
+
transform (transforms.Compose): Image preprocessing pipeline.
|
| 81 |
+
"""
|
| 82 |
+
|
| 83 |
+
def __init__(
|
| 84 |
+
self,
|
| 85 |
+
model_path="models/engine_b_model.pth",
|
| 86 |
+
classes_path="models/engine_b_classes.json",
|
| 87 |
+
):
|
| 88 |
+
"""
|
| 89 |
+
Initializes the vision inference engine.
|
| 90 |
+
|
| 91 |
+
Args:
|
| 92 |
+
model_path (str): Path to the trained PyTorch state dictionary.
|
| 93 |
+
classes_path (str): Path to the JSON list mapping indices to class names.
|
| 94 |
+
"""
|
| 95 |
+
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 96 |
+
self.classes = []
|
| 97 |
+
if os.path.exists(classes_path):
|
| 98 |
+
with open(classes_path, "r") as f:
|
| 99 |
+
self.classes = json.load(f)
|
| 100 |
+
else:
|
| 101 |
+
# Fallback dummy classes if not trained yet
|
| 102 |
+
self.classes = [f"Class_{i}" for i in range(24)]
|
| 103 |
+
|
| 104 |
+
num_classes = len(self.classes)
|
| 105 |
+
self.model = MalwareCNN(num_classes=num_classes)
|
| 106 |
+
|
| 107 |
+
if os.path.exists(model_path):
|
| 108 |
+
self.model.load_state_dict(
|
| 109 |
+
torch.load(model_path, map_location=self.device, weights_only=True)
|
| 110 |
+
)
|
| 111 |
+
else:
|
| 112 |
+
print(f"Warning: {model_path} not found. Using untrained weights.")
|
| 113 |
+
|
| 114 |
+
self.model.to(self.device)
|
| 115 |
+
self.model.eval()
|
| 116 |
+
|
| 117 |
+
self.transform = transforms.Compose(
|
| 118 |
+
[
|
| 119 |
+
transforms.Resize((128, 128)),
|
| 120 |
+
transforms.ToTensor(),
|
| 121 |
+
]
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
def predict(self, file_path):
|
| 125 |
+
"""
|
| 126 |
+
Converts the target file to an image and runs CNN inference.
|
| 127 |
+
|
| 128 |
+
Args:
|
| 129 |
+
file_path (str): Path to the target file.
|
| 130 |
+
|
| 131 |
+
Returns:
|
| 132 |
+
dict: Contains 'family' (str), 'confidence' (float),
|
| 133 |
+
'all_probabilities' (dict), and 'image' (PIL.Image upscaled).
|
| 134 |
+
"""
|
| 135 |
+
img = bytes_to_image(file_path)
|
| 136 |
+
tensor = self.transform(img).unsqueeze(0).to(self.device)
|
| 137 |
+
|
| 138 |
+
with torch.no_grad():
|
| 139 |
+
outputs = self.model(tensor)
|
| 140 |
+
probabilities = torch.nn.functional.softmax(outputs, dim=1)[0]
|
| 141 |
+
|
| 142 |
+
# Create a dictionary of all class probabilities
|
| 143 |
+
all_probs = {
|
| 144 |
+
self.classes[i]: probabilities[i].item()
|
| 145 |
+
for i in range(len(self.classes))
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
top_prob, top_class = torch.max(probabilities, 0)
|
| 149 |
+
class_name = self.classes[top_class.item()]
|
| 150 |
+
|
| 151 |
+
return {
|
| 152 |
+
"family": class_name,
|
| 153 |
+
"confidence": top_prob.item(),
|
| 154 |
+
"all_probabilities": all_probs,
|
| 155 |
+
"image": img.resize((512, 512), resample=Image.NEAREST),
|
| 156 |
+
}
|
src/ui/__init__.py
ADDED
|
File without changes
|
src/ui/app.py
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Gradio UI Application for the Dual-Engine Malware Analysis Pipeline.
|
| 3 |
+
|
| 4 |
+
This module provides the frontend interface for uploading executables or simulated profiles,
|
| 5 |
+
routing them to the static structural engine (Engine A) and the visual byte-map engine (Engine B),
|
| 6 |
+
and rendering their ensemble metrics securely in the browser.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import gradio as gr
|
| 10 |
+
from src.engine_a.inference import EngineAInfer
|
| 11 |
+
from src.engine_b.inference import EngineBInfer
|
| 12 |
+
import os
|
| 13 |
+
import datetime
|
| 14 |
+
import torch
|
| 15 |
+
from fpdf import FPDF
|
| 16 |
+
import tempfile
|
| 17 |
+
|
| 18 |
+
print("Loading ML models...")
|
| 19 |
+
engine_a = EngineAInfer()
|
| 20 |
+
engine_b = EngineBInfer()
|
| 21 |
+
print("Models loaded successfully.")
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def analyze_malware(file_path):
|
| 25 |
+
"""
|
| 26 |
+
Main processing pipeline triggered by the Gradio 'Analyze File' button.
|
| 27 |
+
|
| 28 |
+
Args:
|
| 29 |
+
file_path (str): The path to the uploaded file.
|
| 30 |
+
|
| 31 |
+
Returns:
|
| 32 |
+
tuple: (output_a_text, image_b, prob_table, ensemble_verdict) for the UI components.
|
| 33 |
+
"""
|
| 34 |
+
if not file_path:
|
| 35 |
+
return "No file provided.", None, {}, "N/A"
|
| 36 |
+
|
| 37 |
+
try:
|
| 38 |
+
# Engine A Analysis
|
| 39 |
+
result_a = engine_a.predict(file_path)
|
| 40 |
+
prob_a = result_a["malware_prob"] * 100
|
| 41 |
+
status_a = "Malicious" if result_a["is_malware"] else "Benign"
|
| 42 |
+
|
| 43 |
+
text_a = f"Status: {status_a}\nConfidence: {prob_a:.2f}%"
|
| 44 |
+
|
| 45 |
+
# Build explainability for gr.Label
|
| 46 |
+
explain_dict = {}
|
| 47 |
+
total_abs_shap = sum(abs(f[1]) for f in result_a["top_features"])
|
| 48 |
+
if total_abs_shap == 0:
|
| 49 |
+
total_abs_shap = 1
|
| 50 |
+
|
| 51 |
+
for feature, shap_val in result_a["top_features"]:
|
| 52 |
+
impact_dir = "Malicious" if shap_val > 0 else "Benign"
|
| 53 |
+
label_name = f"{feature} [{impact_dir}]"
|
| 54 |
+
# Normalize to 0-1 so gr.Label can render the horizontal bars properly
|
| 55 |
+
normalized_impact = float(abs(shap_val) / total_abs_shap)
|
| 56 |
+
explain_dict[label_name] = normalized_impact
|
| 57 |
+
|
| 58 |
+
# Engine B Analysis
|
| 59 |
+
result_b = engine_b.predict(file_path)
|
| 60 |
+
all_probs = result_b["all_probabilities"]
|
| 61 |
+
image_b = result_b["image"]
|
| 62 |
+
|
| 63 |
+
# Ensemble Logic
|
| 64 |
+
ensemble_score = (prob_a + result_b["confidence"] * 100) / 2
|
| 65 |
+
ensemble_verdict = f"Overall Risk Score: {ensemble_score:.1f}/100"
|
| 66 |
+
|
| 67 |
+
# Generate PDF Report
|
| 68 |
+
pdf_path = generate_pdf_report(
|
| 69 |
+
file_path=file_path,
|
| 70 |
+
status_a=status_a,
|
| 71 |
+
prob_a=prob_a,
|
| 72 |
+
top_features=result_a["top_features"],
|
| 73 |
+
family_b=result_b["family"],
|
| 74 |
+
prob_b=result_b["confidence"] * 100,
|
| 75 |
+
image_b=image_b,
|
| 76 |
+
ensemble_score=ensemble_score,
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
return (
|
| 80 |
+
text_a,
|
| 81 |
+
explain_dict,
|
| 82 |
+
image_b,
|
| 83 |
+
all_probs,
|
| 84 |
+
ensemble_verdict,
|
| 85 |
+
gr.update(value=pdf_path, visible=True),
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
except Exception as e:
|
| 89 |
+
return (
|
| 90 |
+
f"Error processing file in Engine A: {e}",
|
| 91 |
+
{},
|
| 92 |
+
None,
|
| 93 |
+
{},
|
| 94 |
+
"Error",
|
| 95 |
+
gr.update(visible=False),
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def generate_pdf_report(
|
| 100 |
+
file_path, status_a, prob_a, top_features, family_b, prob_b, image_b, ensemble_score
|
| 101 |
+
):
|
| 102 |
+
|
| 103 |
+
class PDFReport(FPDF):
|
| 104 |
+
def header(self):
|
| 105 |
+
# Header Bar
|
| 106 |
+
self.set_fill_color(40, 40, 40)
|
| 107 |
+
self.rect(0, 0, 210, 30, "F")
|
| 108 |
+
self.set_y(10)
|
| 109 |
+
self.set_font("Helvetica", style="B", size=24)
|
| 110 |
+
self.set_text_color(255, 255, 255)
|
| 111 |
+
self.cell(0, 10, "MALWARE ANALYSIS REPORT", border=0, align="C")
|
| 112 |
+
self.ln(15)
|
| 113 |
+
|
| 114 |
+
def footer(self):
|
| 115 |
+
self.set_y(-15)
|
| 116 |
+
self.set_font("Helvetica", style="I", size=8)
|
| 117 |
+
self.set_text_color(128, 128, 128)
|
| 118 |
+
self.cell(0, 10, f"Page {self.page_no()}", align="C")
|
| 119 |
+
|
| 120 |
+
pdf = PDFReport()
|
| 121 |
+
pdf.add_page()
|
| 122 |
+
pdf.set_auto_page_break(auto=True, margin=15)
|
| 123 |
+
pdf.set_y(40) # Start below header
|
| 124 |
+
|
| 125 |
+
# 1. Overview Section
|
| 126 |
+
pdf.set_fill_color(230, 230, 230)
|
| 127 |
+
pdf.set_text_color(0, 0, 0)
|
| 128 |
+
pdf.set_font("Helvetica", style="B", size=14)
|
| 129 |
+
pdf.cell(
|
| 130 |
+
0,
|
| 131 |
+
10,
|
| 132 |
+
" 1. Execution Overview",
|
| 133 |
+
border=0,
|
| 134 |
+
new_x="LMARGIN",
|
| 135 |
+
new_y="NEXT",
|
| 136 |
+
fill=True,
|
| 137 |
+
)
|
| 138 |
+
pdf.ln(2)
|
| 139 |
+
|
| 140 |
+
pdf.set_font("Helvetica", size=11)
|
| 141 |
+
pdf.cell(40, 8, "Target File:", border=0)
|
| 142 |
+
pdf.set_font("Helvetica", style="B", size=11)
|
| 143 |
+
pdf.cell(
|
| 144 |
+
0, 8, f"{os.path.basename(file_path)}", border=0, new_x="LMARGIN", new_y="NEXT"
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
pdf.set_font("Helvetica", size=11)
|
| 148 |
+
pdf.cell(40, 8, "Timestamp:", border=0)
|
| 149 |
+
pdf.set_font("Helvetica", style="B", size=11)
|
| 150 |
+
pdf.cell(
|
| 151 |
+
0,
|
| 152 |
+
8,
|
| 153 |
+
f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
| 154 |
+
border=0,
|
| 155 |
+
new_x="LMARGIN",
|
| 156 |
+
new_y="NEXT",
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
pdf.set_font("Helvetica", size=11)
|
| 160 |
+
pdf.cell(40, 8, "Risk Score:", border=0)
|
| 161 |
+
pdf.set_font("Helvetica", style="B", size=12)
|
| 162 |
+
# Highlight risk score in text
|
| 163 |
+
pdf.cell(
|
| 164 |
+
0, 8, f"{ensemble_score:.1f} / 100", border=0, new_x="LMARGIN", new_y="NEXT"
|
| 165 |
+
)
|
| 166 |
+
pdf.ln(5)
|
| 167 |
+
|
| 168 |
+
# 2. Engine A
|
| 169 |
+
pdf.set_font("Helvetica", style="B", size=14)
|
| 170 |
+
pdf.cell(
|
| 171 |
+
0,
|
| 172 |
+
10,
|
| 173 |
+
" 2. Structural Engine (EMBER)",
|
| 174 |
+
border=0,
|
| 175 |
+
new_x="LMARGIN",
|
| 176 |
+
new_y="NEXT",
|
| 177 |
+
fill=True,
|
| 178 |
+
)
|
| 179 |
+
pdf.ln(2)
|
| 180 |
+
|
| 181 |
+
pdf.set_font("Helvetica", size=11)
|
| 182 |
+
pdf.cell(
|
| 183 |
+
0,
|
| 184 |
+
8,
|
| 185 |
+
f"Verdict: {status_a.upper()} (Confidence: {prob_a:.1f}%)",
|
| 186 |
+
new_x="LMARGIN",
|
| 187 |
+
new_y="NEXT",
|
| 188 |
+
)
|
| 189 |
+
pdf.ln(2)
|
| 190 |
+
|
| 191 |
+
pdf.set_font("Helvetica", style="B", size=10)
|
| 192 |
+
pdf.cell(140, 8, "Top Contributing Feature (SHAP)", border=1, fill=True)
|
| 193 |
+
pdf.cell(
|
| 194 |
+
50,
|
| 195 |
+
8,
|
| 196 |
+
"Impact Direction",
|
| 197 |
+
border=1,
|
| 198 |
+
new_x="LMARGIN",
|
| 199 |
+
new_y="NEXT",
|
| 200 |
+
fill=True,
|
| 201 |
+
align="C",
|
| 202 |
+
)
|
| 203 |
+
|
| 204 |
+
pdf.set_font("Helvetica", size=10)
|
| 205 |
+
for feature, shap_val in top_features:
|
| 206 |
+
impact_dir = "MALICIOUS" if shap_val > 0 else "BENIGN"
|
| 207 |
+
pdf.cell(140, 8, f" {feature}", border=1)
|
| 208 |
+
pdf.cell(
|
| 209 |
+
50,
|
| 210 |
+
8,
|
| 211 |
+
f"{shap_val:+.2f} ({impact_dir})",
|
| 212 |
+
border=1,
|
| 213 |
+
new_x="LMARGIN",
|
| 214 |
+
new_y="NEXT",
|
| 215 |
+
align="C",
|
| 216 |
+
)
|
| 217 |
+
pdf.ln(8)
|
| 218 |
+
|
| 219 |
+
# 3. Engine B
|
| 220 |
+
pdf.set_font("Helvetica", style="B", size=14)
|
| 221 |
+
pdf.cell(
|
| 222 |
+
0,
|
| 223 |
+
10,
|
| 224 |
+
" 3. Visual Engine (Malimg)",
|
| 225 |
+
border=0,
|
| 226 |
+
new_x="LMARGIN",
|
| 227 |
+
new_y="NEXT",
|
| 228 |
+
fill=True,
|
| 229 |
+
)
|
| 230 |
+
pdf.ln(2)
|
| 231 |
+
|
| 232 |
+
pdf.set_font("Helvetica", size=11)
|
| 233 |
+
pdf.cell(
|
| 234 |
+
0,
|
| 235 |
+
8,
|
| 236 |
+
f"Predicted Malware Family: {family_b.upper()} (Confidence: {prob_b:.1f}%)",
|
| 237 |
+
new_x="LMARGIN",
|
| 238 |
+
new_y="NEXT",
|
| 239 |
+
)
|
| 240 |
+
pdf.ln(2)
|
| 241 |
+
|
| 242 |
+
# Embed Image
|
| 243 |
+
temp_dir = tempfile.gettempdir()
|
| 244 |
+
img_path = os.path.join(temp_dir, "byte_map.png")
|
| 245 |
+
image_b.save(img_path)
|
| 246 |
+
|
| 247 |
+
pdf.set_font("Helvetica", style="B", size=10)
|
| 248 |
+
pdf.cell(0, 8, "Grayscale Byte Map Render:", new_x="LMARGIN", new_y="NEXT")
|
| 249 |
+
|
| 250 |
+
# Draw image with a border
|
| 251 |
+
x_pos = pdf.get_x()
|
| 252 |
+
y_pos = pdf.get_y()
|
| 253 |
+
pdf.rect(x_pos, y_pos, 70, 70)
|
| 254 |
+
pdf.image(img_path, x=x_pos, y=y_pos, w=70, h=70)
|
| 255 |
+
|
| 256 |
+
report_path = os.path.join(
|
| 257 |
+
temp_dir, f"Analysis_Report_{os.path.basename(file_path)}.pdf"
|
| 258 |
+
)
|
| 259 |
+
pdf.output(report_path)
|
| 260 |
+
return report_path
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
def get_system_status():
|
| 264 |
+
"""
|
| 265 |
+
Fetches real-time system metrics for the UI header.
|
| 266 |
+
|
| 267 |
+
Returns:
|
| 268 |
+
str: Formatted markdown string containing time, compute device, and engine status.
|
| 269 |
+
"""
|
| 270 |
+
current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 271 |
+
device = "CUDA" if torch.cuda.is_available() else "CPU"
|
| 272 |
+
return f"**System Time:** {current_time} | **Compute Device:** {device} | **Engines:** Online & Ready"
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
# Build the Gradio UI
|
| 276 |
+
with gr.Blocks() as app:
|
| 277 |
+
# Use HTML to make the title massively larger than the buttons
|
| 278 |
+
gr.HTML(
|
| 279 |
+
"<h1 style='text-align: center; font-size: 3.5rem; margin-bottom: 0.2rem; font-weight: bold;'>Dual-Engine Malware Analysis</h1>"
|
| 280 |
+
)
|
| 281 |
+
gr.HTML(
|
| 282 |
+
"<p style='text-align: center; font-size: 1.2rem; color: gray;'>Upload a Windows Executable (`.exe`, `.dll`) or a Simulated Profile (`.json`)</p>"
|
| 283 |
+
)
|
| 284 |
+
|
| 285 |
+
status_bar = gr.Markdown(get_system_status())
|
| 286 |
+
|
| 287 |
+
# Compact inputs
|
| 288 |
+
with gr.Row():
|
| 289 |
+
file_input = gr.File(label="Upload File", type="filepath", scale=4)
|
| 290 |
+
with gr.Column(scale=1):
|
| 291 |
+
analyze_btn = gr.Button("Analyze File", variant="primary", size="sm")
|
| 292 |
+
download_btn = gr.File(label="Download Full Report (PDF)", visible=False)
|
| 293 |
+
|
| 294 |
+
gr.Markdown("---")
|
| 295 |
+
|
| 296 |
+
# Single column layout for maximum horizontal space
|
| 297 |
+
ensemble_output = gr.Textbox(
|
| 298 |
+
label="Final Verdict", value="Pending", interactive=False
|
| 299 |
+
)
|
| 300 |
+
output_a = gr.Textbox(
|
| 301 |
+
label="Engine A: Structural (EMBER)",
|
| 302 |
+
value="Awaiting input...",
|
| 303 |
+
interactive=False,
|
| 304 |
+
lines=2,
|
| 305 |
+
)
|
| 306 |
+
explain_plot = gr.Label(
|
| 307 |
+
label="Engine A: Explainability (Feature Impact)", num_top_classes=3
|
| 308 |
+
)
|
| 309 |
+
image_output = gr.Image(label="Engine B: Grayscale Byte Map", type="pil")
|
| 310 |
+
output_b = gr.Label(label="Engine B: Family Probabilities", num_top_classes=25)
|
| 311 |
+
|
| 312 |
+
analyze_btn.click(
|
| 313 |
+
analyze_malware,
|
| 314 |
+
inputs=[file_input],
|
| 315 |
+
outputs=[
|
| 316 |
+
output_a,
|
| 317 |
+
explain_plot,
|
| 318 |
+
image_output,
|
| 319 |
+
output_b,
|
| 320 |
+
ensemble_output,
|
| 321 |
+
download_btn,
|
| 322 |
+
],
|
| 323 |
+
)
|
| 324 |
+
|
| 325 |
+
# Live update the status bar
|
| 326 |
+
timer = gr.Timer(1)
|
| 327 |
+
timer.tick(get_system_status, inputs=None, outputs=status_bar)
|
| 328 |
+
|
| 329 |
+
# Load immediately on page open
|
| 330 |
+
app.load(get_system_status, inputs=None, outputs=status_bar)
|