summaryrefslogtreecommitdiff
path: root/python/examples
diff options
context:
space:
mode:
authorJordan Wiens <github@psifertex.com>2026-01-01 12:21:22 -0500
committerJordan Wiens <github@psifertex.com>2026-01-01 12:21:22 -0500
commit37a0604d805991b85a0590279ad993a19ddee7bb (patch)
treefa8054497a4761f134ab5b537dfb0dbfc9a64f32 /python/examples
parent2448131059638409902821451115872036230328 (diff)
add whole-file entropy calculation to triage
Diffstat (limited to 'python/examples')
-rw-r--r--python/examples/triage/entropy.py25
-rw-r--r--python/examples/triage/file_info.py154
-rw-r--r--python/examples/triage/view.py10
3 files changed, 186 insertions, 3 deletions
diff --git a/python/examples/triage/entropy.py b/python/examples/triage/entropy.py
index a2ab41a6..8dbd3900 100644
--- a/python/examples/triage/entropy.py
+++ b/python/examples/triage/entropy.py
@@ -2,7 +2,7 @@ import math
import threading
from PySide6.QtWidgets import QWidget
from PySide6.QtGui import QImage, QColor, QPainter
-from PySide6.QtCore import Qt, QSize, QTimer
+from PySide6.QtCore import Qt, QSize, QTimer, Signal
import binaryninjaui
from binaryninjaui import ViewFrame, UIContext
from binaryninja.enums import ThemeColor
@@ -14,11 +14,22 @@ class EntropyThread(threading.Thread):
self.image = image
self.block_size = block_size
self.updated = False
+ self.average_entropy = 0.0
+ self.sample_count = 0
+ self.lock = threading.Lock()
def run(self):
width = self.image.width()
for i in range(0, width):
- v = int(self.data.get_entropy(self.data.start + i * self.block_size, self.block_size)[0] * 255)
+ entropy_result = self.data.get_entropy(self.data.start + i * self.block_size, self.block_size)
+ entropy_value = entropy_result[0] if entropy_result else 0.0
+ v = int(entropy_value * 255)
+
+ # Update running average
+ with self.lock:
+ self.sample_count += 1
+ self.average_entropy += (entropy_value - self.average_entropy) / self.sample_count
+
if v >= 240:
color = binaryninjaui.getThemeColor(ThemeColor.YellowStandardHighlightColor)
self.image.setPixelColor(i, 0, color)
@@ -29,8 +40,14 @@ class EntropyThread(threading.Thread):
self.image.setPixelColor(i, 0, color)
self.updated = True
+ def get_average_entropy(self):
+ with self.lock:
+ return self.average_entropy
+
class EntropyWidget(QWidget):
+ entropyUpdated = Signal(float)
+
def __init__(self, parent, view, data):
super(EntropyWidget, self).__init__(parent)
self.view = view
@@ -72,6 +89,7 @@ class EntropyWidget(QWidget):
if self.thread.updated:
self.thread.updated = False
self.update()
+ self.entropyUpdated.emit(self.thread.get_average_entropy())
def mousePressEvent(self, event):
if event.button() != Qt.LeftButton:
@@ -88,3 +106,6 @@ class EntropyWidget(QWidget):
self.setToolTip(f"0x{addr:x}")
else:
self.setToolTip(f"File offset: 0x{offset:x}")
+
+ def get_average_entropy(self):
+ return self.thread.get_average_entropy()
diff --git a/python/examples/triage/file_info.py b/python/examples/triage/file_info.py
new file mode 100644
index 00000000..ec98aeff
--- /dev/null
+++ b/python/examples/triage/file_info.py
@@ -0,0 +1,154 @@
+import os
+import hashlib
+import threading
+from PySide6.QtWidgets import QWidget, QLabel, QGridLayout
+from PySide6.QtCore import Qt
+from binaryninjaui import UIContext
+from binaryninja.enums import ThemeColor
+import binaryninjaui
+
+
+class CopyableLabel(QLabel):
+ def __init__(self, text, color):
+ super(CopyableLabel, self).__init__(text)
+ from PySide6.QtGui import QPalette
+ style = QPalette(self.palette())
+ style.setColor(QPalette.WindowText, color)
+ self.setPalette(style)
+ self.setFont(binaryninjaui.getMonospaceFont(self))
+ self.setCursor(Qt.PointingHandCursor)
+ self.setMouseTracking(True)
+ self.setToolTip("Click to Copy")
+ self.copy_text = "" # Empty - will fall back to current text
+
+ def mousePressEvent(self, event):
+ if event.button() == Qt.LeftButton:
+ from PySide6.QtWidgets import QApplication
+ # Fallback to current text if copy_text not set
+ if self.copy_text:
+ QApplication.clipboard().setText(self.copy_text)
+ else:
+ QApplication.clipboard().setText(self.text())
+
+ def setCopyText(self, text):
+ self.copy_text = text
+
+
+class FileInfoWidget(QWidget):
+ def __init__(self, parent, data, entropy_widget):
+ super(FileInfoWidget, self).__init__(parent)
+ self.data = data
+ self.entropy_widget = entropy_widget
+
+ layout = QGridLayout()
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.setVerticalSpacing(1)
+
+ row = 0
+
+ # Get the file path
+ file_path = data.file.original_filename
+ view = data.parent_view if data.parent_view else data
+
+ # Add path on disk
+ self._add_field(layout, row, "Path on disk:", file_path)
+ row += 1
+
+ # Add path in project if available
+ if data.file.project_file:
+ project_path = data.file.project_file.path_in_project
+ self._add_field(layout, row, "Path in project:", project_path)
+ row += 1
+
+ # Add file size
+ file_size = f"0x{view.length:x}"
+ self._add_copyable_field(layout, row, "Size:", file_size)
+ row += 1
+
+ # Add entropy (will be updated when calculation completes)
+ self.entropy_label = CopyableLabel("Calculating...",
+ binaryninjaui.getThemeColor(ThemeColor.AlphanumericHighlightColor))
+ layout.addWidget(QLabel("Entropy:"), row, 0)
+ layout.addWidget(self.entropy_label, row, 1)
+ row += 1
+
+ # Add hash fields (calculated in background)
+ self.md5_label = CopyableLabel("Calculating...",
+ binaryninjaui.getThemeColor(ThemeColor.AlphanumericHighlightColor))
+ layout.addWidget(QLabel("MD5:"), row, 0)
+ layout.addWidget(self.md5_label, row, 1)
+ row += 1
+
+ self.sha1_label = CopyableLabel("Calculating...",
+ binaryninjaui.getThemeColor(ThemeColor.AlphanumericHighlightColor))
+ layout.addWidget(QLabel("SHA-1:"), row, 0)
+ layout.addWidget(self.sha1_label, row, 1)
+ row += 1
+
+ self.sha256_label = CopyableLabel("Calculating...",
+ binaryninjaui.getThemeColor(ThemeColor.AlphanumericHighlightColor))
+ layout.addWidget(QLabel("SHA-256:"), row, 0)
+ layout.addWidget(self.sha256_label, row, 1)
+ row += 1
+
+ # Set column stretch so value column expands
+ layout.setColumnStretch(1, 1)
+
+ self.setLayout(layout)
+
+ # Connect to entropy widget signal
+ if entropy_widget:
+ entropy_widget.entropyUpdated.connect(self._update_entropy)
+
+ # Start hash calculation in background thread
+ self.hash_thread = threading.Thread(target=self._calculate_hashes)
+ self.hash_thread.daemon = True
+ self.hash_thread.start()
+
+ def _add_field(self, layout, row, name, value):
+ value_label = QLabel(value)
+ value_label.setFont(binaryninjaui.getMonospaceFont(self))
+ layout.addWidget(QLabel(name), row, 0)
+ layout.addWidget(value_label, row, 1)
+
+ def _add_copyable_field(self, layout, row, name, value):
+ value_label = CopyableLabel(value,
+ binaryninjaui.getThemeColor(ThemeColor.AlphanumericHighlightColor))
+ layout.addWidget(QLabel(name), row, 0)
+ layout.addWidget(value_label, row, 1)
+
+ def _calculate_hashes(self):
+ view = self.data.parent_view if self.data.parent_view else self.data
+ total_size = view.length
+
+ md5 = hashlib.md5()
+ sha1 = hashlib.sha1()
+ sha256 = hashlib.sha256()
+
+ offset = 0
+ chunk_size = 128 * 1024 * 1024 # 128MB chunks
+
+ while offset < total_size:
+ remaining = total_size - offset
+ current_chunk = min(chunk_size, remaining)
+ data = view.read(offset, current_chunk)
+
+ if data is None or len(data) != current_chunk:
+ self.md5_label.setText("Error")
+ self.sha1_label.setText("Error")
+ self.sha256_label.setText("Error")
+ return
+
+ md5.update(data)
+ sha1.update(data)
+ sha256.update(data)
+ offset += current_chunk
+
+ # Update labels on main thread
+ from PySide6.QtCore import QMetaObject, Q_ARG
+ QMetaObject.invokeMethod(self.md5_label, "setText", Qt.QueuedConnection, Q_ARG(str, md5.hexdigest()))
+ QMetaObject.invokeMethod(self.sha1_label, "setText", Qt.QueuedConnection, Q_ARG(str, sha1.hexdigest()))
+ QMetaObject.invokeMethod(self.sha256_label, "setText", Qt.QueuedConnection, Q_ARG(str, sha256.hexdigest()))
+
+ def _update_entropy(self, avg_entropy):
+ self.entropy_label.setText(f"{avg_entropy:.6f}")
diff --git a/python/examples/triage/view.py b/python/examples/triage/view.py
index 5a39197f..28a99a75 100644
--- a/python/examples/triage/view.py
+++ b/python/examples/triage/view.py
@@ -7,6 +7,7 @@ from PySide6.QtWidgets import QScrollArea, QWidget, QVBoxLayout, QHBoxLayout, QP
from PySide6.QtCore import Qt
from . import headers
from . import entropy
+from . import file_info
from . import imports
from . import exports
from . import sections
@@ -30,10 +31,17 @@ class TriageView(QScrollArea, View):
entropyGroup = QGroupBox("Entropy", container)
entropyLayout = QVBoxLayout()
- entropyLayout.addWidget(entropy.EntropyWidget(entropyGroup, self, self.data))
+ self.entropyWidget = entropy.EntropyWidget(entropyGroup, self, self.data)
+ entropyLayout.addWidget(self.entropyWidget)
entropyGroup.setLayout(entropyLayout)
layout.addWidget(entropyGroup)
+ fileInfoGroup = QGroupBox("File Info", container)
+ fileInfoLayout = QVBoxLayout()
+ fileInfoLayout.addWidget(file_info.FileInfoWidget(fileInfoGroup, self.data, self.entropyWidget))
+ fileInfoGroup.setLayout(fileInfoLayout)
+ layout.addWidget(fileInfoGroup)
+
hdr = None
try:
if self.data.view_type == "PE":