RohanSardar commited on
Commit
88d577b
·
verified ·
1 Parent(s): a7f1dfe

added all files

Browse files
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)