From 2f2bacdb858172f5697b3ae63e930826c509a428 Mon Sep 17 00:00:00 2001 From: RRFRRF <1195131157@qq.com> Date: Fri, 29 Nov 2024 23:47:47 +0800 Subject: [PATCH 01/42] fix image-display && add dockerfile --- GUI/function.py | 20 ++++++++++++++++---- GUI/ui.py | 11 +++++++---- dockerfile | 27 +++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 8 deletions(-) create mode 100644 dockerfile diff --git a/GUI/function.py b/GUI/function.py index b1940fe..985d962 100644 --- a/GUI/function.py +++ b/GUI/function.py @@ -57,10 +57,22 @@ def display_image(self, plotvis): def update_image_display(self): if self.current_pixmap: - available_width = self.ui.width() - available_height = self.ui.height() - self.ui.visualization_label.geometry().bottom() - 50 - - scaled_pixmap = self.current_pixmap.scaled(available_width, available_height, Qt.KeepAspectRatio, Qt.SmoothTransformation) + # Get the actual size of the visualization_display + display_height = self.ui.visualization_display.height() + display_width = self.ui.visualization_display.width() + # Get the original dimensions of the image + original_width = self.current_pixmap.width() + original_height = self.current_pixmap.height() + # Calculate the scaling ratio + width_ratio = display_width / original_width + height_ratio = display_height / original_height + # Use the smaller ratio to ensure the image fits completely within the display area + scale_ratio = min(width_ratio, height_ratio) + # Calculate the scaled dimensions + new_width = int(original_width * scale_ratio) + new_height = int(original_height * scale_ratio) + # Scale the image + scaled_pixmap = self.current_pixmap.scaled(new_width, new_height, Qt.KeepAspectRatio, Qt.SmoothTransformation) self.ui.visualization_display.setPixmap(scaled_pixmap) def on_resize(self, event): diff --git a/GUI/ui.py b/GUI/ui.py index bde47fe..e39e32f 100644 --- a/GUI/ui.py +++ b/GUI/ui.py @@ -1,4 +1,4 @@ -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QLineEdit, QPushButton, QTextEdit, QHBoxLayout, QTabWidget +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QLineEdit, QPushButton, QTextEdit, QHBoxLayout, QTabWidget, QSizePolicy from PyQt5.QtGui import QFont from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QApplication @@ -162,11 +162,14 @@ def init_phish_test_page(self): self.visualization_label = QLabel('Visualization Result:') self.visualization_label.setFixedHeight(self.visualization_label.fontMetrics().height()) visualization_layout.addWidget(self.visualization_label) + self.visualization_display = QLabel() self.visualization_display.setAlignment(Qt.AlignCenter) - self.visualization_display.setMinimumSize(300, 200) - visualization_layout.addWidget(self.visualization_display) - layout.addLayout(visualization_layout) + self.visualization_display.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self.visualization_display.setMinimumSize(300, 300) + visualization_layout.addWidget(self.visualization_display, 1) + + layout.addLayout(visualization_layout, 1) self.phish_test_page.setLayout(layout) diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..15f4cde --- /dev/null +++ b/dockerfile @@ -0,0 +1,27 @@ +# Use Ubuntu 20.04 as the base image +FROM ubuntu:20.04 +# Prevent interactive prompts during installation +ARG DEBIAN_FRONTEND=noninteractive +# Install Miniconda +RUN apt-get update && apt-get install -y wget && \ + wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh && \ + bash Miniconda3-latest-Linux-x86_64.sh -b -p /opt/miniconda && \ + rm Miniconda3-latest-Linux-x86_64.sh +# Add Miniconda to PATH +ENV PATH="/opt/miniconda/bin:${PATH}" +# Set working directory +WORKDIR /workspace +# Install git +RUN apt-get install -y git +# Clone the Phishpedia project from GitHub into the container +RUN git clone https://github.com/lindsey98/Phishpedia.git /workspace/Phishpedia +# Change to the project directory and run setup.sh to configure the environment +WORKDIR /workspace/Phishpedia +# Install dos2unix +RUN apt-get install -y dos2unix +# Convert setup.sh to Unix format and RUN it +RUN dos2unix setup.sh +RUN chmod +x setup.sh +RUN bash setup.sh +# Set the default command to execute when the container starts +CMD ["bash", "-c", "cd /workspace/Phishpedia && /bin/bash"] \ No newline at end of file From 6e3ffa521b0a1c8fe0b826d23f5d6175cfa4ef3d Mon Sep 17 00:00:00 2001 From: sk_han Date: Mon, 2 Dec 2024 11:29:17 +0800 Subject: [PATCH 02/42] Create Plugin_for_Chrome --- Plugin_for_Chrome/client/background.js | 33 ++++++++++++ Plugin_for_Chrome/client/content.js | 23 +++++++++ Plugin_for_Chrome/client/manifest.json | 32 ++++++++++++ Plugin_for_Chrome/client/popup.html | 17 ++++++ Plugin_for_Chrome/client/popup.js | 71 ++++++++++++++++++++++++++ Plugin_for_Chrome/server/server.py | 44 ++++++++++++++++ 6 files changed, 220 insertions(+) create mode 100644 Plugin_for_Chrome/client/background.js create mode 100644 Plugin_for_Chrome/client/content.js create mode 100644 Plugin_for_Chrome/client/manifest.json create mode 100644 Plugin_for_Chrome/client/popup.html create mode 100644 Plugin_for_Chrome/client/popup.js create mode 100644 Plugin_for_Chrome/server/server.py diff --git a/Plugin_for_Chrome/client/background.js b/Plugin_for_Chrome/client/background.js new file mode 100644 index 0000000..f35f5ce --- /dev/null +++ b/Plugin_for_Chrome/client/background.js @@ -0,0 +1,33 @@ +chrome.commands.onCommand.addListener((command) => { + if (command === "capture-page") { + chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { + const currentTab = tabs[0]; + + // 获取页面截图 + chrome.tabs.captureVisibleTab(null, {format: 'png'}, function(dataUrl) { + // 将截图和URL发送到服务器 + const data = { + url: currentTab.url, + screenshot: dataUrl + }; + + fetch('http://localhost:5000/upload', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data) + }) + .then(response => response.json()) + .then(result => { + // 显示结果通知 + chrome.tabs.sendMessage(currentTab.id, { + type: 'show_result', + result: result + }); + }) + .catch(error => console.error('Error:', error)); + }); + }); + } +}); diff --git a/Plugin_for_Chrome/client/content.js b/Plugin_for_Chrome/client/content.js new file mode 100644 index 0000000..b890d88 --- /dev/null +++ b/Plugin_for_Chrome/client/content.js @@ -0,0 +1,23 @@ +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.type === 'show_result') { + // 创建一个悬浮提示框 + const div = document.createElement('div'); + div.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + padding: 15px; + background: #333; + color: white; + border-radius: 5px; + z-index: 10000; + `; + div.textContent = message.result.message; + document.body.appendChild(div); + + // 3秒后自动消失 + setTimeout(() => { + div.remove(); + }, 3000); + } +}); diff --git a/Plugin_for_Chrome/client/manifest.json b/Plugin_for_Chrome/client/manifest.json new file mode 100644 index 0000000..c314d05 --- /dev/null +++ b/Plugin_for_Chrome/client/manifest.json @@ -0,0 +1,32 @@ +{ + "manifest_version": 3, + "name": "页面捕获插件", + "version": "1.0", + "description": "捕获当前页面URL和截图", + "permissions": [ + "activeTab", + "tabs", + "storage", + "commands" + ], + "background": { + "service_worker": "background.js" + }, + "action": { + "default_popup": "popup.html" + }, + "commands": { + "capture-page": { + "suggested_key": { + "default": "Ctrl+Shift+S" + }, + "description": "捕获当前页面" + } + }, + "content_scripts": [ + { + "matches": [""], + "js": ["content.js"] + } + ] +} diff --git a/Plugin_for_Chrome/client/popup.html b/Plugin_for_Chrome/client/popup.html new file mode 100644 index 0000000..d784504 --- /dev/null +++ b/Plugin_for_Chrome/client/popup.html @@ -0,0 +1,17 @@ + + + + + + + +

页面捕获插件

+

使用快捷键 Ctrl+Shift+S 捕获当前页面

+ + + diff --git a/Plugin_for_Chrome/client/popup.js b/Plugin_for_Chrome/client/popup.js new file mode 100644 index 0000000..ed247d6 --- /dev/null +++ b/Plugin_for_Chrome/client/popup.js @@ -0,0 +1,71 @@ +document.addEventListener('DOMContentLoaded', function() { + // 获取当前快捷键设置并显示 + chrome.commands.getAll((commands) => { + const captureCommand = commands.find(command => command.name === "capture-page"); + if (captureCommand && captureCommand.shortcut) { + const shortcutText = document.querySelector('p'); + shortcutText.textContent = `使用快捷键 ${captureCommand.shortcut} 捕获当前页面`; + } + }); + + // 添加手动捕获按钮(可选) + const captureButton = document.createElement('button'); + captureButton.textContent = '手动捕获'; + captureButton.style.cssText = ` + margin-top: 10px; + padding: 8px 16px; + background-color: #4CAF50; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + `; + + captureButton.addEventListener('click', function() { + chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { + const currentTab = tabs[0]; + + chrome.tabs.captureVisibleTab(null, {format: 'png'}, function(dataUrl) { + const data = { + url: currentTab.url, + screenshot: dataUrl + }; + + fetch('http://localhost:5000/upload', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data) + }) + .then(response => response.json()) + .then(result => { + // 显示结果 + const resultDiv = document.createElement('div'); + resultDiv.textContent = result.message; + resultDiv.style.cssText = ` + margin-top: 10px; + padding: 8px; + background-color: #f0f0f0; + border-radius: 4px; + `; + document.body.appendChild(resultDiv); + + // 3秒后移除结果提示 + setTimeout(() => { + resultDiv.remove(); + }, 3000); + }) + .catch(error => { + console.error('Error:', error); + const errorDiv = document.createElement('div'); + errorDiv.textContent = '发生错误,请检查服务器是否正常运行'; + errorDiv.style.color = 'red'; + document.body.appendChild(errorDiv); + }); + }); + }); + }); + + document.body.appendChild(captureButton); +}); diff --git a/Plugin_for_Chrome/server/server.py b/Plugin_for_Chrome/server/server.py new file mode 100644 index 0000000..a6a39b8 --- /dev/null +++ b/Plugin_for_Chrome/server/server.py @@ -0,0 +1,44 @@ +from flask import Flask, request, jsonify +from flask_cors import CORS +import base64 +import os +import time + +app = Flask(__name__) +CORS(app) # 启用跨域支持 + +UPLOAD_FOLDER = 'uploads' +if not os.path.exists(UPLOAD_FOLDER): + os.makedirs(UPLOAD_FOLDER) + +@app.route('/upload', methods=['POST']) +def upload_file(): + data = request.json + url = data.get('url') + screenshot = data.get('screenshot') + + # 从Base64字符串中提取图片数据 + image_data = screenshot.split(',')[1] + image_binary = base64.b64decode(image_data) + + # 生成文件名 + timestamp = time.strftime("%Y%m%d-%H%M%S") + filename = f"screenshot_{timestamp}.png" + filepath = os.path.join(UPLOAD_FOLDER, filename) + + # 保存图片 + with open(filepath, 'wb') as f: + f.write(image_binary) + + # 这里可以添加您的图片处理逻辑 + # process_image(filepath) + + return jsonify({ + "status": "success", + "message": "截图已保存并处理完成!", + "url": url, + "filename": filename + }) + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5000) From d260dd1d842fd1dc9fe08fbc2ea25e1d67a6f99a Mon Sep 17 00:00:00 2001 From: sk_han Date: Mon, 2 Dec 2024 11:34:09 +0800 Subject: [PATCH 03/42] Create Plugin_for_Chrome --- Plugin_for_Chrome/server/server.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Plugin_for_Chrome/server/server.py b/Plugin_for_Chrome/server/server.py index a6a39b8..6881318 100644 --- a/Plugin_for_Chrome/server/server.py +++ b/Plugin_for_Chrome/server/server.py @@ -11,6 +11,7 @@ if not os.path.exists(UPLOAD_FOLDER): os.makedirs(UPLOAD_FOLDER) + @app.route('/upload', methods=['POST']) def upload_file(): data = request.json @@ -40,5 +41,6 @@ def upload_file(): "filename": filename }) + if __name__ == '__main__': app.run(host='0.0.0.0', port=5000) From 1fe05c94a567db7cc45d8608f2b8b4bcbd3781d5 Mon Sep 17 00:00:00 2001 From: RRFRRF <1195131157@qq.com> Date: Mon, 2 Dec 2024 14:58:55 +0800 Subject: [PATCH 04/42] fix --- GUI/ui.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/GUI/ui.py b/GUI/ui.py index ef0e9e7..63134c8 100644 --- a/GUI/ui.py +++ b/GUI/ui.py @@ -168,10 +168,10 @@ def init_phish_test_page(self): self.visualization_display = QLabel() self.visualization_display.setAlignment(Qt.AlignCenter) self.visualization_display.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - self.visualization_display.setMinimumSize(300, 300) - visualization_layout.addWidget(self.visualization_display, 1) + self.visualization_display.setMinimumSize(300, 300) + visualization_layout.addWidget(self.visualization_display, 1) - layout.addLayout(visualization_layout, 1) + layout.addLayout(visualization_layout, 1) self.phish_test_page.setLayout(layout) From 7b5f826fdbfe3dfccc104742d1452ff1c57a0395 Mon Sep 17 00:00:00 2001 From: sk_han Date: Mon, 9 Dec 2024 20:26:48 +0800 Subject: [PATCH 05/42] =?UTF-8?q?=E6=8F=92=E4=BB=B6=E9=87=8D=E5=86=99?= =?UTF-8?q?=E5=B9=B6=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Plugin_for_Chrome/client/background.js | 88 +++++++++++++++-------- Plugin_for_Chrome/client/content.js | 23 ------ Plugin_for_Chrome/client/manifest.json | 34 +++++---- Plugin_for_Chrome/client/popup.html | 17 ----- Plugin_for_Chrome/client/popup.js | 71 ------------------ Plugin_for_Chrome/client/popup/popup.css | 49 +++++++++++++ Plugin_for_Chrome/client/popup/popup.html | 28 ++++++++ Plugin_for_Chrome/client/popup/popup.js | 46 ++++++++++++ Plugin_for_Chrome/server/app.py | 45 ++++++++++++ Plugin_for_Chrome/server/server.py | 46 ------------ 10 files changed, 242 insertions(+), 205 deletions(-) delete mode 100644 Plugin_for_Chrome/client/content.js delete mode 100644 Plugin_for_Chrome/client/popup.html delete mode 100644 Plugin_for_Chrome/client/popup.js create mode 100644 Plugin_for_Chrome/client/popup/popup.css create mode 100644 Plugin_for_Chrome/client/popup/popup.html create mode 100644 Plugin_for_Chrome/client/popup/popup.js create mode 100644 Plugin_for_Chrome/server/app.py delete mode 100644 Plugin_for_Chrome/server/server.py diff --git a/Plugin_for_Chrome/client/background.js b/Plugin_for_Chrome/client/background.js index f35f5ce..daeef8e 100644 --- a/Plugin_for_Chrome/client/background.js +++ b/Plugin_for_Chrome/client/background.js @@ -1,33 +1,61 @@ -chrome.commands.onCommand.addListener((command) => { - if (command === "capture-page") { - chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { - const currentTab = tabs[0]; - - // 获取页面截图 - chrome.tabs.captureVisibleTab(null, {format: 'png'}, function(dataUrl) { - // 将截图和URL发送到服务器 - const data = { - url: currentTab.url, - screenshot: dataUrl - }; - - fetch('http://localhost:5000/upload', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(data) - }) - .then(response => response.json()) - .then(result => { - // 显示结果通知 - chrome.tabs.sendMessage(currentTab.id, { - type: 'show_result', - result: result - }); - }) - .catch(error => console.error('Error:', error)); - }); +// 处理截图和URL获取 +async function captureTabInfo(tab) { + try { + // 获取截图 + const screenshot = await chrome.tabs.captureVisibleTab(null, { + format: 'png' + }); + + // 获取当前URL + const url = tab.url; + + // 发送到服务器进行分析 + const response = await fetch('http://localhost:5000/analyze', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + url: url, + screenshot: screenshot + }) + }); + + const result = await response.json(); + + // 将结果发送到popup + chrome.runtime.sendMessage({ + type: 'analysisResult', + data: result + }); + + } catch (error) { + console.error('Error capturing tab info:', error); + chrome.runtime.sendMessage({ + type: 'error', + data: error.message }); } +} + +// 监听快捷键命令 +chrome.commands.onCommand.addListener(async (command) => { + if (command === '_execute_action') { + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + if (tab) { + await captureTabInfo(tab); + } + } }); + +// 监听来自popup的消息 +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.type === 'analyze') { + chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => { + if (tabs[0]) { + await captureTabInfo(tabs[0]); + } + }); + } + return true; +}); \ No newline at end of file diff --git a/Plugin_for_Chrome/client/content.js b/Plugin_for_Chrome/client/content.js deleted file mode 100644 index b890d88..0000000 --- a/Plugin_for_Chrome/client/content.js +++ /dev/null @@ -1,23 +0,0 @@ -chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { - if (message.type === 'show_result') { - // 创建一个悬浮提示框 - const div = document.createElement('div'); - div.style.cssText = ` - position: fixed; - top: 20px; - right: 20px; - padding: 15px; - background: #333; - color: white; - border-radius: 5px; - z-index: 10000; - `; - div.textContent = message.result.message; - document.body.appendChild(div); - - // 3秒后自动消失 - setTimeout(() => { - div.remove(); - }, 3000); - } -}); diff --git a/Plugin_for_Chrome/client/manifest.json b/Plugin_for_Chrome/client/manifest.json index c314d05..c147490 100644 --- a/Plugin_for_Chrome/client/manifest.json +++ b/Plugin_for_Chrome/client/manifest.json @@ -1,32 +1,30 @@ { "manifest_version": 3, - "name": "页面捕获插件", + "name": "Phishing Detector", "version": "1.0", - "description": "捕获当前页面URL和截图", + "description": "Detect phishing websites using screenshot and URL analysis", "permissions": [ "activeTab", - "tabs", + "scripting", "storage", - "commands" + "tabs" ], + "host_permissions": [ + "http://localhost:5000/*" + ], + "action": { + "default_popup": "popup/popup.html" + }, "background": { "service_worker": "background.js" }, - "action": { - "default_popup": "popup.html" - }, "commands": { - "capture-page": { + "_execute_action": { "suggested_key": { - "default": "Ctrl+Shift+S" + "default": "Ctrl+Shift+H", + "mac": "Command+Shift+P" }, - "description": "捕获当前页面" - } - }, - "content_scripts": [ - { - "matches": [""], - "js": ["content.js"] + "description": "Analyze current page for phishing" } - ] -} + } +} \ No newline at end of file diff --git a/Plugin_for_Chrome/client/popup.html b/Plugin_for_Chrome/client/popup.html deleted file mode 100644 index d784504..0000000 --- a/Plugin_for_Chrome/client/popup.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - -

页面捕获插件

-

使用快捷键 Ctrl+Shift+S 捕获当前页面

- - - diff --git a/Plugin_for_Chrome/client/popup.js b/Plugin_for_Chrome/client/popup.js deleted file mode 100644 index ed247d6..0000000 --- a/Plugin_for_Chrome/client/popup.js +++ /dev/null @@ -1,71 +0,0 @@ -document.addEventListener('DOMContentLoaded', function() { - // 获取当前快捷键设置并显示 - chrome.commands.getAll((commands) => { - const captureCommand = commands.find(command => command.name === "capture-page"); - if (captureCommand && captureCommand.shortcut) { - const shortcutText = document.querySelector('p'); - shortcutText.textContent = `使用快捷键 ${captureCommand.shortcut} 捕获当前页面`; - } - }); - - // 添加手动捕获按钮(可选) - const captureButton = document.createElement('button'); - captureButton.textContent = '手动捕获'; - captureButton.style.cssText = ` - margin-top: 10px; - padding: 8px 16px; - background-color: #4CAF50; - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - `; - - captureButton.addEventListener('click', function() { - chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { - const currentTab = tabs[0]; - - chrome.tabs.captureVisibleTab(null, {format: 'png'}, function(dataUrl) { - const data = { - url: currentTab.url, - screenshot: dataUrl - }; - - fetch('http://localhost:5000/upload', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(data) - }) - .then(response => response.json()) - .then(result => { - // 显示结果 - const resultDiv = document.createElement('div'); - resultDiv.textContent = result.message; - resultDiv.style.cssText = ` - margin-top: 10px; - padding: 8px; - background-color: #f0f0f0; - border-radius: 4px; - `; - document.body.appendChild(resultDiv); - - // 3秒后移除结果提示 - setTimeout(() => { - resultDiv.remove(); - }, 3000); - }) - .catch(error => { - console.error('Error:', error); - const errorDiv = document.createElement('div'); - errorDiv.textContent = '发生错误,请检查服务器是否正常运行'; - errorDiv.style.color = 'red'; - document.body.appendChild(errorDiv); - }); - }); - }); - }); - - document.body.appendChild(captureButton); -}); diff --git a/Plugin_for_Chrome/client/popup/popup.css b/Plugin_for_Chrome/client/popup/popup.css new file mode 100644 index 0000000..d7dd44c --- /dev/null +++ b/Plugin_for_Chrome/client/popup/popup.css @@ -0,0 +1,49 @@ +.container { + width: 300px; + padding: 16px; + } + + h1 { + font-size: 18px; + margin-bottom: 16px; + } + + button { + width: 100%; + padding: 8px; + background-color: #4CAF50; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + margin-bottom: 16px; + } + + button:hover { + background-color: #45a049; + } + + .hidden { + display: none; + } + + #loading { + text-align: center; + margin: 16px 0; + } + + #result { + margin-top: 16px; + } + + .safe { + color: #4CAF50; + } + + .dangerous { + color: #f44336; + } + + .error-message { + color: #f44336; + } \ No newline at end of file diff --git a/Plugin_for_Chrome/client/popup/popup.html b/Plugin_for_Chrome/client/popup/popup.html new file mode 100644 index 0000000..5d0c9f9 --- /dev/null +++ b/Plugin_for_Chrome/client/popup/popup.html @@ -0,0 +1,28 @@ + + + + + + + +
+

Phishing Detector

+ + + + +
+ + + \ No newline at end of file diff --git a/Plugin_for_Chrome/client/popup/popup.js b/Plugin_for_Chrome/client/popup/popup.js new file mode 100644 index 0000000..d707721 --- /dev/null +++ b/Plugin_for_Chrome/client/popup/popup.js @@ -0,0 +1,46 @@ +document.addEventListener('DOMContentLoaded', () => { + const analyzeBtn = document.getElementById('analyzeBtn'); + const loading = document.getElementById('loading'); + const result = document.getElementById('result'); + const status = document.getElementById('status'); + const legitUrl = document.getElementById('legitUrl'); + const legitUrlLink = document.getElementById('legitUrlLink'); + const error = document.getElementById('error'); + + // 点击分析按钮 + analyzeBtn.addEventListener('click', () => { + // 显示加载状态 + loading.classList.remove('hidden'); + result.classList.add('hidden'); + error.classList.add('hidden'); + + // 发送消息给background script + chrome.runtime.sendMessage({ + type: 'analyze' + }); + }); + + // 监听来自background的消息 + chrome.runtime.onMessage.addListener((message) => { + loading.classList.add('hidden'); + + if (message.type === 'analysisResult') { + result.classList.remove('hidden'); + + if (message.data.isPhishing) { + status.innerHTML = '⚠️ 警告:这可能是一个钓鱼网站!'; + if (message.data.legitUrl) { + legitUrl.classList.remove('hidden'); + legitUrlLink.href = message.data.legitUrl; + legitUrlLink.textContent = message.data.legitUrl; + } + } else { + status.innerHTML = '✓ 这是一个安全的网站'; + legitUrl.classList.add('hidden'); + } + } else if (message.type === 'error') { + error.classList.remove('hidden'); + error.querySelector('.error-message').textContent = message.data; + } + }); +}); \ No newline at end of file diff --git a/Plugin_for_Chrome/server/app.py b/Plugin_for_Chrome/server/app.py new file mode 100644 index 0000000..7819cde --- /dev/null +++ b/Plugin_for_Chrome/server/app.py @@ -0,0 +1,45 @@ +from flask import Flask, request, jsonify +from flask_cors import CORS +import base64 +from io import BytesIO +from PIL import Image +import os + +app = Flask(__name__) +CORS(app) + +# 这里后续添加模型加载代码 +def load_model(): + # TODO: 加载识别模型 + pass + +# 在创建应用时初始化模型 +with app.app_context(): + load_model() + +@app.route('/analyze', methods=['POST']) +def analyze(): + try: + data = request.get_json() + url = data.get('url') + screenshot_data = data.get('screenshot') + + # 解码Base64图片数据 + image_data = base64.b64decode(screenshot_data.split(',')[1]) + image = Image.open(BytesIO(image_data)) + + # TODO: 这里添加识别逻辑 + # 目前返回示例数据 + result = { + "isPhishing": False, + "legitUrl": None, + "confidence": 0.95 + } + + return jsonify(result) + + except Exception as e: + return jsonify({"error": str(e)}), 500 + +if __name__ == '__main__': + app.run(debug=True) \ No newline at end of file diff --git a/Plugin_for_Chrome/server/server.py b/Plugin_for_Chrome/server/server.py deleted file mode 100644 index 6881318..0000000 --- a/Plugin_for_Chrome/server/server.py +++ /dev/null @@ -1,46 +0,0 @@ -from flask import Flask, request, jsonify -from flask_cors import CORS -import base64 -import os -import time - -app = Flask(__name__) -CORS(app) # 启用跨域支持 - -UPLOAD_FOLDER = 'uploads' -if not os.path.exists(UPLOAD_FOLDER): - os.makedirs(UPLOAD_FOLDER) - - -@app.route('/upload', methods=['POST']) -def upload_file(): - data = request.json - url = data.get('url') - screenshot = data.get('screenshot') - - # 从Base64字符串中提取图片数据 - image_data = screenshot.split(',')[1] - image_binary = base64.b64decode(image_data) - - # 生成文件名 - timestamp = time.strftime("%Y%m%d-%H%M%S") - filename = f"screenshot_{timestamp}.png" - filepath = os.path.join(UPLOAD_FOLDER, filename) - - # 保存图片 - with open(filepath, 'wb') as f: - f.write(image_binary) - - # 这里可以添加您的图片处理逻辑 - # process_image(filepath) - - return jsonify({ - "status": "success", - "message": "截图已保存并处理完成!", - "url": url, - "filename": filename - }) - - -if __name__ == '__main__': - app.run(host='0.0.0.0', port=5000) From a4eccaaef02f4cec764cb59bf9fcdba631dedd1a Mon Sep 17 00:00:00 2001 From: sk_han Date: Wed, 11 Dec 2024 16:31:36 +0800 Subject: [PATCH 06/42] =?UTF-8?q?=E8=A1=A5=E5=85=85=E8=AF=86=E5=88=AB?= =?UTF-8?q?=E8=BF=87=E7=A8=8B=E4=BB=A5=E5=8F=8Areadme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Plugin_for_Chrome/README.md | 60 +++++++++++++++++++++++++++++++++ Plugin_for_Chrome/server/app.py | 46 ++++++++++++++++++++----- 2 files changed, 97 insertions(+), 9 deletions(-) create mode 100644 Plugin_for_Chrome/README.md diff --git a/Plugin_for_Chrome/README.md b/Plugin_for_Chrome/README.md new file mode 100644 index 0000000..4065ba6 --- /dev/null +++ b/Plugin_for_Chrome/README.md @@ -0,0 +1,60 @@ +# Plugin_for_Chrome + +## 项目简介 + +`Plugin_for_Chrome` 是一个用于检测钓鱼网站的Chrome插件项目。该插件可以在用户按下设置好的快捷键或者点击插件按钮后,自动获取当前网页的网址以及截图,并将其发送到服务端进行钓鱼网站检测。服务端使用Flask框架,加载Phishpedia模型进行识别,并返回检测结果。 + +## 目录结构 + +``` +Plugin_for_Chrome/ +├── client/ +│ ├── background.js # 处理插件的后台逻辑,包括快捷键和按钮点击事件。 +│ ├── manifest.json # Chrome插件的配置文件。 +│ └── popup/ +│ ├── popup.html # 插件弹出页面的HTML文件。 +│ ├── popup.js # 插件弹出页面的JavaScript文件。 +│ └── popup.css # 插件弹出页面的CSS文件。 +└── server/ + └── app.py # Flask服务端的主程序,处理客户端请求并调用Phishpedia模型进行识别。 + +``` + +## 安装与使用 + +### 前端部分 + +1. 打开Chrome浏览器,进入 `chrome://extensions/`。 +2. 启用开发者模式。 +3. 点击“加载已解压的扩展程序”,选择 `Plugin_for_Chrome` 目录。 + +### 后端部分 + +1. 进入 `server` 目录: + ```sh + cd Plugin_for_Chrome/server + ``` +2. 安装所需依赖: + ```sh + pip install flask flask_cors + ``` +3. 运行Flask服务: + ```sh + python app.py + ``` + +### 使用插件 + +1. 在Chrome浏览器中,按下快捷键 `Ctrl+Shift+Y` 或点击插件按钮。 +2. 插件会自动获取当前网页的网址和截图,并发送到服务端进行检测。 +3. 服务端返回检测结果,插件会显示该网页是否为钓鱼网站,以及对应的正版网站。 + + +## 注意事项 + +- 确保服务端在本地运行,并监听默认的5000端口。 +- 插件和服务端需要在同一网络环境下运行。 + +## 贡献 + +欢迎提交问题和贡献代码! diff --git a/Plugin_for_Chrome/server/app.py b/Plugin_for_Chrome/server/app.py index 7819cde..65e16b1 100644 --- a/Plugin_for_Chrome/server/app.py +++ b/Plugin_for_Chrome/server/app.py @@ -3,23 +3,33 @@ import base64 from io import BytesIO from PIL import Image +from datetime import datetime import os +import sys + +current_dir = os.path.dirname(os.path.realpath(__file__)) +root_dir = os.path.abspath(os.path.join(current_dir, os.pardir)) +root_dir = os.path.abspath(os.path.join(root_dir, os.pardir)) +sys.path.append(root_dir) + +from phishpedia import PhishpediaWrapper +from phishpedia import result_file_write app = Flask(__name__) CORS(app) -# 这里后续添加模型加载代码 -def load_model(): - # TODO: 加载识别模型 - pass # 在创建应用时初始化模型 with app.app_context(): - load_model() + log_dir = os.path.join(current_dir, 'logs') + os.makedirs(log_dir, exist_ok=True) + global phishpedia_cls + phishpedia_cls = PhishpediaWrapper() @app.route('/analyze', methods=['POST']) def analyze(): try: + print('Request received') data = request.get_json() url = data.get('url') screenshot_data = data.get('screenshot') @@ -27,18 +37,36 @@ def analyze(): # 解码Base64图片数据 image_data = base64.b64decode(screenshot_data.split(',')[1]) image = Image.open(BytesIO(image_data)) + screenshot_path = 'temp_screenshot.png' + image.save(screenshot_path, format='PNG') + + # 调用Phishpedia模型进行识别 + phish_category, pred_target, matched_domain, \ + plotvis, siamese_conf, pred_boxes, \ + logo_recog_time, logo_match_time = phishpedia_cls.test_orig_phishpedia(url, screenshot_path, None) - # TODO: 这里添加识别逻辑 + today = datetime.now().strftime('%Y%m%d') + log_file_path = os.path.join(log_dir, f'{today}_results.txt') + + try: + with open(log_file_path, "a+", encoding='ISO-8859-1') as f: + result_file_write(f, current_dir, url, phish_category, pred_target, matched_domain, siamese_conf, + logo_recog_time, logo_match_time) + except UnicodeError: + with open(log_file_path, "a+", encoding='utf-8') as f: + result_file_write(f, current_dir, url, phish_category, pred_target, matched_domain, siamese_conf, + logo_recog_time, logo_match_time) # 目前返回示例数据 result = { - "isPhishing": False, - "legitUrl": None, - "confidence": 0.95 + "isPhishing": bool(phish_category), + "legitUrl": pred_target, + "confidence": float(siamese_conf) } return jsonify(result) except Exception as e: + print(e) return jsonify({"error": str(e)}), 500 if __name__ == '__main__': From 63b9ca1eb89034ce2ba45c90b4371db9d16b8f81 Mon Sep 17 00:00:00 2001 From: sk_han Date: Wed, 11 Dec 2024 16:32:38 +0800 Subject: [PATCH 07/42] =?UTF-8?q?=E8=B0=83=E6=95=B4=E5=BF=AB=E6=8D=B7?= =?UTF-8?q?=E9=94=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Plugin_for_Chrome/README.md | 2 +- Plugin_for_Chrome/client/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Plugin_for_Chrome/README.md b/Plugin_for_Chrome/README.md index 4065ba6..806e94e 100644 --- a/Plugin_for_Chrome/README.md +++ b/Plugin_for_Chrome/README.md @@ -45,7 +45,7 @@ Plugin_for_Chrome/ ### 使用插件 -1. 在Chrome浏览器中,按下快捷键 `Ctrl+Shift+Y` 或点击插件按钮。 +1. 在Chrome浏览器中,按下快捷键 `Ctrl+Shift+H` 然后点击插件按钮。 2. 插件会自动获取当前网页的网址和截图,并发送到服务端进行检测。 3. 服务端返回检测结果,插件会显示该网页是否为钓鱼网站,以及对应的正版网站。 diff --git a/Plugin_for_Chrome/client/manifest.json b/Plugin_for_Chrome/client/manifest.json index c147490..468e984 100644 --- a/Plugin_for_Chrome/client/manifest.json +++ b/Plugin_for_Chrome/client/manifest.json @@ -22,7 +22,7 @@ "_execute_action": { "suggested_key": { "default": "Ctrl+Shift+H", - "mac": "Command+Shift+P" + "mac": "Command+Shift+H" }, "description": "Analyze current page for phishing" } From ca566043947f0a4b7ed2a8cb0691dbef7766215b Mon Sep 17 00:00:00 2001 From: sk_han Date: Wed, 11 Dec 2024 16:43:07 +0800 Subject: [PATCH 08/42] =?UTF-8?q?=E6=A0=B9=E6=8D=AELint=E4=BD=9C=E9=83=A8?= =?UTF-8?q?=E5=88=86=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Plugin_for_Chrome/server/app.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Plugin_for_Chrome/server/app.py b/Plugin_for_Chrome/server/app.py index 65e16b1..a100bc8 100644 --- a/Plugin_for_Chrome/server/app.py +++ b/Plugin_for_Chrome/server/app.py @@ -23,9 +23,9 @@ with app.app_context(): log_dir = os.path.join(current_dir, 'logs') os.makedirs(log_dir, exist_ok=True) - global phishpedia_cls phishpedia_cls = PhishpediaWrapper() + @app.route('/analyze', methods=['POST']) def analyze(): try: @@ -69,5 +69,6 @@ def analyze(): print(e) return jsonify({"error": str(e)}), 500 + if __name__ == '__main__': - app.run(debug=True) \ No newline at end of file + app.run(debug=True) From d090494f9bf516087d477b670c6ccc4023ee9e30 Mon Sep 17 00:00:00 2001 From: sk_han Date: Thu, 12 Dec 2024 15:40:00 +0800 Subject: [PATCH 09/42] =?UTF-8?q?=E8=B0=83=E6=95=B4=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=E5=86=85=E5=AE=B9=EF=BC=8C=E4=BF=AE=E5=A4=8D=E9=83=A8=E5=88=86?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Plugin_for_Chrome/{server => }/app.py | 20 ++++++++------------ Plugin_for_Chrome/client/popup/popup.js | 2 +- Plugin_for_Chrome/logs/20241212_results.txt | 14 ++++++++++++++ 3 files changed, 23 insertions(+), 13 deletions(-) rename Plugin_for_Chrome/{server => }/app.py (84%) create mode 100644 Plugin_for_Chrome/logs/20241212_results.txt diff --git a/Plugin_for_Chrome/server/app.py b/Plugin_for_Chrome/app.py similarity index 84% rename from Plugin_for_Chrome/server/app.py rename to Plugin_for_Chrome/app.py index a100bc8..b457b32 100644 --- a/Plugin_for_Chrome/server/app.py +++ b/Plugin_for_Chrome/app.py @@ -4,16 +4,10 @@ from io import BytesIO from PIL import Image from datetime import datetime -import os -import sys +import os, sys +sys.path.append("..") +from phishpedia import PhishpediaWrapper, result_file_write -current_dir = os.path.dirname(os.path.realpath(__file__)) -root_dir = os.path.abspath(os.path.join(current_dir, os.pardir)) -root_dir = os.path.abspath(os.path.join(root_dir, os.pardir)) -sys.path.append(root_dir) - -from phishpedia import PhishpediaWrapper -from phishpedia import result_file_write app = Flask(__name__) CORS(app) @@ -21,6 +15,7 @@ # 在创建应用时初始化模型 with app.app_context(): + current_dir = os.path.dirname(os.path.realpath(__file__)) log_dir = os.path.join(current_dir, 'logs') os.makedirs(log_dir, exist_ok=True) phishpedia_cls = PhishpediaWrapper() @@ -59,10 +54,11 @@ def analyze(): # 目前返回示例数据 result = { "isPhishing": bool(phish_category), - "legitUrl": pred_target, + "brand": pred_target, + "legitUrl": "https://"+matched_domain[0], "confidence": float(siamese_conf) } - + print(matched_domain) return jsonify(result) except Exception as e: @@ -71,4 +67,4 @@ def analyze(): if __name__ == '__main__': - app.run(debug=True) + app.run(debug=False) diff --git a/Plugin_for_Chrome/client/popup/popup.js b/Plugin_for_Chrome/client/popup/popup.js index d707721..76afece 100644 --- a/Plugin_for_Chrome/client/popup/popup.js +++ b/Plugin_for_Chrome/client/popup/popup.js @@ -32,7 +32,7 @@ document.addEventListener('DOMContentLoaded', () => { if (message.data.legitUrl) { legitUrl.classList.remove('hidden'); legitUrlLink.href = message.data.legitUrl; - legitUrlLink.textContent = message.data.legitUrl; + legitUrlLink.textContent = message.data.brand; } } else { status.innerHTML = '✓ 这是一个安全的网站'; diff --git a/Plugin_for_Chrome/logs/20241212_results.txt b/Plugin_for_Chrome/logs/20241212_results.txt new file mode 100644 index 0000000..d51d4c4 --- /dev/null +++ b/Plugin_for_Chrome/logs/20241212_results.txt @@ -0,0 +1,14 @@ +C:\Users\Areedd\Documents\GitRepo\Phishpedia\Plugin_for_Chrome file:///C:/Users/Areedd/Desktop/Experience%20League%20_%20Adobe.html 1 Adobe ['adobe.com'] 0.8715651 0.6723 0.152 +C:\Users\Areedd\Documents\GitRepo\Phishpedia\Plugin_for_Chrome file:///C:/Users/Areedd/Desktop/Experience%20League%20_%20Adobe.html 1 Adobe ['adobe.com'] 0.8715651 0.0922 0.0499 +C:\Users\Areedd\Documents\GitRepo\Phishpedia\Plugin_for_Chrome file:///C:/Users/Areedd/Desktop/Experience%20League%20_%20Adobe.html 1 Adobe ['adobe.com'] 0.8715651 0.0904 0.0514 +C:\Users\Areedd\Documents\GitRepo\Phishpedia\Plugin_for_Chrome file:///C:/Users/Areedd/Desktop/Experience%20League%20_%20Adobe.html 1 Adobe ['adobe.com'] 0.8715651 0.4287 0.1309 +C:\Users\Areedd\Documents\GitRepo\Phishpedia\Plugin_for_Chrome chrome-extension://ljfmkehpohiaikajnpcjbionknfhgdho/popup/adobe.com 0 None None 0.6350205 0.445 0.1278 +C:\Users\Areedd\Documents\GitRepo\Phishpedia\Plugin_for_Chrome file:///C:/Users/Areedd/Desktop/Experience%20League%20_%20Adobe.html 1 Adobe ['adobe.com'] 0.8715651 0.0936 0.0569 +C:\Users\Areedd\Documents\GitRepo\Phishpedia\Plugin_for_Chrome file:///C:/Users/Areedd/Desktop/Experience%20League%20_%20Adobe.html 1 Adobe ['adobe.com'] 0.8715651 0.4273 0.1313 +C:\Users\Areedd\Documents\GitRepo\Phishpedia\Plugin_for_Chrome file:///C:/Users/Areedd/Desktop/Experience%20League%20_%20Adobe.html 1 Adobe ['adobe.com'] 0.8715651 0.4363 0.1363 +C:\Users\Areedd\Documents\GitRepo\Phishpedia\Plugin_for_Chrome file:///C:/Users/Areedd/Desktop/Experience%20League%20_%20Adobe.html 1 Adobe ['adobe.com'] 0.8715651 0.0858 0.0471 +C:\Users\Areedd\Documents\GitRepo\Phishpedia\Plugin_for_Chrome file:///C:/Users/Areedd/Desktop/Experience%20League%20_%20Adobe.html 1 Adobe ['adobe.com'] 0.8715651 0.1032 0.0741 +C:\Users\Areedd\Documents\GitRepo\Phishpedia\Plugin_for_Chrome https://lobe.aicnn.xyz/chat/3425a828-1832-41b0-9d3b-5874d818cc4b 0 None None 0.6955235 0.447 0.135 +C:\Users\Areedd\Documents\GitRepo\Phishpedia\Plugin_for_Chrome file:///C:/Users/Areedd/Desktop/Experience%20League%20_%20Adobe.html 1 Adobe ['adobe.com'] 0.8715651 0.0832 0.046 +C:\Users\Areedd\Documents\GitRepo\Phishpedia\Plugin_for_Chrome file:///C:/Users/Areedd/Desktop/Experience%20League%20_%20Adobe.html 1 Adobe ['adobe.com'] 0.8715651 0.0822 0.0451 +C:\Users\Areedd\Documents\GitRepo\Phishpedia\Plugin_for_Chrome file:///C:/Users/Areedd/Desktop/Experience%20League%20_%20Adobe.html 1 Adobe ['adobe.com'] 0.8715651 0.4083 0.1292 From 59b356bebb7a542ab7f4c1aefdc2a61d6ca89147 Mon Sep 17 00:00:00 2001 From: xuao1 Date: Thu, 12 Dec 2024 15:49:50 +0800 Subject: [PATCH 10/42] Update Python version from 3.8 to 3.9 in Conda environment setup to support PyQt5.sip --- setup.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.sh b/setup.sh index e9b1f05..756f0e2 100755 --- a/setup.sh +++ b/setup.sh @@ -53,8 +53,8 @@ source "$CONDA_BASE/etc/profile.d/conda.sh" if conda info --envs | grep -w "^$ENV_NAME" > /dev/null 2>&1; then echo "Activating existing Conda environment: $ENV_NAME" else - echo "Creating new Conda environment: $ENV_NAME with Python 3.8" - conda create -y -n "$ENV_NAME" python=3.8 + echo "Creating new Conda environment: $ENV_NAME with Python 3.9" + conda create -y -n "$ENV_NAME" python=3.9 fi # 8. Activate the Conda environment From 9cb9fcba91a226719116a9fccb6317d98ab880b1 Mon Sep 17 00:00:00 2001 From: xuao1 Date: Thu, 12 Dec 2024 15:57:44 +0800 Subject: [PATCH 11/42] Enhance file moving logic to include hidden files --- setup.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/setup.sh b/setup.sh index 756f0e2..1957d72 100755 --- a/setup.sh +++ b/setup.sh @@ -133,10 +133,16 @@ cd expand_targetlist || exit 1 # Exit if the directory doesn't exist # Check if there's a nested 'expand_targetlist/' directory if [ -d "expand_targetlist" ]; then echo "Nested directory 'expand_targetlist/' detected. Moving contents up..." - + + # Enable dotglob to include hidden files + shopt -s dotglob + # Move everything from the nested directory to the current directory mv expand_targetlist/* . + # Disable dotglob to revert back to normal behavior + shopt -u dotglob + # Remove the now-empty nested directory rmdir expand_targetlist cd ../ From bf51da9496e0cf5af4779dc6e29d27f92c9826ee Mon Sep 17 00:00:00 2001 From: sk_han Date: Thu, 12 Dec 2024 16:13:18 +0800 Subject: [PATCH 12/42] =?UTF-8?q?=E8=A7=A3=E5=86=B3lint=E6=8A=A5=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Plugin_for_Chrome/logs/20241212_results.txt | 14 -------------- Plugin_for_Chrome/app.py => app.py | 11 ++++++----- 2 files changed, 6 insertions(+), 19 deletions(-) delete mode 100644 Plugin_for_Chrome/logs/20241212_results.txt rename Plugin_for_Chrome/app.py => app.py (91%) diff --git a/Plugin_for_Chrome/logs/20241212_results.txt b/Plugin_for_Chrome/logs/20241212_results.txt deleted file mode 100644 index d51d4c4..0000000 --- a/Plugin_for_Chrome/logs/20241212_results.txt +++ /dev/null @@ -1,14 +0,0 @@ -C:\Users\Areedd\Documents\GitRepo\Phishpedia\Plugin_for_Chrome file:///C:/Users/Areedd/Desktop/Experience%20League%20_%20Adobe.html 1 Adobe ['adobe.com'] 0.8715651 0.6723 0.152 -C:\Users\Areedd\Documents\GitRepo\Phishpedia\Plugin_for_Chrome file:///C:/Users/Areedd/Desktop/Experience%20League%20_%20Adobe.html 1 Adobe ['adobe.com'] 0.8715651 0.0922 0.0499 -C:\Users\Areedd\Documents\GitRepo\Phishpedia\Plugin_for_Chrome file:///C:/Users/Areedd/Desktop/Experience%20League%20_%20Adobe.html 1 Adobe ['adobe.com'] 0.8715651 0.0904 0.0514 -C:\Users\Areedd\Documents\GitRepo\Phishpedia\Plugin_for_Chrome file:///C:/Users/Areedd/Desktop/Experience%20League%20_%20Adobe.html 1 Adobe ['adobe.com'] 0.8715651 0.4287 0.1309 -C:\Users\Areedd\Documents\GitRepo\Phishpedia\Plugin_for_Chrome chrome-extension://ljfmkehpohiaikajnpcjbionknfhgdho/popup/adobe.com 0 None None 0.6350205 0.445 0.1278 -C:\Users\Areedd\Documents\GitRepo\Phishpedia\Plugin_for_Chrome file:///C:/Users/Areedd/Desktop/Experience%20League%20_%20Adobe.html 1 Adobe ['adobe.com'] 0.8715651 0.0936 0.0569 -C:\Users\Areedd\Documents\GitRepo\Phishpedia\Plugin_for_Chrome file:///C:/Users/Areedd/Desktop/Experience%20League%20_%20Adobe.html 1 Adobe ['adobe.com'] 0.8715651 0.4273 0.1313 -C:\Users\Areedd\Documents\GitRepo\Phishpedia\Plugin_for_Chrome file:///C:/Users/Areedd/Desktop/Experience%20League%20_%20Adobe.html 1 Adobe ['adobe.com'] 0.8715651 0.4363 0.1363 -C:\Users\Areedd\Documents\GitRepo\Phishpedia\Plugin_for_Chrome file:///C:/Users/Areedd/Desktop/Experience%20League%20_%20Adobe.html 1 Adobe ['adobe.com'] 0.8715651 0.0858 0.0471 -C:\Users\Areedd\Documents\GitRepo\Phishpedia\Plugin_for_Chrome file:///C:/Users/Areedd/Desktop/Experience%20League%20_%20Adobe.html 1 Adobe ['adobe.com'] 0.8715651 0.1032 0.0741 -C:\Users\Areedd\Documents\GitRepo\Phishpedia\Plugin_for_Chrome https://lobe.aicnn.xyz/chat/3425a828-1832-41b0-9d3b-5874d818cc4b 0 None None 0.6955235 0.447 0.135 -C:\Users\Areedd\Documents\GitRepo\Phishpedia\Plugin_for_Chrome file:///C:/Users/Areedd/Desktop/Experience%20League%20_%20Adobe.html 1 Adobe ['adobe.com'] 0.8715651 0.0832 0.046 -C:\Users\Areedd\Documents\GitRepo\Phishpedia\Plugin_for_Chrome file:///C:/Users/Areedd/Desktop/Experience%20League%20_%20Adobe.html 1 Adobe ['adobe.com'] 0.8715651 0.0822 0.0451 -C:\Users\Areedd\Documents\GitRepo\Phishpedia\Plugin_for_Chrome file:///C:/Users/Areedd/Desktop/Experience%20League%20_%20Adobe.html 1 Adobe ['adobe.com'] 0.8715651 0.4083 0.1292 diff --git a/Plugin_for_Chrome/app.py b/app.py similarity index 91% rename from Plugin_for_Chrome/app.py rename to app.py index b457b32..81641a5 100644 --- a/Plugin_for_Chrome/app.py +++ b/app.py @@ -4,8 +4,8 @@ from io import BytesIO from PIL import Image from datetime import datetime -import os, sys -sys.path.append("..") +import os +import sys from phishpedia import PhishpediaWrapper, result_file_write @@ -16,7 +16,7 @@ # 在创建应用时初始化模型 with app.app_context(): current_dir = os.path.dirname(os.path.realpath(__file__)) - log_dir = os.path.join(current_dir, 'logs') + log_dir = os.path.join(current_dir, 'plugin_logs') os.makedirs(log_dir, exist_ok=True) phishpedia_cls = PhishpediaWrapper() @@ -55,10 +55,11 @@ def analyze(): result = { "isPhishing": bool(phish_category), "brand": pred_target, - "legitUrl": "https://"+matched_domain[0], + "legitUrl": "https://" + matched_domain[0], "confidence": float(siamese_conf) } - print(matched_domain) + if os.path.exists(screenshot_path): + os.remove(screenshot_path) return jsonify(result) except Exception as e: From 1b4176537d08569d1a684744c521fae4fe3b72b5 Mon Sep 17 00:00:00 2001 From: sk_han Date: Thu, 12 Dec 2024 16:20:24 +0800 Subject: [PATCH 13/42] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=8A=A5=E9=94=99?= =?UTF-8?q?=E6=97=A5=E5=BF=97=EF=BC=8C=E4=BC=98=E5=8C=96=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E5=AF=BC=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app.py b/app.py index 81641a5..9fc7eb4 100644 --- a/app.py +++ b/app.py @@ -5,7 +5,6 @@ from PIL import Image from datetime import datetime import os -import sys from phishpedia import PhishpediaWrapper, result_file_write @@ -64,7 +63,10 @@ def analyze(): except Exception as e: print(e) - return jsonify({"error": str(e)}), 500 + log_error_path = os.path.join(log_dir, f'log_error.txt') + with open(log_error_path, "a+", encoding='utf-8') as f: + f.write(f'{datetime.now().strftime("%Y-%m-%d %H:%M:%S")} - {str(e)}\n') + return jsonify("ERROR"), 500 if __name__ == '__main__': From 9a357a96009065133442971a24ef65288d48993b Mon Sep 17 00:00:00 2001 From: sk_han Date: Thu, 12 Dec 2024 16:23:53 +0800 Subject: [PATCH 14/42] =?UTF-8?q?=E8=A7=A3=E5=86=B3lint=E6=8A=A5=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.py b/app.py index 9fc7eb4..ff95865 100644 --- a/app.py +++ b/app.py @@ -63,7 +63,7 @@ def analyze(): except Exception as e: print(e) - log_error_path = os.path.join(log_dir, f'log_error.txt') + log_error_path = os.path.join(log_dir, 'log_error.txt') with open(log_error_path, "a+", encoding='utf-8') as f: f.write(f'{datetime.now().strftime("%Y-%m-%d %H:%M:%S")} - {str(e)}\n') return jsonify("ERROR"), 500 From a51a2f2b47def591549898da13bb50a178c617b6 Mon Sep 17 00:00:00 2001 From: xuao1 Date: Sat, 21 Dec 2024 21:24:32 +0800 Subject: [PATCH 15/42] Update numpy version in requirements.txt to 1.23.0 to ensure compatibility --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3b048ba..8811e99 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ scipy tldextract opencv-python pandas -numpy +numpy==1.23.0 tqdm Pillow==8.4.0 pathlib From a143cfa8c541981115ad16938ac15cf7aa66475e Mon Sep 17 00:00:00 2001 From: xuao1 Date: Sat, 21 Dec 2024 21:27:12 +0800 Subject: [PATCH 16/42] update Dockerfile --- dockerfile | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/dockerfile b/dockerfile index 15f4cde..387563f 100644 --- a/dockerfile +++ b/dockerfile @@ -11,8 +11,8 @@ RUN apt-get update && apt-get install -y wget && \ ENV PATH="/opt/miniconda/bin:${PATH}" # Set working directory WORKDIR /workspace -# Install git -RUN apt-get install -y git +# Install git, unzip and other dependencies +RUN apt-get install -y git unzip libgl1-mesa-glx libglib2.0-0 # Clone the Phishpedia project from GitHub into the container RUN git clone https://github.com/lindsey98/Phishpedia.git /workspace/Phishpedia # Change to the project directory and run setup.sh to configure the environment @@ -22,6 +22,8 @@ RUN apt-get install -y dos2unix # Convert setup.sh to Unix format and RUN it RUN dos2unix setup.sh RUN chmod +x setup.sh -RUN bash setup.sh +RUN bash setup.sh || true +# Ensure Conda is initialized and phishpedia environment is activated by default +RUN echo "source /opt/miniconda/etc/profile.d/conda.sh && conda activate phishpedia" >> ~/.bashrc # Set the default command to execute when the container starts -CMD ["bash", "-c", "cd /workspace/Phishpedia && /bin/bash"] \ No newline at end of file +CMD ["bash", "-c", "source /opt/miniconda/etc/profile.d/conda.sh && conda activate phishpedia && cd /workspace/Phishpedia && /bin/bash"] \ No newline at end of file From 14c8739746d1bde57ed415dfd96f02997cf9a00f Mon Sep 17 00:00:00 2001 From: RRFRRF <1195131157@qq.com> Date: Thu, 26 Dec 2024 22:21:42 +0800 Subject: [PATCH 17/42] =?UTF-8?q?update=20GUI=20=E6=94=AF=E6=8C=81=20?= =?UTF-8?q?=E5=A2=9E=E5=88=A0=E6=94=B9=E6=9F=A5=20logo=E5=BA=93,=20?= =?UTF-8?q?=E5=90=8C=E6=97=B6=E6=9B=B4=E6=96=B0=E5=AF=B9=E5=BA=94=E7=9A=84?= =?UTF-8?q?domain=5Fmap.pkl=E4=B8=AD=E7=9A=84=E6=9D=A1=E7=9B=AE=EF=BC=8C?= =?UTF-8?q?=20=E5=A2=9E=E5=8A=A0reload=E6=8C=89=E9=92=AE=EF=BC=8C=E5=9C=A8?= =?UTF-8?q?=E5=AF=B9=E6=95=B0=E6=8D=AE=E9=9B=86=E8=BF=9B=E8=A1=8C=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=E5=90=8Ereload=20=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- GUI/function.py | 295 +++++++++++++++++++++++++++++++++++++++++++++++- GUI/ui.py | 76 ++++++++++--- 2 files changed, 357 insertions(+), 14 deletions(-) diff --git a/GUI/function.py b/GUI/function.py index d4d0d3e..8f851ca 100644 --- a/GUI/function.py +++ b/GUI/function.py @@ -1,8 +1,12 @@ -from PyQt5.QtWidgets import QFileDialog +from PyQt5.QtWidgets import QFileDialog, QDialog, QVBoxLayout, QLabel, QTreeWidgetItem, QInputDialog, QMessageBox from PyQt5.QtGui import QPixmap, QImage from PyQt5.QtCore import Qt from phishpedia import PhishpediaWrapper import cv2 +import os +import shutil +from configs import load_config +import pickle class PhishpediaFunction: @@ -80,3 +84,292 @@ def update_image_display(self): def on_resize(self, event): self.update_image_display() + + def get_directory_structure(self, path): + import os + directory_structure = {} + for root, dirs, files in os.walk(path): + # Get relative path + relative_path = os.path.relpath(root, path) + # Skip the root directory (.) + if relative_path == '.': + continue + # Store directory structure + directory_structure[relative_path] = files + return directory_structure + + def on_item_clicked(self, item, column): + # Check if it's a logo file (child item) + if item.parent() is not None: # Only for logo files (child items) + logo_path = f"models/expand_targetlist/{item.parent().text(0)}/{item.text(0)}" + if logo_path.endswith('.png'): + self.show_logo_image(logo_path) + + def show_logo_image(self, logo_path): + try: + image = QImage(logo_path) + if image.isNull(): + QMessageBox.warning(self.ui, "Warning", f"Failed to load image: {logo_path}") + return + + # Scale image if it's too large + max_width = 800 + max_height = 600 + if image.width() > max_width or image.height() > max_height: + image = image.scaled(max_width, max_height, Qt.KeepAspectRatio, Qt.SmoothTransformation) + + pixmap = QPixmap.fromImage(image) + + # Create dialog with a title showing the logo name + dialog = QDialog(self.ui) + dialog.setWindowTitle(f"Logo Image - {os.path.basename(logo_path)}") + + # Set dialog size based on image size plus padding + dialog.resize(pixmap.width() + 40, pixmap.height() + 40) + + # Center the image in the dialog + dialog_layout = QVBoxLayout() + image_label = QLabel() + image_label.setPixmap(pixmap) + image_label.setAlignment(Qt.AlignCenter) + dialog_layout.addWidget(image_label) + + dialog.setLayout(dialog_layout) + dialog.exec_() + except Exception as e: + QMessageBox.critical(self.ui, "Error", f"Error displaying image: {str(e)}") + + def populate_tree(self, tree_widget, directory_structure): + for brand, logos in directory_structure.items(): + brand_item = QTreeWidgetItem([brand]) + for logo in logos: + logo_item = QTreeWidgetItem([logo]) + brand_item.addChild(logo_item) + tree_widget.addTopLevelItem(brand_item) + + def add_brand(self): + from PyQt5.QtWidgets import QInputDialog, QLineEdit + import os + + # Get brand name + brand_name, ok = QInputDialog.getText(self.ui, 'Add Brand', 'Enter brand name:') + + if ok and brand_name: + # Validate brand name + if not all(c.isalnum() or c.isspace() or c in '-_' for c in brand_name): + QMessageBox.warning(self.ui, "Warning", "Brand name can only contain letters, numbers, spaces, hyphens and underscores!") + return + + # Get domain names (supports multiple domains separated by commas) + domains, ok = QInputDialog.getText(self.ui, 'Add Domains', + 'Enter domain names, separated by commas\nExample: example.com, test.example.com', + QLineEdit.Normal) + if not ok or not domains: + QMessageBox.warning(self.ui, "Warning", "Domain name is required!") + return + + # Create brand directory + brand_path = os.path.join('models/expand_targetlist', brand_name) + try: + if not os.path.exists(brand_path): + os.makedirs(brand_path) + # Update tree view + brand_item = QTreeWidgetItem([brand_name]) + self.ui.tree_widget.addTopLevelItem(brand_item) + + # Update domain mapping + if self.domain_map_add(brand_name, domains): + QMessageBox.information(self.ui, "Success", + "Brand and domains added successfully!\nPlease click 'Reload Model' button to reload the models.") + else: + QMessageBox.warning(self.ui, "Warning", "Brand already exists!") + except Exception as e: + QMessageBox.critical(self.ui, "Error", f"Failed to create brand: {str(e)}") + + def delete_brand(self): + # Get selected item + selected_item = self.ui.tree_widget.currentItem() + + if selected_item and not selected_item.parent(): # Ensure it's a brand (top-level directory) + brand_name = selected_item.text(0) + brand_path = os.path.join('models/expand_targetlist', brand_name) + + # Protect root directory + if brand_path == 'models/expand_targetlist': + QMessageBox.warning(self.ui, "Warning", "Cannot delete root directory!") + return + + # Confirm deletion + reply = QMessageBox.question(self.ui, 'Confirm Delete', + f'Are you sure you want to delete brand "{brand_name}" and all its logos?\nThis will also delete the corresponding domain mapping.', + QMessageBox.Yes | QMessageBox.No, QMessageBox.No) + + if reply == QMessageBox.Yes: + try: + # Delete directory and its contents + shutil.rmtree(brand_path) + # Remove from tree view + self.ui.tree_widget.takeTopLevelItem( + self.ui.tree_widget.indexOfTopLevelItem(selected_item)) + + # Update domain mapping + self.domain_map_delete(brand_name) + + QMessageBox.information(self.ui, "Success", + "Brand and domains deleted successfully!\nPlease click 'Reload Model' button to reload the models.") + except Exception as e: + QMessageBox.critical(self.ui, "Error", f"Failed to delete brand: {str(e)}") + else: + QMessageBox.warning(self.ui, "Warning", "Please select a brand to delete!") + + def reload_models(self): + """Reload models and domain mapping""" + try: + load_config(reload_targetlist=True) + # Reinitialize Phishpedia + self.phishpedia_cls = PhishpediaWrapper() + QMessageBox.information(self.ui, "Success", "Models reloaded successfully!") + except Exception as e: + QMessageBox.critical(self.ui, "Error", f"Failed to reload models: {str(e)}") + + def add_logo(self): + # Get selected brand item + selected_item = self.ui.tree_widget.currentItem() + + if selected_item and not selected_item.parent(): # Ensure it's a brand (top-level directory) + brand_name = selected_item.text(0) + + # Open file dialog + options = QFileDialog.Options() + file_name, _ = QFileDialog.getOpenFileName( + self.ui, "Select Logo Image", "", + "PNG Images (*.png)", options=options) + + if file_name: + # Get target path + target_dir = os.path.join('models/expand_targetlist', brand_name) + base_name = os.path.basename(file_name) + target_file = os.path.join(target_dir, base_name) + + # Check if file already exists + if os.path.exists(target_file): + QMessageBox.warning(self.ui, "Warning", f"A logo with name '{base_name}' already exists!") + return + + try: + # Copy file to target directory + shutil.copy2(file_name, target_file) + # Update tree view + logo_item = QTreeWidgetItem([base_name]) + selected_item.addChild(logo_item) + except Exception as e: + QMessageBox.critical(self.ui, "Error", f"Failed to add logo: {str(e)}") + else: + QMessageBox.warning(self.ui, "Warning", "Please select a brand first!") + + def delete_logo(self): + # Get selected item + selected_item = self.ui.tree_widget.currentItem() + + if selected_item and selected_item.parent(): # Ensure it's a logo (child item) + brand_name = selected_item.parent().text(0) + logo_name = selected_item.text(0) + + # Confirm deletion + reply = QMessageBox.question(self.ui, 'Confirm Delete', + f'Are you sure you want to delete logo "{logo_name}"?', + QMessageBox.Yes | QMessageBox.No, QMessageBox.No) + + if reply == QMessageBox.Yes: + # Build file path + logo_path = os.path.join('models/expand_targetlist', brand_name, logo_name) + + try: + # Delete file + os.remove(logo_path) + # Remove from tree view + selected_item.parent().removeChild(selected_item) + except Exception as e: + QMessageBox.critical(self.ui, "Error", f"Failed to delete logo: {str(e)}") + else: + QMessageBox.warning(self.ui, "Warning", "Please select a logo to delete!") + + def domain_map_add(self, brand_name: str, domains_str: str) -> bool: + """Add brand and domains to domain_map.pkl + Args: + brand_name: Brand name + domains_str: Domain string, multiple domains separated by commas + Returns: + bool: Whether the addition was successful + """ + try: + domain_map_path = 'models/domain_map.pkl' + + # Process domain string, split and clean whitespace + domains = [domain.strip() for domain in domains_str.split(',') if domain.strip()] + + if not domains: + QMessageBox.warning(self.ui, "Warning", "Please enter at least one valid domain!") + return False + + # Load existing domain mapping + with open(domain_map_path, 'rb') as f: + domain_map = pickle.load(f) + + # Add new brand and domains + if brand_name in domain_map: + if isinstance(domain_map[brand_name], list): + # Add new domains, avoid duplicates + existing_domains = set(domain_map[brand_name]) + for domain in domains: + if domain not in existing_domains: + domain_map[brand_name].append(domain) + else: + # If current value is not a list, convert to list + old_domain = domain_map[brand_name] + domain_map[brand_name] = [old_domain] + [d for d in domains if d != old_domain] + else: + domain_map[brand_name] = domains + + # Save updated mapping + with open(domain_map_path, 'wb') as f: + pickle.dump(domain_map, f) + + # Display added domains + domains_added = '\n'.join(f" - {d}" for d in domains) + QMessageBox.information(self.ui, "Success", + f"Added the following domains to brand '{brand_name}':\n{domains_added}") + + return True + + except Exception as e: + QMessageBox.critical(self.ui, "Error", f"Failed to update domain mapping: {str(e)}") + return False + + def domain_map_delete(self, brand_name: str) -> bool: + """Delete brand and its domains from domain_map.pkl + Args: + brand_name: Brand name to delete + Returns: + bool: Whether the deletion was successful + """ + try: + domain_map_path = 'models/domain_map.pkl' + + # Load existing domain mapping + with open(domain_map_path, 'rb') as f: + domain_map = pickle.load(f) + + # Delete brand and its domains + if brand_name in domain_map: + del domain_map[brand_name] + + # Save updated mapping + with open(domain_map_path, 'wb') as f: + pickle.dump(domain_map, f) + + return True + + except Exception as e: + QMessageBox.critical(self.ui, "Error", f"Failed to delete domain mapping: {str(e)}") + return False diff --git a/GUI/ui.py b/GUI/ui.py index 63134c8..88cc139 100644 --- a/GUI/ui.py +++ b/GUI/ui.py @@ -1,5 +1,5 @@ -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QLineEdit, QPushButton, QTextEdit, QHBoxLayout, QTabWidget, QSizePolicy -from PyQt5.QtGui import QFont +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QLineEdit, QPushButton, QTextEdit, QHBoxLayout, QTabWidget, QSizePolicy, QTreeWidget, QTreeWidgetItem, QDialog +from PyQt5.QtGui import QFont, QImage, QPixmap from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QApplication from .function import PhishpediaFunction @@ -26,11 +26,6 @@ def initUI(self): self.init_phish_test_page() self.tab_widget.addTab(self.phish_test_page, "PhishTest") - # Import Model Page - self.import_model_page = QWidget() - self.init_import_model_page() - self.tab_widget.addTab(self.import_model_page, "Import Model") - # Dataset Page self.dataset_page = QWidget() self.init_dataset_page() @@ -112,8 +107,8 @@ def initUI(self): def set_dynamic_font_size(self): screen = QApplication.primaryScreen() dpi = screen.logicalDotsPerInch() - base_font_size = 16 # Base font size for 150 DPI - font_size = base_font_size * (dpi / 150) + base_font_size = 16 # Base font size for 200 DPI + font_size = base_font_size * (dpi / 175) font = QFont() font.setPointSizeF(font_size) @@ -175,12 +170,67 @@ def init_phish_test_page(self): self.phish_test_page.setLayout(layout) - def init_import_model_page(self): - layout = QVBoxLayout() - self.import_model_page.setLayout(layout) - def init_dataset_page(self): layout = QVBoxLayout() + + # Get directory structure + directory_structure = self.function.get_directory_structure('models/expand_targetlist') + + # Create button layout + button_layout = QHBoxLayout() + + # Create buttons + self.add_brand_btn = QPushButton("Add Brand") + self.delete_brand_btn = QPushButton("Delete Brand") + self.add_logo_btn = QPushButton("Add Logo") + self.delete_logo_btn = QPushButton("Delete Logo") + + # Add buttons to layout + button_layout.addWidget(self.add_brand_btn) + button_layout.addWidget(self.delete_brand_btn) + button_layout.addWidget(self.add_logo_btn) + button_layout.addWidget(self.delete_logo_btn) + + # Connect button click events + self.add_brand_btn.clicked.connect(self.function.add_brand) + self.delete_brand_btn.clicked.connect(self.function.delete_brand) + self.add_logo_btn.clicked.connect(self.function.add_logo) + self.delete_logo_btn.clicked.connect(self.function.delete_logo) + + # Create tree view + self.tree_widget = QTreeWidget() + self.tree_widget.setHeaderLabel("Brand Logos") + self.tree_widget.itemDoubleClicked.connect(self.function.on_item_clicked) + + # Populate tree view + self.function.populate_tree(self.tree_widget, directory_structure) + + # Create reload model button + reload_layout = QHBoxLayout() + self.reload_model_btn = QPushButton("Reload Model") + self.reload_model_btn.setStyleSheet(""" + QPushButton { + background-color: #4CAF50; + color: white; + padding: 8px 16px; + border-radius: 6px; + font-weight: bold; + } + QPushButton:hover { + background-color: #45a049; + } + """) + reload_layout.addStretch() + reload_layout.addWidget(self.reload_model_btn) + + # Connect reload model button event + self.reload_model_btn.clicked.connect(self.function.reload_models) + + # Add all components to main layout + layout.addLayout(button_layout) + layout.addWidget(self.tree_widget) + layout.addLayout(reload_layout) # Add reload button layout + self.dataset_page.setLayout(layout) def resizeEvent(self, event): From 3e6ca58e4e06af179daf0cd29b17d691ac204cdc Mon Sep 17 00:00:00 2001 From: RRFRRF <1195131157@qq.com> Date: Thu, 26 Dec 2024 22:27:13 +0800 Subject: [PATCH 18/42] fix --- GUI/function.py | 84 ++++++++++++++++++++++++++++++++++--------------- GUI/ui.py | 7 +++-- 2 files changed, 64 insertions(+), 27 deletions(-) diff --git a/GUI/function.py b/GUI/function.py index 8f851ca..55cb883 100644 --- a/GUI/function.py +++ b/GUI/function.py @@ -1,12 +1,14 @@ -from PyQt5.QtWidgets import QFileDialog, QDialog, QVBoxLayout, QLabel, QTreeWidgetItem, QInputDialog, QMessageBox +from PyQt5.QtWidgets import ( + QMessageBox, QFileDialog, QTreeWidgetItem, QInputDialog, QLineEdit, QDialog, QVBoxLayout, QLabel +) from PyQt5.QtGui import QPixmap, QImage from PyQt5.QtCore import Qt -from phishpedia import PhishpediaWrapper import cv2 import os import shutil -from configs import load_config import pickle +from configs import load_config +from phishpedia import PhishpediaWrapper class PhishpediaFunction: @@ -148,22 +150,26 @@ def populate_tree(self, tree_widget, directory_structure): tree_widget.addTopLevelItem(brand_item) def add_brand(self): - from PyQt5.QtWidgets import QInputDialog, QLineEdit - import os - # Get brand name brand_name, ok = QInputDialog.getText(self.ui, 'Add Brand', 'Enter brand name:') if ok and brand_name: # Validate brand name if not all(c.isalnum() or c.isspace() or c in '-_' for c in brand_name): - QMessageBox.warning(self.ui, "Warning", "Brand name can only contain letters, numbers, spaces, hyphens and underscores!") + QMessageBox.warning( + self.ui, + "Warning", + "Brand name can only contain letters, numbers, spaces, hyphens and underscores!" + ) return # Get domain names (supports multiple domains separated by commas) - domains, ok = QInputDialog.getText(self.ui, 'Add Domains', - 'Enter domain names, separated by commas\nExample: example.com, test.example.com', - QLineEdit.Normal) + domains, ok = QInputDialog.getText( + self.ui, + 'Add Domains', + 'Enter domain names, separated by commas\nExample: example.com, test.example.com', + QLineEdit.Normal + ) if not ok or not domains: QMessageBox.warning(self.ui, "Warning", "Domain name is required!") return @@ -179,8 +185,11 @@ def add_brand(self): # Update domain mapping if self.domain_map_add(brand_name, domains): - QMessageBox.information(self.ui, "Success", - "Brand and domains added successfully!\nPlease click 'Reload Model' button to reload the models.") + QMessageBox.information( + self.ui, + "Success", + "Brand and domains added successfully!\nPlease click 'Reload Model' button to reload the models." + ) else: QMessageBox.warning(self.ui, "Warning", "Brand already exists!") except Exception as e: @@ -200,9 +209,14 @@ def delete_brand(self): return # Confirm deletion - reply = QMessageBox.question(self.ui, 'Confirm Delete', - f'Are you sure you want to delete brand "{brand_name}" and all its logos?\nThis will also delete the corresponding domain mapping.', - QMessageBox.Yes | QMessageBox.No, QMessageBox.No) + reply = QMessageBox.question( + self.ui, + 'Confirm Delete', + f'Are you sure you want to delete brand "{brand_name}" and all its logos?\n' + 'This will also delete the corresponding domain mapping.', + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) if reply == QMessageBox.Yes: try: @@ -210,13 +224,18 @@ def delete_brand(self): shutil.rmtree(brand_path) # Remove from tree view self.ui.tree_widget.takeTopLevelItem( - self.ui.tree_widget.indexOfTopLevelItem(selected_item)) + self.ui.tree_widget.indexOfTopLevelItem(selected_item) + ) # Update domain mapping self.domain_map_delete(brand_name) - QMessageBox.information(self.ui, "Success", - "Brand and domains deleted successfully!\nPlease click 'Reload Model' button to reload the models.") + QMessageBox.information( + self.ui, + "Success", + "Brand and domains deleted successfully!\n" + "Please click 'Reload Model' button to reload the models." + ) except Exception as e: QMessageBox.critical(self.ui, "Error", f"Failed to delete brand: {str(e)}") else: @@ -276,9 +295,13 @@ def delete_logo(self): logo_name = selected_item.text(0) # Confirm deletion - reply = QMessageBox.question(self.ui, 'Confirm Delete', - f'Are you sure you want to delete logo "{logo_name}"?', - QMessageBox.Yes | QMessageBox.No, QMessageBox.No) + reply = QMessageBox.question( + self.ui, + 'Confirm Delete', + f'Are you sure you want to delete logo "{logo_name}"?', + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) if reply == QMessageBox.Yes: # Build file path @@ -337,13 +360,20 @@ def domain_map_add(self, brand_name: str, domains_str: str) -> bool: # Display added domains domains_added = '\n'.join(f" - {d}" for d in domains) - QMessageBox.information(self.ui, "Success", - f"Added the following domains to brand '{brand_name}':\n{domains_added}") + QMessageBox.information( + self.ui, + "Success", + f"Added the following domains to brand '{brand_name}':\n{domains_added}" + ) return True except Exception as e: - QMessageBox.critical(self.ui, "Error", f"Failed to update domain mapping: {str(e)}") + QMessageBox.critical( + self.ui, + "Error", + f"Failed to update domain mapping: {str(e)}" + ) return False def domain_map_delete(self, brand_name: str) -> bool: @@ -371,5 +401,9 @@ def domain_map_delete(self, brand_name: str) -> bool: return True except Exception as e: - QMessageBox.critical(self.ui, "Error", f"Failed to delete domain mapping: {str(e)}") + QMessageBox.critical( + self.ui, + "Error", + f"Failed to delete domain mapping: {str(e)}" + ) return False diff --git a/GUI/ui.py b/GUI/ui.py index 88cc139..c093c66 100644 --- a/GUI/ui.py +++ b/GUI/ui.py @@ -1,7 +1,10 @@ -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QLineEdit, QPushButton, QTextEdit, QHBoxLayout, QTabWidget, QSizePolicy, QTreeWidget, QTreeWidgetItem, QDialog +from PyQt5.QtWidgets import ( + QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, + QPushButton, QTextEdit, QTabWidget, QSizePolicy, QTreeWidget, + QTreeWidgetItem, QDialog +) from PyQt5.QtGui import QFont, QImage, QPixmap from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QApplication from .function import PhishpediaFunction From 4701651bf31444d4a19d458cb65417ac8e80467a Mon Sep 17 00:00:00 2001 From: RRFRRF <1195131157@qq.com> Date: Thu, 26 Dec 2024 22:32:32 +0800 Subject: [PATCH 19/42] fix flask --- GUI/ui.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/GUI/ui.py b/GUI/ui.py index c093c66..ac5e151 100644 --- a/GUI/ui.py +++ b/GUI/ui.py @@ -1,13 +1,11 @@ from PyQt5.QtWidgets import ( - QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, - QPushButton, QTextEdit, QTabWidget, QSizePolicy, QTreeWidget, - QTreeWidgetItem, QDialog + QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, + QPushButton, QTextEdit, QTabWidget, QSizePolicy, QTreeWidget ) -from PyQt5.QtGui import QFont, QImage, QPixmap +from PyQt5.QtGui import QFont from PyQt5.QtCore import Qt from .function import PhishpediaFunction - class PhishpediaUI(QWidget): def __init__(self): super().__init__() @@ -238,4 +236,4 @@ def init_dataset_page(self): def resizeEvent(self, event): super().resizeEvent(event) - self.function.on_resize(event) + self.function.on_resize(event) \ No newline at end of file From e6024605bb1292d035750386f3bef3bd07ceaa18 Mon Sep 17 00:00:00 2001 From: RRFRRF <1195131157@qq.com> Date: Thu, 26 Dec 2024 22:37:21 +0800 Subject: [PATCH 20/42] fix flask --- GUI/ui.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/GUI/ui.py b/GUI/ui.py index ac5e151..3f8954e 100644 --- a/GUI/ui.py +++ b/GUI/ui.py @@ -6,6 +6,7 @@ from PyQt5.QtCore import Qt from .function import PhishpediaFunction + class PhishpediaUI(QWidget): def __init__(self): super().__init__() @@ -236,4 +237,4 @@ def init_dataset_page(self): def resizeEvent(self, event): super().resizeEvent(event) - self.function.on_resize(event) \ No newline at end of file + self.function.on_resize(event) From 455adc0850edc8c92da61dbb4dfd67cafe4a3a36 Mon Sep 17 00:00:00 2001 From: RRFRRF <1195131157@qq.com> Date: Thu, 26 Dec 2024 23:03:32 +0800 Subject: [PATCH 21/42] update GUI --- GUI/function.py | 13 +++----- GUI/ui.py | 84 ++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 84 insertions(+), 13 deletions(-) diff --git a/GUI/function.py b/GUI/function.py index 55cb883..4d89c66 100644 --- a/GUI/function.py +++ b/GUI/function.py @@ -88,16 +88,11 @@ def on_resize(self, event): self.update_image_display() def get_directory_structure(self, path): - import os directory_structure = {} - for root, dirs, files in os.walk(path): - # Get relative path - relative_path = os.path.relpath(root, path) - # Skip the root directory (.) - if relative_path == '.': - continue - # Store directory structure - directory_structure[relative_path] = files + for item in os.listdir(path): + item_path = os.path.join(path, item) + if os.path.isdir(item_path): + directory_structure[item] = [f for f in os.listdir(item_path) if os.path.isfile(os.path.join(item_path, f))] return directory_structure def on_item_clicked(self, item, column): diff --git a/GUI/ui.py b/GUI/ui.py index 3f8954e..d1aabf9 100644 --- a/GUI/ui.py +++ b/GUI/ui.py @@ -187,11 +187,42 @@ def init_dataset_page(self): self.add_logo_btn = QPushButton("Add Logo") self.delete_logo_btn = QPushButton("Delete Logo") + # 设置按钮样式 + button_style = """ + QPushButton { + background-color: #F0F0F0; + color: #333; + padding: 10px 20px; + border-radius: 8px; + font-size: 14px; + font-weight: 500; + border: 1px solid #D0D0D0; + margin: 0 5px; + } + QPushButton:hover { + background-color: #E0E0E0; + } + QPushButton:pressed { + background-color: #D0D0D0; + } + """ + + self.add_brand_btn.setStyleSheet(button_style) + self.delete_brand_btn.setStyleSheet(button_style) + self.add_logo_btn.setStyleSheet(button_style) + self.delete_logo_btn.setStyleSheet(button_style) + + # 设置按钮大小策略 + for btn in [self.add_brand_btn, self.delete_brand_btn, self.add_logo_btn, self.delete_logo_btn]: + btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + btn.setMinimumHeight(40) + # Add buttons to layout button_layout.addWidget(self.add_brand_btn) button_layout.addWidget(self.delete_brand_btn) button_layout.addWidget(self.add_logo_btn) button_layout.addWidget(self.delete_logo_btn) + button_layout.setSpacing(10) # 设置按钮之间的间距 # Connect button click events self.add_brand_btn.clicked.connect(self.function.add_brand) @@ -204,24 +235,69 @@ def init_dataset_page(self): self.tree_widget.setHeaderLabel("Brand Logos") self.tree_widget.itemDoubleClicked.connect(self.function.on_item_clicked) + # 优化树形控件样式 + tree_style = """ + QTreeWidget { + background-color: #FFFFFF; + alternate-background-color: #F5F5F5; + border: 1px solid #E0E0E0; + border-radius: 8px; + padding: 5px; + font-size: 13px; + } + QTreeWidget::item { + padding: 6px; + margin: 2px 0; + border-radius: 4px; + } + QTreeWidget::item:hover { + background-color: #F0F0F0; + } + QTreeWidget::item:selected { + background-color: #E0E0E0; + color: #333; + } + QHeaderView::section { + background-color: #F0F0F0; + color: #333; + padding: 5px; + font-weight: bold; + border: none; + border-bottom: 1px solid #D0D0D0; + } + """ + self.tree_widget.setStyleSheet(tree_style) + self.tree_widget.setAlternatingRowColors(True) + self.tree_widget.setIndentation(15) # 调整缩进 + # Populate tree view self.function.populate_tree(self.tree_widget, directory_structure) # Create reload model button reload_layout = QHBoxLayout() self.reload_model_btn = QPushButton("Reload Model") - self.reload_model_btn.setStyleSheet(""" + reload_btn_style = """ QPushButton { background-color: #4CAF50; color: white; - padding: 8px 16px; - border-radius: 6px; + padding: 10px 20px; + border-radius: 8px; + font-size: 14px; font-weight: bold; + border: none; + margin-top: 10px; } QPushButton:hover { background-color: #45a049; } - """) + QPushButton:pressed { + background-color: #3D8B40; + } + """ + self.reload_model_btn.setStyleSheet(reload_btn_style) + self.reload_model_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + self.reload_model_btn.setMinimumWidth(150) + self.reload_model_btn.setMinimumHeight(40) reload_layout.addStretch() reload_layout.addWidget(self.reload_model_btn) From 5cebac9acfc9d2019db068096d4029d21ab81d19 Mon Sep 17 00:00:00 2001 From: RRFRRF <1195131157@qq.com> Date: Thu, 26 Dec 2024 23:22:17 +0800 Subject: [PATCH 22/42] fix bug --- GUI/function.py | 55 +++++++++++++++--------- GUI/ui.py | 109 +++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 144 insertions(+), 20 deletions(-) diff --git a/GUI/function.py b/GUI/function.py index 4d89c66..6fb7673 100644 --- a/GUI/function.py +++ b/GUI/function.py @@ -1,5 +1,6 @@ from PyQt5.QtWidgets import ( - QMessageBox, QFileDialog, QTreeWidgetItem, QInputDialog, QLineEdit, QDialog, QVBoxLayout, QLabel + QMessageBox, QFileDialog, QTreeWidgetItem, QInputDialog, QLineEdit, QDialog, QVBoxLayout, QLabel, + QPushButton, QHBoxLayout ) from PyQt5.QtGui import QPixmap, QImage from PyQt5.QtCore import Qt @@ -145,30 +146,36 @@ def populate_tree(self, tree_widget, directory_structure): tree_widget.addTopLevelItem(brand_item) def add_brand(self): - # Get brand name - brand_name, ok = QInputDialog.getText(self.ui, 'Add Brand', 'Enter brand name:') + # Create dialog using UI method + dialog, brand_input, domain_input, add_btn, cancel_btn = self.ui.create_add_brand_dialog(self) - if ok and brand_name: + # Button connections + def on_add(): + brand_name = brand_input.text().strip() + domains = domain_input.text().strip() + # Validate brand name + if not brand_name: + QMessageBox.warning( + dialog, + "Warning", + "Brand name is required!" + ) + return + if not all(c.isalnum() or c.isspace() or c in '-_' for c in brand_name): QMessageBox.warning( - self.ui, + dialog, "Warning", "Brand name can only contain letters, numbers, spaces, hyphens and underscores!" ) return - - # Get domain names (supports multiple domains separated by commas) - domains, ok = QInputDialog.getText( - self.ui, - 'Add Domains', - 'Enter domain names, separated by commas\nExample: example.com, test.example.com', - QLineEdit.Normal - ) - if not ok or not domains: - QMessageBox.warning(self.ui, "Warning", "Domain name is required!") + + # Validate domains + if not domains: + QMessageBox.warning(dialog, "Warning", "Domain name is required!") return - + # Create brand directory brand_path = os.path.join('models/expand_targetlist', brand_name) try: @@ -181,14 +188,24 @@ def add_brand(self): # Update domain mapping if self.domain_map_add(brand_name, domains): QMessageBox.information( - self.ui, + dialog, "Success", "Brand and domains added successfully!\nPlease click 'Reload Model' button to reload the models." ) + dialog.accept() else: - QMessageBox.warning(self.ui, "Warning", "Brand already exists!") + QMessageBox.warning(dialog, "Warning", "Brand already exists!") except Exception as e: - QMessageBox.critical(self.ui, "Error", f"Failed to create brand: {str(e)}") + QMessageBox.critical(dialog, "Error", f"Failed to create brand: {str(e)}") + + def on_cancel(): + dialog.reject() + + add_btn.clicked.connect(on_add) + cancel_btn.clicked.connect(on_cancel) + + # Show dialog + dialog.exec_() def delete_brand(self): # Get selected item diff --git a/GUI/ui.py b/GUI/ui.py index d1aabf9..35655da 100644 --- a/GUI/ui.py +++ b/GUI/ui.py @@ -1,6 +1,6 @@ from PyQt5.QtWidgets import ( QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, - QPushButton, QTextEdit, QTabWidget, QSizePolicy, QTreeWidget + QPushButton, QTextEdit, QTabWidget, QSizePolicy, QTreeWidget, QDialog ) from PyQt5.QtGui import QFont from PyQt5.QtCore import Qt @@ -311,6 +311,113 @@ def init_dataset_page(self): self.dataset_page.setLayout(layout) + def create_add_brand_dialog(self, function_instance): + # Create a custom dialog for adding brand + dialog = QDialog(self) + dialog.setWindowTitle('Add Brand') + dialog.setModal(True) + + # Main layout + layout = QVBoxLayout() + + # Brand name input + brand_label = QLabel('Brand Name:') + brand_input = QLineEdit() + brand_input.setPlaceholderText('Enter brand name') + + # Domain names input + domain_label = QLabel('Domain Names:') + domain_input = QLineEdit() + domain_input.setPlaceholderText('Example: www.example1.com, www.example2.com') + + # Add input fields to layout + layout.addWidget(brand_label) + layout.addWidget(brand_input) + layout.addWidget(domain_label) + layout.addWidget(domain_input) + + # Button layout + button_layout = QHBoxLayout() + add_btn = QPushButton('Add') + cancel_btn = QPushButton('Cancel') + button_layout.addWidget(add_btn) + button_layout.addWidget(cancel_btn) + layout.addLayout(button_layout) + + dialog.setLayout(layout) + + # Apply StyleSheet to dialog + dialog.setStyleSheet(""" + QWidget { + font-family: 'Segoe UI', 'Arial', sans-serif; + color: #424242; + background-color: #ffffff; + } + + QLabel { + color: #424242; + font-weight: 500; + } + + QLineEdit, QTextEdit { + background-color: #f8f9fa; + border: 1px solid #e9ecef; + padding: 8px 12px; + border-radius: 6px; + color: #424242; + } + + QLineEdit:focus, QTextEdit:focus { + border: 2px solid #6c757d; + background-color: #ffffff; + } + + QPushButton { + background-color: #495057; + color: white; + border: none; + padding: 8px 16px; + border-radius: 6px; + font-weight: 500; + } + + QPushButton:hover { + background-color: #5a6268; + } + + QPushButton:pressed { + background-color: #3d4246; + } + + QTabWidget::pane { + border: 1px solid #e9ecef; + border-radius: 6px; + background: white; + } + + QTabBar::tab { + background: #f8f9fa; + color: #424242; + padding: 8px 16px; + margin-right: 2px; + border-top-left-radius: 6px; + border-top-right-radius: 6px; + } + + QTabBar::tab:selected { + background: #495057; + color: white; + } + + QTabBar::tab:hover:!selected { + background: #e9ecef; + } + """) + + dialog.setStyleSheet + + return dialog, brand_input, domain_input, add_btn, cancel_btn + def resizeEvent(self, event): super().resizeEvent(event) self.function.on_resize(event) From d07a616ecd565f8e1f66a14551fd4b7fade01939 Mon Sep 17 00:00:00 2001 From: RRFRRF <1195131157@qq.com> Date: Thu, 26 Dec 2024 23:25:24 +0800 Subject: [PATCH 23/42] flask --- GUI/function.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/GUI/function.py b/GUI/function.py index 6fb7673..c00b150 100644 --- a/GUI/function.py +++ b/GUI/function.py @@ -1,6 +1,5 @@ from PyQt5.QtWidgets import ( - QMessageBox, QFileDialog, QTreeWidgetItem, QInputDialog, QLineEdit, QDialog, QVBoxLayout, QLabel, - QPushButton, QHBoxLayout + QMessageBox, QFileDialog, QTreeWidgetItem, QDialog, QVBoxLayout, QLabel ) from PyQt5.QtGui import QPixmap, QImage from PyQt5.QtCore import Qt From 6c2eb82f5c938cb50ef74819ce33ecd9eeba3c70 Mon Sep 17 00:00:00 2001 From: RRFRRF <1195131157@qq.com> Date: Fri, 27 Dec 2024 14:59:34 +0800 Subject: [PATCH 24/42] =?UTF-8?q?update=20GUI=20readme=EF=BC=8C=20?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=BB=93=E6=9E=9C=E6=98=BE=E7=A4=BA=20?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E4=BF=AE=E6=94=B9=E5=89=8D=E7=AB=AF=E6=96=87?= =?UTF-8?q?=E5=AD=97=E5=A4=A7=E5=B0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- {GUI => GUItool}/function.py | 31 +++-- GUItool/readme.md | 192 +++++++++++++++++++++++++++++ GUItool/requirements.txt | 1 + {GUI => GUItool}/ui.py | 232 +++++++++++++++++++++++++++++------ phishpedia_gui.py | 6 +- requirements.txt | 3 +- 6 files changed, 409 insertions(+), 56 deletions(-) rename {GUI => GUItool}/function.py (94%) create mode 100644 GUItool/readme.md create mode 100644 GUItool/requirements.txt rename {GUI => GUItool}/ui.py (63%) diff --git a/GUI/function.py b/GUItool/function.py similarity index 94% rename from GUI/function.py rename to GUItool/function.py index c00b150..a47a647 100644 --- a/GUI/function.py +++ b/GUItool/function.py @@ -29,22 +29,31 @@ def detect_phishing(self): screenshot_path = self.ui.image_input.text() if not url or not screenshot_path: - self.ui.result_display.setText("Please enter URL and upload a screenshot.") + self.ui.category_display.setText("Please enter URL and upload a screenshot.") + self.ui.target_display.clear() + self.ui.domain_display.clear() return phish_category, pred_target, matched_domain, plotvis, siamese_conf, pred_boxes, logo_recog_time, logo_match_time = self.phishpedia_cls.test_orig_phishpedia( url, screenshot_path, None) - # 根据 phish_category 改变颜色 - phish_category_color = 'green' if phish_category == 0 else 'red' - result_text = f'Phish Category(0 for benign, 1 for phish, default is benign): {phish_category}
' - result_text += f"Predicted Target: {pred_target}
" - result_text += f"Matched Domain: {matched_domain}
" - result_text += f"Siamese Confidence: {siamese_conf}
" - result_text += f"Logo Recognition Time: {logo_recog_time} seconds
" - result_text += f"Logo Match Time: {logo_match_time} seconds
" - - self.ui.result_display.setText(result_text) + # 设置检测结果类别和颜色 + if phish_category == 0: + self.ui.category_display.setStyleSheet("color: green;") + self.ui.category_display.setText("Benign") + elif phish_category == 1: + self.ui.category_display.setStyleSheet("color: red;") + self.ui.category_display.setText("Phish") + + # 如果没有匹配到目标,显示黄色的No match + if pred_target is None or pred_target == "": + self.ui.category_display.setStyleSheet("color: orange;") + self.ui.category_display.setText("No Match") + pred_target = "None" + + # 更新其他显示框的内容 + self.ui.target_display.setText(str(pred_target)) + self.ui.domain_display.setText(str(matched_domain)) if phish_category == 1 and plotvis is not None: self.display_image(plotvis) diff --git a/GUItool/readme.md b/GUItool/readme.md new file mode 100644 index 0000000..62ecdc3 --- /dev/null +++ b/GUItool/readme.md @@ -0,0 +1,192 @@ +# Phishpedia GUI Tool + +Phishpedia GUI is a graphical interface tool for phishing website detection. It provides a user-friendly interface with brand and domain management capabilities, as well as visualization features for phishing detection. + +## Installation Requirements + +Before using, make sure all necessary dependencies are installed: + +```bash +pip install -r requirements.txt +``` + +## How to Run + +Run the following command in the project root directory: + +```bash +python phishpedia_gui.py +``` + +## User Guide + +### 1. Phishing Detection Page (PhishTest) + +1. **URL Detection** + - Enter the URL to be tested in the "Enter URL" input box + - Click the "Browse" button to select the corresponding website screenshot + - Click the "Detect" button to start detection + - Detection results will be displayed below, including text results and visual presentation + +2. **Result Display** + - The detection results will be displayed in the "Result" text box + - The matched logos will be displayed in the "Target" text box + - The matched domains will be displayed in the "Domain" text box + - Visual results will be displayed in the "Visualization Result" area + - You can clearly see the detected brand identifiers and related information + +### 2. Dataset Management Page (Dataset) + +1. **Brand Management** + - Click "Add Brand" to add a new brand + - Enter brand name and corresponding domains in the popup window + - Click "Delete Brand" to remove the selected brand + +2. **Logo Management** + - After selecting a brand, click "Add Logo" to add brand logos + - Click "Delete Logo" to remove selected logos + - All logo files will be displayed in the tree view + +3. **Data Update** + - After making changes, click the "Reload Model" button + - The system will reload the updated dataset + +## Main Features + +1. **Phishing Detection** + - URL input and detection + - Screenshot upload and analysis + - Detection result visualization + +2. **Brand Management** + - Add/Delete brands + - Add/Delete brand logos + - Domain management + - Model reloading + +## Directory Structure + +``` +GUItool/ +├── ui.py # UI layout and style definitions +├── function.py # Core functionality implementation +├── readme.md # Documentation +└── requirements.txt # Dependency list +``` + +### File Description + +- **ui.py**: + - Defines main window layout + - Contains all UI component styles + - Implements dynamic font size adjustment + - Manages two main tabs: PhishTest and Dataset + +- **function.py**: + - Implements all core functionalities + - Handles brand and logo addition/deletion + - Manages domain mapping + - Executes phishing detection logic + - Handles file upload and visualization + +- **requirements.txt**: + - Lists all required Python packages + - Contains PyQt5 UI dependencies + +--- + +# Phishpedia GUI 工具 + +Phishpedia GUI 是一个用于钓鱼网站检测的图形界面工具。它提供了友好的用户界面,支持品牌和域名管理,以及钓鱼网站的可视化检测功能。 + +## 安装要求 + +在使用之前,请确保已安装所有必要的依赖: + +```bash +pip install -r requirements.txt +``` + +## 运行方法 + +在项目根目录下运行以下命令: + +```bash +python phishpedia_gui.py +``` + +## 使用指南 + +### 1. 钓鱼检测页面(PhishTest) + +1. **URL检测** + - 在"Enter URL"输入框中输入待检测的网址 + - 点击"Browse"按钮选择对应的网站截图 + - 点击"Detect"按钮开始检测 + - 检测结果将在下方显示,包括文字结果和可视化展示 + + +2. **结果展示** + - 检测结果会显示在"Result"文本框中 + - 匹配到的logo显示在"Target"文本框中 + - 匹配到的域名显示在"Domain"文本框中 + - 可视化结果会在"Visualization Result"区域展示 + - 可以清晰看到检测到的品牌标识和相关信息 + +### 2. 数据集管理页面(Dataset) + +1. **品牌管理** + - 点击"Add Brand"添加新的品牌 + - 在弹出窗口中输入品牌名称和对应的域名 + - 点击"Delete Brand"可删除选中的品牌 + +2. **Logo管理** + - 选择品牌后,点击"Add Logo"添加品牌Logo + - 点击"Delete Logo"可删除选中的Logo + - 所有Logo文件会在树形视图中显示 + +3. **数据更新** + - 完成修改后,点击"Reload Model"按钮 + - 系统会重新加载更新后的数据集 + +## 主要功能 + +1. **钓鱼检测** + - URL输入和检测 + - 截图上传和分析 + - 检测结果可视化展示 + +2. **品牌管理** + - 添加/删除品牌 + - 添加/删除品牌Logo + - 域名管理 + - 模型重新加载 + +## 目录结构 + +``` +GUItool/ +├── ui.py # 界面布局和样式定义 +├── function.py # 功能实现模块 +├── readme.md # 说明文档 +└── requirements.txt # 依赖包列表 +``` + +### 文件说明 + +- **ui.py**: + - 定义了主窗口界面布局 + - 包含所有UI组件的样式设置 + - 实现了动态字体大小调整 + - 管理界面的两个主要标签页:PhishTest和Dataset + +- **function.py**: + - 实现所有核心功能 + - 处理品牌和Logo的添加/删除 + - 管理域名映射 + - 执行钓鱼检测逻辑 + - 处理文件上传和可视化 + +- **requirements.txt**: + - 列出所有必需的Python包 + - 包含PyQt5 UI相关依赖 \ No newline at end of file diff --git a/GUItool/requirements.txt b/GUItool/requirements.txt new file mode 100644 index 0000000..4bb02ba --- /dev/null +++ b/GUItool/requirements.txt @@ -0,0 +1 @@ +PyQt5 # GUI框架 \ No newline at end of file diff --git a/GUI/ui.py b/GUItool/ui.py similarity index 63% rename from GUI/ui.py rename to GUItool/ui.py index 35655da..97df66a 100644 --- a/GUI/ui.py +++ b/GUItool/ui.py @@ -1,8 +1,9 @@ from PyQt5.QtWidgets import ( QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, - QPushButton, QTextEdit, QTabWidget, QSizePolicy, QTreeWidget, QDialog + QPushButton, QTextEdit, QTabWidget, QSizePolicy, QTreeWidget, QDialog, + QComboBox, QTabBar ) -from PyQt5.QtGui import QFont +from PyQt5.QtGui import QFont, QFontMetrics from PyQt5.QtCore import Qt from .function import PhishpediaFunction @@ -11,6 +12,8 @@ class PhishpediaUI(QWidget): def __init__(self): super().__init__() self.function = PhishpediaFunction(self) + self.default_font_size = 10 # 默认字体大小 + self.current_font_size = self.default_font_size self.initUI() def initUI(self): @@ -19,22 +22,72 @@ def initUI(self): main_layout = QVBoxLayout() - # Navigation Bar - self.tab_widget = QTabWidget() - main_layout.addWidget(self.tab_widget) + # Top Bar: Tab Labels and Font Size Control + top_bar = QHBoxLayout() + + # Tab Labels (left side) + tab_labels = QHBoxLayout() + self.phish_test_btn = QPushButton("PhishTest") + self.dataset_btn = QPushButton("Dataset") + self.phish_test_btn.setCheckable(True) + self.dataset_btn.setCheckable(True) + self.phish_test_btn.setChecked(True) + + # 连接按钮点击事件 + self.phish_test_btn.clicked.connect(lambda: self.switch_page(0)) + self.dataset_btn.clicked.connect(lambda: self.switch_page(1)) + + tab_labels.addWidget(self.phish_test_btn) + tab_labels.addWidget(self.dataset_btn) + tab_labels.addStretch() + top_bar.addLayout(tab_labels) + + # Font Size Control (right side) + font_control = QHBoxLayout() + font_size_label = QLabel("Word Size:") + self.font_size_combo = QComboBox() + font_sizes = [str(size) for size in range(5, 31)] + self.font_size_combo.addItems(font_sizes) + self.font_size_combo.setCurrentText(str(self.default_font_size)) + self.font_size_combo.currentTextChanged.connect(self.update_global_font_size) + + # 设置下拉框自适应内容宽度 + self.font_size_combo.setSizeAdjustPolicy(QComboBox.AdjustToContents) + # 计算最宽的选项需要的宽度 + max_width = 0 + fm = self.font_size_combo.fontMetrics() + for size in font_sizes: + width = fm.horizontalAdvance(size) + 30 # 30是额外的padding和箭头的空间 + max_width = max(max_width, width) + self.font_size_combo.setMinimumWidth(max_width) + + font_control.addWidget(font_size_label) + font_control.addWidget(self.font_size_combo) + top_bar.addLayout(font_control) + + main_layout.addLayout(top_bar) + + # Content Stack + self.content_stack = QTabWidget() + self.content_stack.setTabBar(QTabBar()) # 创建一个空的TabBar + self.content_stack.tabBar().setVisible(False) # 隐藏TabBar + main_layout.addWidget(self.content_stack) # PhishTest Page self.phish_test_page = QWidget() self.init_phish_test_page() - self.tab_widget.addTab(self.phish_test_page, "PhishTest") + self.content_stack.addTab(self.phish_test_page, "") # 空标题,因为我们使用自定义按钮 # Dataset Page self.dataset_page = QWidget() self.init_dataset_page() - self.tab_widget.addTab(self.dataset_page, "Dataset") + self.content_stack.addTab(self.dataset_page, "") self.setLayout(main_layout) + # Apply initial font size + self.update_global_font_size(str(self.default_font_size)) + # Apply stylesheet self.setStyleSheet(""" QWidget { @@ -43,6 +96,24 @@ def initUI(self): background-color: #ffffff; } + QPushButton { + background-color: #495057; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + margin: 0px; + } + + QPushButton:checked { + background-color: #E0E0E0; + color: #424242; + } + + QPushButton:hover:!checked { + background-color: #5a6268; + } + QLabel { color: #424242; font-weight: 500; @@ -101,30 +172,88 @@ def initUI(self): QTabBar::tab:hover:!selected { background: #e9ecef; } + + QComboBox { + padding: 5px 25px 5px 5px; /* 使用CSS注释格式 */ + border: 1px solid #e9ecef; + border-radius: 4px; + background: white; + } + + QComboBox::drop-down { + border: none; + width: 20px; + } + + QComboBox::down-arrow { + image: none; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px solid #424242; + } + + QComboBox:on { + border: 2px solid #6c757d; + } + + QComboBox QAbstractItemView { + border: 1px solid #e9ecef; + selection-background-color: #f8f9fa; + selection-color: #424242; + background: white; + padding: 5px; + } """) - # Set dynamic font size based on screen DPI - self.set_dynamic_font_size() - - def set_dynamic_font_size(self): - screen = QApplication.primaryScreen() - dpi = screen.logicalDotsPerInch() - base_font_size = 16 # Base font size for 200 DPI - font_size = base_font_size * (dpi / 175) - - font = QFont() - font.setPointSizeF(font_size) - - for widget in self.findChildren(QLabel) + self.findChildren(QLineEdit) + self.findChildren(QPushButton) + [ - self.result_display]: - widget.setFont(font) + def switch_page(self, index): + """切换页面并更新按钮状态""" + self.content_stack.setCurrentIndex(index) + self.phish_test_btn.setChecked(index == 0) + self.dataset_btn.setChecked(index == 1) + + def update_global_font_size(self, size): + """更新所有UI元素的字体大小""" + try: + size = int(size) + font = QFont() + font.setPointSize(size) + + # 更新所有部件的字体 + self.update_widget_fonts(self, font) + + # 更新标签页字体 + self.content_stack.setFont(font) + + # 保存当前字体大小,用于新创建的对话框 + QApplication.instance().setFont(font) + + except ValueError as e: + print(f"Error updating font size: {e}") + + def update_widget_fonts(self, widget, font): + """递归更新所有子部件的字体""" + for child in widget.findChildren(QWidget): + child.setFont(font) + if isinstance(child, QTreeWidget): + # 更新树形控件的所有项目字体 + for i in range(child.topLevelItemCount()): + item = child.topLevelItem(i) + self.update_tree_item_font(item, font) + self.update_widget_fonts(child, font) + + def update_tree_item_font(self, item, font): + """更新树形控件项目的字体""" + item.setFont(0, font) + for i in range(item.childCount()): + child_item = item.child(i) + self.update_tree_item_font(child_item, font) def init_phish_test_page(self): layout = QVBoxLayout() # URL Input url_layout = QHBoxLayout() - self.url_label = QLabel('Enter URL:') + self.url_label = QLabel('URL:') url_layout.addWidget(self.url_label) self.url_input = QLineEdit() url_layout.addWidget(self.url_input) @@ -132,7 +261,7 @@ def init_phish_test_page(self): # Image Upload image_layout = QHBoxLayout() - self.image_label = QLabel('Upload Screenshot:') + self.image_label = QLabel('Screenshot:') image_layout.addWidget(self.image_label) self.image_input = QLineEdit() image_layout.addWidget(self.image_input) @@ -146,20 +275,46 @@ def init_phish_test_page(self): self.detect_button.clicked.connect(self.function.detect_phishing) layout.addWidget(self.detect_button) - # Result Display - result_layout = QHBoxLayout() - self.result_label = QLabel('Detection Result:') - result_layout.addWidget(self.result_label) - self.result_display = QTextEdit() - self.result_display.setReadOnly(True) - self.result_display.setFixedHeight(100) - result_layout.addWidget(self.result_display) + # Result Display Section + result_layout = QVBoxLayout() # 主结果布局为垂直 + + + # 第一行:检测结果 + detection_layout = QHBoxLayout() + self.result_label = QLabel('Result:') + self.category_display = QLineEdit() + self.category_display.setReadOnly(True) + detection_layout.addWidget(self.result_label) + detection_layout.addWidget(self.category_display) + result_layout.addLayout(detection_layout) + + # 第二行:预测目标和匹配域名(水平排列) + details_layout = QHBoxLayout() + + # 预测目标 + target_layout = QHBoxLayout() + self.target_label = QLabel('Target:') + self.target_display = QLineEdit() + self.target_display.setReadOnly(True) + target_layout.addWidget(self.target_label) + target_layout.addWidget(self.target_display) + details_layout.addLayout(target_layout) + + # 匹配域名 + domain_layout = QHBoxLayout() + self.domain_label = QLabel('Domain:') + self.domain_display = QLineEdit() + self.domain_display.setReadOnly(True) + domain_layout.addWidget(self.domain_label) + domain_layout.addWidget(self.domain_display) + details_layout.addLayout(domain_layout) + + result_layout.addLayout(details_layout) layout.addLayout(result_layout) # Visualization Display visualization_layout = QVBoxLayout() self.visualization_label = QLabel('Visualization Result:') - self.visualization_label.setFixedHeight(self.visualization_label.fontMetrics().height()) visualization_layout.addWidget(self.visualization_label) self.visualization_display = QLabel() @@ -194,7 +349,6 @@ def init_dataset_page(self): color: #333; padding: 10px 20px; border-radius: 8px; - font-size: 14px; font-weight: 500; border: 1px solid #D0D0D0; margin: 0 5px; @@ -215,7 +369,7 @@ def init_dataset_page(self): # 设置按钮大小策略 for btn in [self.add_brand_btn, self.delete_brand_btn, self.add_logo_btn, self.delete_logo_btn]: btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) - btn.setMinimumHeight(40) + btn.setMinimumHeight(60) # Add buttons to layout button_layout.addWidget(self.add_brand_btn) @@ -243,7 +397,6 @@ def init_dataset_page(self): border: 1px solid #E0E0E0; border-radius: 8px; padding: 5px; - font-size: 13px; } QTreeWidget::item { padding: 6px; @@ -282,7 +435,6 @@ def init_dataset_page(self): color: white; padding: 10px 20px; border-radius: 8px; - font-size: 14px; font-weight: bold; border: none; margin-top: 10px; @@ -347,7 +499,7 @@ def create_add_brand_dialog(self, function_instance): dialog.setLayout(layout) # Apply StyleSheet to dialog - dialog.setStyleSheet(""" + dialog.setStyleSheet(""" QWidget { font-family: 'Segoe UI', 'Arial', sans-serif; color: #424242; @@ -413,8 +565,8 @@ def create_add_brand_dialog(self, function_instance): background: #e9ecef; } """) - - dialog.setStyleSheet + + dialog.setFont(QFont('Segoe UI', self.current_font_size)) return dialog, brand_input, domain_input, add_btn, cancel_btn diff --git a/phishpedia_gui.py b/phishpedia_gui.py index e1ec1a7..b29417d 100644 --- a/phishpedia_gui.py +++ b/phishpedia_gui.py @@ -1,9 +1,9 @@ import sys from PyQt5.QtWidgets import QApplication -from GUI.ui import PhishpediaUI +from GUItool.ui import PhishpediaUI if __name__ == '__main__': app = QApplication(sys.argv) - ex = PhishpediaUI() - ex.show() + window = PhishpediaUI() + window.show() sys.exit(app.exec_()) diff --git a/requirements.txt b/requirements.txt index 8811e99..ec2f6e6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,5 +12,4 @@ scikit-learn lxml gdown memory-profiler -psutil -PyQt5 # GUI框架 \ No newline at end of file +psutil \ No newline at end of file From 2bbc3da9f460790ea7a6576405a4df535b84312b Mon Sep 17 00:00:00 2001 From: RRFRRF <1195131157@qq.com> Date: Fri, 27 Dec 2024 15:02:57 +0800 Subject: [PATCH 25/42] flask --- GUItool/ui.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/GUItool/ui.py b/GUItool/ui.py index 97df66a..717096b 100644 --- a/GUItool/ui.py +++ b/GUItool/ui.py @@ -1,9 +1,9 @@ from PyQt5.QtWidgets import ( QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, - QPushButton, QTextEdit, QTabWidget, QSizePolicy, QTreeWidget, QDialog, + QPushButton, QTabWidget, QSizePolicy, QTreeWidget, QDialog, QComboBox, QTabBar ) -from PyQt5.QtGui import QFont, QFontMetrics +from PyQt5.QtGui import QFont from PyQt5.QtCore import Qt from .function import PhishpediaFunction @@ -278,7 +278,6 @@ def init_phish_test_page(self): # Result Display Section result_layout = QVBoxLayout() # 主结果布局为垂直 - # 第一行:检测结果 detection_layout = QHBoxLayout() self.result_label = QLabel('Result:') @@ -287,7 +286,7 @@ def init_phish_test_page(self): detection_layout.addWidget(self.result_label) detection_layout.addWidget(self.category_display) result_layout.addLayout(detection_layout) - + # 第二行:预测目标和匹配域名(水平排列) details_layout = QHBoxLayout() @@ -499,7 +498,7 @@ def create_add_brand_dialog(self, function_instance): dialog.setLayout(layout) # Apply StyleSheet to dialog - dialog.setStyleSheet(""" + dialog.setStyleSheet(""" QWidget { font-family: 'Segoe UI', 'Arial', sans-serif; color: #424242; From 327446aaedebce8e3a0cb99641c7b0c631995f0c Mon Sep 17 00:00:00 2001 From: Weiyu-Kong <1625827540@qq.com> Date: Sat, 28 Dec 2024 14:44:10 +0800 Subject: [PATCH 26/42] Add web server and static resources --- WEBtool/phishpedia_web.py | 257 ++++++++++++++++++ WEBtool/static/css/sidebar.css | 337 +++++++++++++++++++++++ WEBtool/static/css/style.css | 431 ++++++++++++++++++++++++++++++ WEBtool/static/icon/file1.png | Bin 0 -> 2335 bytes WEBtool/static/icon/fish.png | Bin 0 -> 9869 bytes WEBtool/static/icon/noresult1.png | Bin 0 -> 4128 bytes WEBtool/static/icon/succ.png | Bin 0 -> 9220 bytes WEBtool/static/js/main.js | 140 ++++++++++ WEBtool/static/js/sidebar.js | 330 +++++++++++++++++++++++ WEBtool/templates/index.html | 163 +++++++++++ WEBtool/utils_web.py | 89 ++++++ 11 files changed, 1747 insertions(+) create mode 100644 WEBtool/phishpedia_web.py create mode 100644 WEBtool/static/css/sidebar.css create mode 100644 WEBtool/static/css/style.css create mode 100644 WEBtool/static/icon/file1.png create mode 100644 WEBtool/static/icon/fish.png create mode 100644 WEBtool/static/icon/noresult1.png create mode 100644 WEBtool/static/icon/succ.png create mode 100644 WEBtool/static/js/main.js create mode 100644 WEBtool/static/js/sidebar.js create mode 100644 WEBtool/templates/index.html create mode 100644 WEBtool/utils_web.py diff --git a/WEBtool/phishpedia_web.py b/WEBtool/phishpedia_web.py new file mode 100644 index 0000000..5e80536 --- /dev/null +++ b/WEBtool/phishpedia_web.py @@ -0,0 +1,257 @@ +import os +import sys +from flask import request, Flask, jsonify,render_template,send_from_directory +from flask_cors import CORS +from utils_web import * + +sys.path.append('..') +from configs import load_config +from phishpedia import PhishpediaWrapper + +phishpedia_cls = None + +# flask for API server +app = Flask(__name__) +cors = CORS(app, supports_credentials=True) +app.config['CORS_HEADERS'] = 'Content-Type' +app.config['UPLOAD_FOLDER'] = 'static/uploads' +app.config['FILE_TREE_ROOT'] = '../models/expand_targetlist' # 主目录路径 +app.config['DOMAIN_MAP_PATH'] = '../models/domain_map.pkl' + +@app.route('/') +def index(): + """渲染主页面""" + return render_template('index.html') + +@app.route('/upload', methods=['POST']) +def upload_file(): + """处理文件上传请求""" + if 'image' not in request.files: + return jsonify({'error': 'No file part'}), 400 + file = request.files['image'] + + if file.filename == '': + return jsonify({'error': 'No selected file'}), 400 + + if file and allowed_file(file.filename): + filename = file.filename + file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) + file.save(file_path) + return jsonify({'success': True, 'imageUrl': f'/uploads/{filename}'}), 200 + + return jsonify({'error': 'Invalid file type'}), 400 + +@app.route('/uploads/') +def uploaded_file(filename): + """提供上传文件的访问路径""" + return send_from_directory(app.config['UPLOAD_FOLDER'], filename) + +@app.route('/clear_upload', methods=['POST']) +def delete_image(): + data = request.get_json() + image_url = data.get('imageUrl') + + if not image_url: + return jsonify({'success': False, 'error': 'No image URL provided'}), 400 + + try: + # 假设 image_url 是相对于静态目录的路径 + filename = image_url.split('/')[-1] + image_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) + os.remove(image_path) + return jsonify({'success': True}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + + +@app.route('/detect', methods=['POST']) +def detect(): + data = request.json + url = data.get('url', '') + imageUrl = data.get('imageUrl', '') + + filename = imageUrl.split('/')[-1] + screenshot_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) + + phish_category, pred_target, matched_domain, plotvis, siamese_conf, pred_boxes, logo_recog_time, logo_match_time = phishpedia_cls.test_orig_phishpedia( + url, screenshot_path, None) + + # 处理检测结果 + if phish_category == 0: + if pred_target is None: + result = 'Unknown' + else: + result = 'Benign' + else: + result = 'Phishing' + + plot_base64 = convert_to_base64(plotvis) + + # 返回检测结果 + result = { + 'result': result, # 检测结果 + 'matched_brand':pred_target, # 匹配到的品牌 + 'correct_domain':matched_domain, # 正确的域名 + 'confidence': round(float(siamese_conf),3), # 置信度,直接返回百分比 + 'detection_time': round(float(logo_recog_time)+float(logo_match_time),3), # 检测时间 + 'logo_extraction': plot_base64 # logo标注结果,直接返回图像 + } + return jsonify(result) + +@app.route('/get-directory', methods=['GET']) +def get_file_tree(): + """ + 获取主目录的文件树 + """ + def build_file_tree(path): + tree = [] + try: + for entry in os.listdir(path): + entry_path = os.path.join(path, entry) + if os.path.isdir(entry_path): + tree.append({ + 'name': entry, + 'type': 'directory', + 'children': build_file_tree(entry_path) # 递归子目录 + }) + elif entry.lower().endswith(('.png', '.jpeg', '.jpg')): + tree.append({ + 'name': entry, + 'type': 'file' + }) + else: + continue + except PermissionError: + pass # 忽略权限错误 + return sorted(tree, key=lambda x: x['name'].lower()) # 按 name 字段排序,不区分大小写 + + root_path = app.config['FILE_TREE_ROOT'] + if not os.path.exists(root_path): + return jsonify({'error': 'Root directory does not exist'}), 404 + + file_tree = build_file_tree(root_path) + return jsonify({'file_tree': file_tree}), 200 + +@app.route('/view-file', methods=['GET']) +def view_file(): + file_name = request.args.get('file') + file_path = os.path.join(app.config['FILE_TREE_ROOT'], file_name) + print(file_name) + + if not os.path.exists(file_path): + return jsonify({'error': 'File not found'}), 404 + + if file_name.lower().endswith(('.png', '.jpeg', '.jpg')): + return send_from_directory(app.config['FILE_TREE_ROOT'], file_name) + + return jsonify({'error': 'Unsupported file type'}), 400 + + +@app.route('/add-logo', methods=['POST']) +def add_logo(): + if 'logo' not in request.files: + return jsonify({'success': False, 'error': 'No file part'}), 400 + + logo = request.files['logo'] + if logo.filename == '': + return jsonify({'success': False, 'error': 'No selected file'}), 400 + + if logo and allowed_file(logo.filename): + directory = request.form.get('directory') + if not directory: + return jsonify({'success': False, 'error': 'No directory specified'}), 400 + + directory_path = os.path.join(app.config['FILE_TREE_ROOT'], directory) + + if not os.path.exists(directory_path): + return jsonify({'success': False, 'error': 'Directory does not exist'}), 400 + + file_path = os.path.join(directory_path, logo.filename) + logo.save(file_path) + return jsonify({'success': True, 'message': 'Logo added successfully'}), 200 + + return jsonify({'success': False, 'error': 'Invalid file type'}), 400 + +@app.route('/del-logo', methods=['POST']) +def del_logo(): + directory = request.form.get('directory') + filename = request.form.get('filename') + + if not directory or not filename: + return jsonify({'success': False, 'error': 'Directory and filename must be specified'}), 400 + + directory_path = os.path.join(app.config['FILE_TREE_ROOT'], directory) + file_path = os.path.join(directory_path, filename) + + if not os.path.exists(file_path): + return jsonify({'success': False, 'error': 'File does not exist'}), 400 + + try: + os.remove(file_path) + return jsonify({'success': True, 'message': 'Logo deleted successfully'}), 200 + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + +@app.route('/add-brand', methods=['POST']) +def add_brand(): + brand_name = request.form.get('brandName') + brand_domain = request.form.get('brandDomain') + + if not brand_name or not brand_domain: + return jsonify({'success': False, 'error': 'Brand name and domain must be specified'}), 400 + + # 创建品牌目录 + brand_directory_path = os.path.join(app.config['FILE_TREE_ROOT'], brand_name) + if os.path.exists(brand_directory_path): + return jsonify({'success': False, 'error': 'Brand already exists'}), 400 + + try: + os.makedirs(brand_directory_path) + domain_map_add(brand_name, brand_domain, app.config['DOMAIN_MAP_PATH']) + return jsonify({'success': True, 'message': 'Brand added successfully'}), 200 + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + +@app.route('/del-brand', methods=['POST']) +def del_brand(): + directory = request.json.get('directory') + + if not directory: + return jsonify({'success': False, 'error': 'Directory must be specified'}), 400 + + directory_path = os.path.join(app.config['FILE_TREE_ROOT'], directory) + + if not os.path.exists(directory_path): + return jsonify({'success': False, 'error': 'Directory does not exist'}), 400 + + try: + shutil.rmtree(directory_path) + domain_map_delete(directory, app.config['DOMAIN_MAP_PATH']) + return jsonify({'success': True, 'message': 'Brand deleted successfully'}), 200 + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + +@app.route('/reload-model', methods=['POST']) +def reload_model(): + try: + load_config(reload_targetlist=True) + # Reinitialize Phishpedia + phishpedia_cls = PhishpediaWrapper() + return jsonify({'success': True, 'message': 'Brand deleted successfully'}), 200 + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + + + +if __name__ == "__main__": + ip_address = '0.0.0.0' + port = 5000 + while check_port_inuse(port, ip_address): + port = port + 1 + + # 加载核心检测逻辑 + phishpedia_cls = PhishpediaWrapper() + + initial_upload_folder(app.config['UPLOAD_FOLDER']) + + app.run(host=ip_address, port=port) \ No newline at end of file diff --git a/WEBtool/static/css/sidebar.css b/WEBtool/static/css/sidebar.css new file mode 100644 index 0000000..1d88ec5 --- /dev/null +++ b/WEBtool/static/css/sidebar.css @@ -0,0 +1,337 @@ +/* 侧边栏样式 */ +.sidebar { + position: fixed; + top: 0; + right: -400px; + width: 300px; + height: 100%; + background-color: #ffffff; + box-shadow: -2px 0 5px rgba(0, 0, 0, 0.1); + transition: right 0.3s ease; + z-index: 1000; + display: flex; + flex-direction: column; + padding: 20px; +} + +/* 侧边栏打开时显示 */ +.sidebar.open { + right: 0; +} + +/* 侧边栏标题 */ +.sidebar-header { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 18px; + font-weight: bold; + margin-bottom: 20px; +} + +/* 关闭按钮 */ +.close-sidebar { + background: none; + border: none; + font-size: 18px; + cursor: pointer; + color: #333; +} + +/* 右上角按钮样式 */ +.sidebar-toggle { + position: absolute; + top: 15px; + right: 15px; + background: #87CEFA; + color: white; + border: none; + border-radius: 5px; + padding: 10px 15px; + font-size: 18px; + font-weight: bold; + cursor: pointer; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + transition: background-color 0.3s ease; +} + +.sidebar-toggle:hover { + background-color: #0056b3; +} + +/* 按钮容器样式 */ +.sidebar-buttons { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-bottom: 20px; + justify-content: space-between; +} + +/* 按钮基础样式 */ +.sidebar-button { + flex: 1 1 calc(50% - 10px); + display: flex; + justify-content: center; + align-items: center; + background-color: #87CEFA; + color: white; + font-size: 14px; + font-weight: bold; + border: none; + border-radius: 3px; + padding: 5px 10px; + cursor: pointer; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + transition: background-color 0.3s ease, transform 0.2s ease; +} + +/* 按钮悬停效果 */ +.sidebar-button:hover { + background-color: #0056b3; + transform: translateY(-2px); +} + +/* 按钮点击效果 */ +.sidebar-button:active { + background-color: #003d80; + transform: translateY(0); +} + +/* ============ 文件树 ============ */ +/* 文件树样式 */ +#file-tree-root { + list-style-type: none; + padding-left: 20px; + height: 580px; + max-height: 580px; + overflow-y: auto; + border: 1px solid #ccc; + background-color: white; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.file-item { + margin-bottom: 5px; +} + +.file-folder { + cursor: pointer; +} + +.folder-name { + display: flex; + align-items: center; +} + +.folder-icon { + margin-right: 5px; +} + +.file-file { + cursor: pointer; +} + +.file-icon { + margin-right: 5px; +} + +.hidden { + display: none; +} + + +.file-folder>ul { + padding-left: 20px; +} + +/* 预览框样式 */ +#image-preview-box { + position: absolute; + background-color: white; + border: 1px solid #ccc; + padding: 10px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + max-width: 400px; + max-height: 300px; + overflow: hidden; +} + +/* 选中样式 */ +.selected { + border: 2px solid #007bff; + padding: 2px; + box-sizing: border-box; +} + + +/* ============== 表单 ============= */ +.form-container { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: #ffffff; + padding: 20px 30px; + border-radius: 10px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + width: 300px; + max-width: 90%; + z-index: 1001; +} + +/* 表单标题 */ +.form-container h3 { + font-size: 22px; + font-weight: bold; + color: #333; + margin-bottom: 20px; + text-align: center; + font-family: 'Arial', sans-serif; +} + +input[type="label"] { + width: 20%; +} + +/* 输入框样式 */ +input[type="text"] { + width: 90%; + padding: 12px; + margin: 12px 0; + border: 1px solid #ddd; + border-radius: 8px; + background-color: #f9f9f9; + font-size: 16px; + color: #333; + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1); + transition: border-color 0.3s ease, background-color 0.3s ease; + text-align: center; +} + +/* 输入框聚焦效果 */ +input[type="text"]:focus { + border-color: #3498db; + background-color: #fff; + outline: none; +} + +/* 提交按钮样式 */ +button[type="submit"] { + background-color: #3498db; + color: white; +} + +/* 取消按钮样式 */ +button[type="button"] { + background-color: #7c7c7c; + color: white; +} + +/* 表单按钮容器 */ +.form-actions { + width: 100%; + display: flex; + justify-content: space-between; + gap: 12px; + margin-top: 20px; +} + +/* 提交按钮样式 */ +button[type="submit"] { + background-color: #3498db; + color: white; + padding: 10px 20px; + border: none; + border-radius: 5px; + font-size: 14px; + cursor: pointer; + transition: background-color 0.3s ease, transform 0.2s ease; +} + +/* 提交按钮悬停效果 */ +button[type="submit"]:hover { + background-color: #2980b9; + transform: translateY(-2px); +} + +/* 提交按钮点击效果 */ +button[type="submit"]:active { + background-color: #1abc9c; + transform: translateY(0); +} + +/* 取消按钮样式 */ +button[type="button"] { + background-color: #7c7c7c; + color: white; + padding: 10px 20px; + border: none; + border-radius: 5px; + font-size: 14px; + cursor: pointer; + transition: background-color 0.3s ease, transform 0.2s ease; +} + +/* 取消按钮悬停效果 */ +button[type="button"]:hover { + background-color: #555; + transform: translateY(-2px); +} + +/* 取消按钮点击效果 */ +button[type="button"]:active { + background-color: #333; + transform: translateY(0); +} + +/* 浮层样式 */ +#overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 1002; +} + +/* 转圈动画样式 */ +#spinner { + border: 2px solid #f3f3f3; + border-top: 2px solid #3498db; + border-radius: 50%; + width: 16px; + height: 16px; + animation: spin 2s linear infinite; + margin-right: 10px; +} + +/* 转圈动画 */ +@keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +/* 浮层中的文本样式 */ +#overlay p { + color: white; + font-size: 16px; + font-weight: bold; + text-align: center; + line-height: 16px; + margin: 0; +} + +#overlay .spinner-container { + display: flex; + align-items: center; +} \ No newline at end of file diff --git a/WEBtool/static/css/style.css b/WEBtool/static/css/style.css new file mode 100644 index 0000000..6b2eef6 --- /dev/null +++ b/WEBtool/static/css/style.css @@ -0,0 +1,431 @@ +body, +html { + margin: 0; + padding: 0; + font-family: Arial, sans-serif; + background-color: #faf4f2; +} + +ul { + list-style-type: none; + padding: 0; +} + +li { + margin: 5px 0; +} + +#header { + display: flex; + align-items: center; + justify-content: flex-start; + position: absolute; + top: 0px; + left: 0px; + background-color: rgba(255, 255, 255, 0.8); + padding: 10px 10px; + border-radius: 5px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + width: 100%; + margin-bottom: 10px; +} + +#logo-icon { + height: 60px; + width: auto; + margin-right: 20px; +} + +#logo-text { + display: flex; + align-items: center; + height: 80px; + line-height: 80px; + letter-spacing: 2px; + background: linear-gradient(90deg, #3498db, #f9f388); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.2); + font-size: 35px; + font-weight: bold; +} + + +#main-container { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + margin-top: 130px; +} + +#input-container { + display: flex; + flex-direction: column; + align-items: center; + width: 1200px; + padding: 20px; + border-radius: 8px; + border: 1px solid #ddd; + background-color: #dff0fb; +} + +.inner-container { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + border-radius: 5px; + border: 3px dashed white; + background-color: #eaf4fb; + padding-top: 20px; + padding-bottom: 20px; +} + +#output-container { + display: flex; + flex-direction: column; + align-items: center; + width: 1240px; + margin-top: 10px; +} + +/* ============================= URL输入区域 =============================*/ +#url-input-container { + display: flex; + justify-content: center; + align-items: center; + gap: 10px; + width: 500px; +} + +.custom-label { + background-color: #87CEFA; + color: white; + border-radius: 25px; + padding: 10px 20px; + font-size: 16px; + font-weight: bold; + border: none; + text-align: center; + white-space: nowrap; +} + +#url-input { + background-color: #dcdcdc; + color: #333; + border: none; + border-radius: 15px; + padding: 10px 20px; + font-size: 16px; + outline: none; + width: 300px; + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1); +} + +#url-input::placeholder { + color: #888; + font-style: italic; +} + +/* ============================= 图片上传区域 =============================*/ +#image-upload-container { + display: flex; + justify-content: center; + align-items: center; + width: 410px; +} + +.drop-area { + border: 2px dashed #007BFF; + border-radius: 8px; + background-color: #ffffff; + padding: 20px; + text-align: center; + font-size: 1.2em; + color: #004085; + margin-top: 10px; + width: 100%; + height: 20vh; + margin: 20px auto; + transition: background-color 0.3s ease; +} + + +.upload-icon { + width: 50px; + height: 50px; + margin-bottom: 10px; +} + +.upload-label { + cursor: pointer; + margin-bottom: -10px; + background-color: white; + color: black; + padding: 10px 20px; + border: 2px solid #ccc; + border-radius: 50%; + border-radius: 6px; + text-align: center; + font-size: small; + display: inline-block; + line-height: 1; + font-family: Arial, + sans-serif; +} + +.upload-label:hover { + background-color: #f0f0f0; +} + +.upload-success-area { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 20px; + border: 2px dashed #007BFF; + border-radius: 8px; + background-color: #ffffff; + margin-top: 10px; + margin-bottom: 10px; +} + +.success-message { + display: flex; + align-items: center; + margin-bottom: 10px; + font-size: larger; +} + +.success-icon { + width: 30px; + height: 30px; + margin-right: 5px; +} + +.success-text { + font-size: 16px; +} + +.uploaded-thumbnail { + width: 400px; + height: auto; + margin-top: 10px; + margin-bottom: 10px; +} + +.clear-button { + padding: 10px 20px; + background-color: #888888; + color: white; + border: none; + border-radius: 8px; + font-size: 16px; + font-weight: bold; + cursor: pointer; + transition: background-color 0.3s ease; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.clear-button:hover { + background-color: #555555; +} + +#start-detection-button { + background-color: #007BFF; + color: white; + border: none; + border-radius: 25px; + padding: 10px 20px; + font-size: 16px; + font-weight: bold; + cursor: pointer; + margin-top: 0px; + width: 410px; + transition: background-color 0.3s ease; +} + +#start-detection-button:hover { + background-color: #0056b3; +} + +/* ============================= 结果容器样式 =============================*/ +#result-container { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: flex-start; + width: 100%; + max-width: 1500px; + gap: 20px; +} + +#original-image-container, +#detection-result-container { + display: flex; + flex-direction: column; + align-items: center; + width: 50%; + height: 450px; + border: 1px solid #ddd; + border-radius: 10px; + padding-top: 10px; + padding-left: 20px; + padding-right: 20px; + padding-bottom: 20px; + background-color: #ffffff; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + transition: transform 0.3s ease; +} + +#original-image-container:hover, +#detection-result-container:hover { + transform: scale(1.02); + transition: transform 0.3s ease; +} + +.result_title { + width: 100%; + height: 20px; + margin-top: 0px; + text-align: center; + padding: 10px; + border-radius: 8px; + font-family: Arial, + sans-serif; + font-weight: bold; + font-size: 18px; +} + +#logo-extraction-result { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; + margin-top: 10px; + background-color: #f9f9f9; + border: 1px solid #ddd; + border-radius: 8px; +} + +#original-image { + max-height: 100%; + max-width: 100%; + object-fit: contain; +} + +#detection-result { + width: 100%; + height: 100%; + margin-top: 10px; + text-align: left; + padding: 10px; + background-color: #f9f9f9; + border: 1px solid #ddd; + border-radius: 8px; +} + +#detection-label { + display: inline-block; + font-family: Arial, sans-serif; + font-size: 14px; + font-weight: bold; + color: white; + padding: 3px 6px; + border-radius: 16px; + text-align: center; + transition: transform 0.2s, box-shadow 0.2s; +} + +#detection-label.benign { + background: linear-gradient(90deg, #4CAF50, #4CAF50); +} + +#detection-label.phishing { + background: linear-gradient(90deg, #F44336, #F44336); +} + +#detection-label.unknown { + background: linear-gradient(90deg, #9E9E9E, #9E9E9E); +} + +#detection-explanation { + font-size: 14px; + color: #333; +} + +.separator { + width: 100%; + height: 2px; + background-color: #ddd; + margin: 10px 0; +} + + +.tasks-list { + list-style: none; + padding: 0; + margin: 0; +} + +.tasks-list li { + display: flex; + align-items: center; + justify-content: flex-start; + padding: 8px 0; + border-bottom: 1px solid #eee; +} + +.tasks-list li:last-child { + border-bottom: none; +} + +.icon { + margin-right: 8px; + font-size: 16px; +} + +.task { + font-size: 14px; + color: #555; + margin-right: 12px; +} + +.result { + font-size: 14px; + color: #5b5b5b; + background-color: #cdcdcd; + padding: 3px 6px; + border-radius: 10px; +} + +#detection-explanation { + font-family: Arial, sans-serif; + font-size: 14px; + line-height: 1.8; + color: #333; + background-color: #f9f9f9; + padding: 16px; + border-left: 4px solid #0078d4; + border-radius: 8px; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); + margin: 16px 0; +} + +#detection-explanation p { + margin: 0; +} + +#detection-explanation strong { + color: #d9534f; + font-weight: bold; + background-color: #fff0f0; + padding: 2px 4px; + border-radius: 4px; +} \ No newline at end of file diff --git a/WEBtool/static/icon/file1.png b/WEBtool/static/icon/file1.png new file mode 100644 index 0000000000000000000000000000000000000000..a7993d5793d4be52e1d19dc3a71ae2d2a39a18ba GIT binary patch literal 2335 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 D$sC+1 literal 0 HcmV?d00001 diff --git a/WEBtool/static/icon/succ.png b/WEBtool/static/icon/succ.png new file mode 100644 index 0000000000000000000000000000000000000000..9fbf3b33ba226b1496938c98efd90d56de69ecb6 GIT binary patch literal 9220 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 literal 0 HcmV?d00001 diff --git a/WEBtool/static/js/main.js b/WEBtool/static/js/main.js new file mode 100644 index 0000000..09579b9 --- /dev/null +++ b/WEBtool/static/js/main.js @@ -0,0 +1,140 @@ +new Vue({ + el: '#main-container', + data() { + return { + url: '', + result: null, + uploadedImage: null, + imageUrl: '', + uploadSuccess: false, + } + }, + methods: { + startDetection() { + if (!this.url) { + alert('Please enter a valid URL.'); + return; + } + + // 发送 POST 请求到 /detect 路由 + fetch('/detect', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + url: this.url, + imageUrl: this.imageUrl + }) + }) + .then(response => response.json()) + .then(data => { + this.result = data; // Update all data + + if (data.logo_extraction) { // Logo Extraction Result + document.getElementById('original-image').src = `data:image/png;base64,${data.logo_extraction}`; + } + + // Detectoin Result + const labelElement = document.getElementById('detection-label'); + const explanationElement = document.getElementById('detection-explanation'); + const matched_brand_element = document.getElementById('matched-brand'); + const siamese_conf_element = document.getElementById('siamese-conf'); + const correct_domain_element = document.getElementById('correct-domain'); + const detection_time_element = document.getElementById('detection-time'); + + detection_time_element.textContent = data.detection_time + ' s'; + if (data.result === 'Benign') { + labelElement.className = 'benign'; + labelElement.textContent = 'Benign'; + matched_brand_element.textContent = data.matched_brand; + siamese_conf_element.textContent = data.confidence; + correct_domain_element.textContent = data.correct_domain; + explanationElement.innerHTML = ` +

This website has been analyzed and determined to be ${labelElement.textContent.toLowerCase()}. + Because we have matched a brand ${data.matched_brand} with confidence ${Math.round(data.confidence * 100, 3)}, + and the domain extracted from url is within the domain list under the brand (which is [${data.correct_domain}]). + Enjoy your surfing!

+ `; + } else if (data.result === 'Phishing') { + labelElement.className = 'phishing'; + labelElement.textContent = 'Phishing'; + matched_brand_element.textContent = data.matched_brand; + siamese_conf_element.textContent = data.confidence; + correct_domain_element.textContent = data.correct_domain; + explanationElement.innerHTML = ` +

This website has been analyzed and determined to be ${labelElement.textContent.toLowerCase()}. + Because we have matched a brand ${data.matched_brand} with confidence ${Math.round(data.confidence * 100, 3)}%, + but the domain extracted from url is NOT within the domain list under the brand (which is [${data.correct_domain}]). + Please proceed with caution!

+ `; + } else { + labelElement.className = 'unknown'; + labelElement.textContent = 'Unknown'; + matched_brand_element.textContent = "unknown"; + siamese_conf_element.textContent = "0.00"; + correct_domain_element.textContent = "unknown"; + explanationElement.innerHTML = ` +

Sorry, we don't find any matched brand in database so this website is determined to be ${labelElement.textContent.toLowerCase()}.

+

It is still possible that this is a phishing site. Please proceed with caution!

+ `; + } + }) + .catch(error => { + console.error('Error:', error); + alert('检测失败,请稍后重试。'); + }); + }, + handleImageUpload(event) { // 处理图片上传事件 + const file = event.target.files[0]; + if (file) { + this.uploadedImage = file; + this.uploadImage(); + } + }, + uploadImage() { // 上传图片到服务器 + const formData = new FormData(); + formData.append('image', this.uploadedImage); + + fetch('/upload', { // 假设上传图片的路由是 /upload + method: 'POST', + body: formData + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + this.imageUrl = data.imageUrl; // 更新图片URL + this.uploadSuccess = true; // 标记上传成功 + } else { + alert('上传图片失败: ' + data.error); + } + }) + .catch(error => { + console.error('Error:', error); + alert('上传图片失败,请稍后重试。'); + }); + }, + clearUpload() { // 清除上传的图像 + fetch('/clear_upload', { // 假设删除图片的路由是 /delete-image + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ imageUrl: this.imageUrl }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + this.imageUrl = ''; + this.uploadSuccess = false; // 重置上传状态 + } else { + alert('删除图片失败: ' + data.error); + } + }) + .catch(error => { + console.error('Error:', error); + alert('删除图片失败,请稍后重试。'); + }); + } + } +}); diff --git a/WEBtool/static/js/sidebar.js b/WEBtool/static/js/sidebar.js new file mode 100644 index 0000000..330c637 --- /dev/null +++ b/WEBtool/static/js/sidebar.js @@ -0,0 +1,330 @@ +// sidebar.js +new Vue({ + el: '#sidebar', + data() { + return { + selectedDirectory: null, // 记录当前选中的目录 + selectedFile: null, // 记录当前选中的文件 + selectedDirectoryName: '', + selectedFileName: '', + showAddBrandForm: false, // 控制表单显示与隐藏 + brandName: '', // 品牌名称 + brandDomain: '', // 品牌域名 + } + }, + mounted() { + // 网页加载时调用 fetchFileTree 函数 + this.fetchFileTree(); + document.getElementById('logo-file-input').addEventListener('change', this.handleLogoFileSelect); + + const sidebar = document.getElementById("sidebar"); + const sidebarToggle = document.getElementById("sidebar-toggle"); + const closeSidebar = document.getElementById("close-sidebar"); + + // 点击打开侧边栏 + sidebarToggle.addEventListener("click", () => { + sidebar.classList.add("open"); + }); + + // 点击关闭侧边栏 + closeSidebar.addEventListener("click", () => { + sidebar.classList.remove("open"); + this.clearSelected(); + }); + + // 点击侧边栏外部关闭 + document.addEventListener("click", (event) => { + if (!sidebar.contains(event.target) && !sidebarToggle.contains(event.target)) { + sidebar.classList.remove("open"); + this.clearSelected(); + } + }); + }, + methods: { + // 递归渲染文件树 + renderFileTree(directory, parentPath = '') { + // 获取文件树容器 + const fileTreeRoot = document.getElementById('file-tree-root'); + fileTreeRoot.innerHTML = ''; // 清空现有内容 + + // 递归生成文件树节点 + const createFileTreeNode = (item, parentPath) => { + const li = document.createElement('li'); + li.classList.add('file-item'); + + const currentPath = parentPath ? `${parentPath}/${item.name}` : item.name; + + if (item.type === 'directory') { + li.classList.add('file-folder'); + + const folderNameContainer = document.createElement('div'); + folderNameContainer.classList.add('folder-name'); + folderNameContainer.innerHTML = `📁${item.name}`; + li.appendChild(folderNameContainer); + + if (item.children) { + const ul = document.createElement('ul'); + ul.classList.add('hidden'); // 默认隐藏子目录 + item.children.forEach((child) => { + ul.appendChild(createFileTreeNode(child, currentPath)); // 传递当前目录的路径 + }); + li.appendChild(ul); + + // 单击选中目录 + folderNameContainer.addEventListener('click', (e) => { + e.stopPropagation(); + this.selectDirectory(e, item.name); + }); + + // 双击展开/隐藏目录 + folderNameContainer.addEventListener('dblclick', (e) => { + e.stopPropagation(); + ul.classList.toggle('hidden'); + }); + } + } else { + li.classList.add('file-file'); + li.innerHTML = `📄${item.name}`; + + // 单击选中文件 + li.addEventListener('click', (event) => { + this.selectFile(event, item.name, parentPath); + }); + } + + return li; + }; + + // 遍历顶层文件和目录 + directory.forEach((item) => { + fileTreeRoot.appendChild(createFileTreeNode(item, parentPath)); + }); + }, + // 获取文件树数据 + fetchFileTree() { + // 发送请求获取文件树数据 + fetch('/get-directory') // 后端文件树接口 + .then((response) => response.json()) + .then((data) => { + if (data.file_tree) { + this.fileTree = data.file_tree; // 存储文件树数据 + this.renderFileTree(this.fileTree); // 渲染文件树 + } else { + console.error('Invalid file tree data'); + alert('文件树加载失败'); + } + }) + .catch((error) => { + console.error('Error fetching file tree:', error); + alert('无法加载文件树,请稍后重试。'); + }); + }, + + // 选中目录 + selectDirectory(event, directoryName) { + const folderNameContainer = event.currentTarget; + + if (this.selectedDirectory) { + this.selectedDirectory.classList.remove('selected'); + } + if (this.selectedFile) { + this.selectedFile.classList.remove('selected'); + } + + // 设置当前选中的目录 + this.selectedDirectory = folderNameContainer; + this.selectedDirectoryName = directoryName; + folderNameContainer.classList.add('selected'); + this.selectedFile = null; + this.selectedFileName = ''; + }, + + // 选中文件 + selectFile(event, fileName, parentPath) { + const fileElement = event.currentTarget; + + if (this.selectedDirectory) { + this.selectedDirectory.classList.remove('selected'); + } + if (this.selectedFile) { + this.selectedFile.classList.remove('selected'); + } + + // 设置当前选中的文件 + this.selectedFile = fileElement; + this.selectedFileName = fileName; + fileElement.classList.add('selected'); + this.selectedDirectory = null; + this.selectedDirectoryName = parentPath; + }, + + // 增加品牌 + addBrand() { + this.showAddBrandForm = true; + }, + + // 关闭添加品牌的表单 + closeAddBrandForm() { + this.showAddBrandForm = false; + this.brandName = ''; + this.brandDomain = ''; + }, + + // 提交添加品牌的表单 + submitAddBrandForm() { + if (!this.brandName || !this.brandDomain) { + alert('Please fill in all fields.'); + closeAddBrandForm() + return; + } + + const formData = new FormData(); + formData.append('brandName', this.brandName); + formData.append('brandDomain', this.brandDomain); + + fetch('/add-brand', { + method: 'POST', + body: formData + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + alert('Brand added successfully.'); + this.fetchFileTree(); + this.closeAddBrandForm(); + } else { + alert('Failed to add brand: ' + data.error); + } + }) + .catch(error => { + console.error('Error:', error); + alert('Failed to add brand, please try again.'); + }); + }, + + // 删除品牌 + delBrand() { + if (this.selectedDirectory == null) { + alert('Please select a brand first.'); + return; + } + const formData = new FormData(); + formData.append('directory', this.selectedDirectoryName); + + fetch('/del-brand', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + directory: this.selectedDirectoryName + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + alert('Brand deletedsuccessfully.'); + this.fetchFileTree(); + } + }) + }, + + // 增加logo + addLogo() { + console.log('addLogo'); + if (this.selectedDirectory == null) { + alert('Please select a brand first.'); + return; + } + document.getElementById('logo-file-input').click(); + }, + + handleLogoFileSelect(event) { + const file = event.target.files[0]; + if (file) { + const formData = new FormData(); + formData.append('logo', file); + formData.append('directory', this.selectedDirectoryName); + + fetch('/add-logo', { + method: 'POST', + body: formData + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + this.fetchFileTree(); + } else { + alert('Failed to add logo: ' + data.error); + } + }) + .catch(error => { + console.error('Error:', error); + alert('Failed to add logo, please try again.'); + }); + } + }, + + // 删除logo + delLogo() { + if (this.selectedFile == null) { + alert('Please select a logo first.'); + return; + } + + const formData = new FormData(); + formData.append('directory', this.selectedDirectoryName); + formData.append('filename', this.selectedFileName); + + fetch('/del-logo', { + method: 'POST', + body: formData + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + this.fetchFileTree(); + } else { + alert('Failed to delete logo: ' + data.error); + } + }) + .catch(error => { + console.error('Error:', error); + alert('Failed to delete logo, please try again.'); + }); + }, + + async reloadModel() { + const overlay = document.getElementById('overlay'); + + overlay.style.display = 'flex'; + + try { + const response = await fetch('/reload-model', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }); + const data = await response.json(); + } catch (error) { + alert('Failed to reload model.'); + } finally { + overlay.style.display = 'none'; + } + }, + + clearSelected() { + if (this.selectedDirectory) { + this.selectedDirectory.classList.remove('selected'); + this.selectDirectory = null; + } + if (this.selectedFile) { + this.selectedFile.classList.remove('selected'); + this.selectFile = null; + } + this.selectedDirectoryName = ''; + this.selectedFileName = ''; + }, + } +}); \ No newline at end of file diff --git a/WEBtool/templates/index.html b/WEBtool/templates/index.html new file mode 100644 index 0000000..b4c7f84 --- /dev/null +++ b/WEBtool/templates/index.html @@ -0,0 +1,163 @@ + + + + + + + PhishPedia + + + + + + + + + + + + + + +
+
+
+ +
+ + +
+ +
+
+ Upload Icon +

+ +

Or ctrl+v here

+ +
+
+
+ Success Icon + Uploaded Successfully! +
+ Uploaded Image + +
+
+ + +
+
+
+
+
+ Logo Extraction +
+ Original Webpage Screenshot +
+
+
+ Detection Result +
+
+ 📊 + Result +
+
+
+
+
    +
  • + 🏷️ + Matched Brand + +
  • +
  • + 💬 + Siamese Confidence + +
  • +
  • + 🌐 + Correct Domain + +
  • +
  • + ⏱️ + Detection Time + +
  • +
  • +
    +
  • +
+
+
+
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/WEBtool/utils_web.py b/WEBtool/utils_web.py new file mode 100644 index 0000000..74bdfd8 --- /dev/null +++ b/WEBtool/utils_web.py @@ -0,0 +1,89 @@ +# help function for phishpedia web app +import os +import pickle +import shutil +import socket +import base64 +import io +from PIL import Image +import cv2 +def check_port_inuse(port, host): + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(1) + s.connect((host, port)) + return True + except socket.error: + return False + finally: + if s: + s.close() + +def allowed_file(filename): + return '.' in filename and \ + filename.rsplit('.', 1)[1].lower() in {'png', 'jpg', 'jpeg'} + + +def initial_upload_folder(upload_folder): + try: + shutil.rmtree(upload_folder) + except FileNotFoundError: + pass + os.makedirs(upload_folder, exist_ok=True) + + +def convert_to_base64(image_array): + if image_array is None: + return None + + image_array_rgb = cv2.cvtColor(image_array, cv2.COLOR_BGR2RGB) + img = Image.fromarray(image_array_rgb) + buffered = io.BytesIO() + img.save(buffered, format="PNG") + plotvis_base64 = base64.b64encode(buffered.getvalue()).decode('utf-8') + return plotvis_base64 + + +def domain_map_add(brand_name, domains_str, domain_map_path): + domains = [domain.strip() for domain in domains_str.split(',') if domain.strip()] + + # Load existing domain mapping + with open(domain_map_path, 'rb') as f: + domain_map = pickle.load(f) + + # Add new brand and domains + if brand_name in domain_map: + if isinstance(domain_map[brand_name], list): + # Add new domains, avoid duplicates + existing_domains = set(domain_map[brand_name]) + for domain in domains: + if domain not in existing_domains: + domain_map[brand_name].append(domain) + else: + # If current value is not a list, convert to list + old_domain = domain_map[brand_name] + domain_map[brand_name] = [old_domain] + [d for d in domains if d != old_domain] + else: + domain_map[brand_name] = domains + + # Save updated mapping + with open(domain_map_path, 'wb') as f: + pickle.dump(domain_map, f) + + +def domain_map_delete(brand_name, domain_map_path): + # Load existing domain mapping + with open(domain_map_path, 'rb') as f: + domain_map = pickle.load(f) + + print("before deleting", len(domain_map)) + + # Delete brand and its domains + if brand_name in domain_map: + del domain_map[brand_name] + + print("after deleting", len(domain_map)) + + # Save updated mapping + with open(domain_map_path, 'wb') as f: + pickle.dump(domain_map, f) \ No newline at end of file From 6902a04ac27f367713664aa1aa958d9961e5d9f0 Mon Sep 17 00:00:00 2001 From: Weiyu-Kong <1625827540@qq.com> Date: Sat, 28 Dec 2024 14:45:19 +0800 Subject: [PATCH 27/42] Add readme for web tool and requirements.txt --- WEBtool/mainpage.png | Bin 0 -> 74087 bytes WEBtool/readme.md | 83 +++++++++++++++++++++++++++++++++++++++ WEBtool/requirements.txt | 4 ++ WEBtool/sidebar.png | Bin 0 -> 130787 bytes 4 files changed, 87 insertions(+) create mode 100644 WEBtool/mainpage.png create mode 100644 WEBtool/readme.md create mode 100644 WEBtool/requirements.txt create mode 100644 WEBtool/sidebar.png diff --git a/WEBtool/mainpage.png b/WEBtool/mainpage.png new file mode 100644 index 0000000000000000000000000000000000000000..48078149b245e9c59e7f833b5569bf923522521b GIT binary patch literal 74087 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 NyK^5}c+33R{{k9V2aW&$ literal 0 HcmV?d00001 diff --git a/WEBtool/readme.md b/WEBtool/readme.md new file mode 100644 index 0000000..1fa9863 --- /dev/null +++ b/WEBtool/readme.md @@ -0,0 +1,83 @@ +# Phishpedia Web Tool + +This is a web tool for Phishpedia which provides a user-friendly interface with brand and domain management capabilities, as well as visualization features for phishing detection. + +## Installation Requirements + +Before using, make sure all necessary dependencies are installed: + +```bash +~Phishpedia$ cd WEBtool +~Phishpedia/WEBtool$ pip install -r requirements.txt +``` + +## How to Run + +Run the following command in the web tool directory: + +```bash +~Phishpedia/WEBtool$ python phishpedia_gui.py +``` + +you should see an URL after the server is started (http://127.0.0.1:500x). Visit it in your browser. + +## User Guide + +### 1. Main Page (For phishing detection) + +![image-20241228141453032](./mainpage.png) + +1. **URL Detection** + - Enter the URL to be tested in the "Enter URL" input box + - Click the "Upload Image" button to select the corresponding website screenshot + - Click the "Start Detection!" button to start detection + - Detection results will be displayed below, including text results and visual presentation +2. **Result Display** + - The original image with logo extracted will be displayed in the "Logo Extraction" box + - Detection results will be displayed in the "Detection Result" box, together with a synthetic explanation + - You can clearly see the detected brand identifiers and related information + + + +### 2. Sidebar (For database management) + +Click the sidebar button "☰" at top right corner, this will trigger a sidebar showing database at backend. + +![image-20241228141419609](./sidebar.png) + +1. **Brand Management** + - Click "Add Brand" to add a new brand + - Enter brand name and corresponding domains in the form + - Click one brand to select, and click "Delete Brand" to remove the selected brand + - Double-click one brand to see the logo under this brand +2. **Logo Management** + - Click one brand to select, and click "Add Logo" to add brand logos + - Click one logo to select, and click "Delete Logo" to remove selected logo +3. **Data Update** + - After making changes, click the "Reload Model" button + - The system will reload the updated dataset + +## Main Features + +1. **Phishing Detection** + - URL input and detection + - Screenshot upload and analysis + - Detection result visualization + +2. **Brand Management** + - Add/Delete brands + - Add/Delete brand logos + - Domain management + - Model reloading + +## Directory Structure + +``` +WEBtool/ +├── static/ # Static resources like css,icon +├── templates/ # Web page +├── phishpedia_web.py # A flask server +├── utils_web.py # Help functions for server +├── readme.md # Documentation +└── requirements.txt # Dependency list +``` diff --git a/WEBtool/requirements.txt b/WEBtool/requirements.txt new file mode 100644 index 0000000..5d4d54a --- /dev/null +++ b/WEBtool/requirements.txt @@ -0,0 +1,4 @@ +Flask +flask_cors +Pillow +opencv-python \ No newline at end of file diff --git a/WEBtool/sidebar.png b/WEBtool/sidebar.png new file mode 100644 index 0000000000000000000000000000000000000000..9c46d624980da935fc8bdce56f72bb85991e5613 GIT binary patch literal 130787 zcmd42^{@%RYhCMFYe&6OSH#14j&twcJv?P4Ijwv59xva!_izXM zKClAc^M>EMM|V$IPFlw|XKyJWM|Z|g>ede0dEPpjDaO;wgM-Zw`Sw$Jxh^kJk)U!@ z)`eP@O1Ijii9kiv##w{IHN+~5{FvW?2m=yC-MTv`fA3>4iIWPnqW=TB)rvp zJjdf~83Nrr9~8Co$PnWxip=-f?ikBK1fZMdBMqxB=m+R^JIy!;Jx7V9v66V;_ zz{UM{TYZ~}*0+k~RUpgcahs%+5WDx*jXCp@KmYPXWK&g<3-`bOdGB5=fA-w{dmSX- z+S=NVH%D1nS^bLae&6bUHdhjpf0XS0`oSryUQrd(Z|Qm>@Sm@M-#AzN9FVbiI{jU{rpGez(A9{eg@c&PT$rV#yeZDbf;-VOE9W4=% z=PFKQA2efV7jjxzm9+0`O8Bqa`OzrICGB7gx}NR|8mF4?ZfcsDLv~$G-v({!=q&n8 z$gjG||7(Z0Q;2y*7-c*t$fGx=*2`0_pt^sQ9e+=1!v1U2rnTlBuddWeh-Zp_iTvl~gHby*NCY4o&}?{AEbCe&~kv7M5Bl?}F7 zu>RLEgrTv99}wTCj+fN3gHdl@t193&-7asyr50|Z`8JY}UHg3(s?eHb+#_bQp^eSe-#jZ>WP^e&Qxb7rbqi7sWR zx7hLx=X`lI^2gQwD*bh_nh^65t zQ`;pf=}$#{>vh_Fhs$=%Uq3V{zV_NBLosdwl*kr>(;H^br|Ow>zkmG&ZSm1)hmU4Z)-E9 z5b@8hVzV;_nQylBZGFHR6l8JnalTA+~{w=V07dHy>^8d4aas$kUOoDkF0qyOFf+o^nB z+l6N5^F@#G4!`4OCj{RXZ#Kx;(cV6?%e3B7-pXoK**q~b%6noWApa9`z}C5B_?Q!>sy4> z%}*7s*5Zqrzh39$1PV~vW4>>|@tv1*o`&*ff?Wv$ElzjL_mLH`mlt8UQZ2rReb(+5 z+D|=w?xxzB#E{4l%ldIs_n|w{kQ(-DR3e}4WsNCq@X-*KkV~NZT^BVPjpg5V?&H&m zoJf4mdj1EdyyYJ+kd0WrUOs>N{k-BEY-U7EEof;;2GuLzP$aavx?{UEdoOz=om(G_m@6qMfy`F_ z3>_F4sL(D$(n_4oTTNUC1qG3r2l5hjUaZ6@##7N3#&CD}?k~2=c>>PwwL6O^_%kUf zDUMReJ+Y>yru*)?yG3fL(P66Dc_BLCV1K_^j1iaKr5odYy+N!YK7l|WT-RtZTZ*8s zxx7{bS%Q#Iy4M?}rKK9dN6BYzQJ8(72kcQ&Ck6P21hdtK+D1mAi#rc;Y~yJpuEMwj zE$(hHy;MahSL1g#5qZ9yZTd`kbij~k zj?BnX^<4k+B_EA!P+TO)8TrhsElzH|nfCIVVomGD^YP=z^5*8VuIqi+do1ZX!0^eR z)!qt)6)7ex2c0jCOJ47p`Ybg;=4zx9G{Hf4*Qk(-pAodaKi=iv-E6KVj^(`(sEWP( zIt$>fO*TalqR|ePZw~YCUzT9(vm-M<+K%C>j2w)RvhlIoL`ijfo9k~DCna&4Hm{XZ z@IOLxw_BpR@?An~UI*_fUCteEsF6Fg{FxYpdwuiHTSBf|>J4HeS91B_@bBg?DQ1jT2)yIa+-gQ zYaw-en2_IBeQ)`hk+Jat5*fMmE%AlVlGogqv{7&c7LD%@JE`kT-tbI(YSH3B`{N&$ z%vA6i@wz+a2kwT_o_dCtJ;_F>!nlaU2E9Pt=j6kl@ zvOgM#vIL?c z0mB`%?bfVx-*z2@V0O*Ty8>Hz6~LGJA*cGl#V3eOa&
?=h4&dJT&C7$GG0U>(2 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 K!*a!^fBiQucg4K` literal 0 HcmV?d00001 From 9473e5c50fdca1b759c3d84ae050e0e3cfc7fbf0 Mon Sep 17 00:00:00 2001 From: sk_han Date: Sat, 28 Dec 2024 15:28:49 +0800 Subject: [PATCH 28/42] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=B5=8F=E8=A7=88?= =?UTF-8?q?=E5=99=A8=E6=8F=92=E4=BB=B6=E5=90=8E=E7=AB=AFDocker=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Plugin_for_Chrome/README.md | 8 ++++++++ Plugin_for_Chrome/dockerfile | 36 ++++++++++++++++++++++++++++++++++++ app.py | 17 ++++++++++++++++- 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 Plugin_for_Chrome/dockerfile diff --git a/Plugin_for_Chrome/README.md b/Plugin_for_Chrome/README.md index 806e94e..b007c23 100644 --- a/Plugin_for_Chrome/README.md +++ b/Plugin_for_Chrome/README.md @@ -42,6 +42,14 @@ Plugin_for_Chrome/ ```sh python app.py ``` +**或者** + +使用Docker进行部署 +```sh +docker build -t phishpedia-app . + +docker run -p 5000:5000 phishpedia-app +``` ### 使用插件 diff --git a/Plugin_for_Chrome/dockerfile b/Plugin_for_Chrome/dockerfile new file mode 100644 index 0000000..5e6177e --- /dev/null +++ b/Plugin_for_Chrome/dockerfile @@ -0,0 +1,36 @@ +# Use Ubuntu 20.04 as the base image +FROM ubuntu:20.04 +# Prevent interactive prompts during installation +ARG DEBIAN_FRONTEND=noninteractive +# Install Miniconda +RUN apt-get update && apt-get install -y wget && \ + wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh && \ + bash Miniconda3-latest-Linux-x86_64.sh -b -p /opt/miniconda && \ + rm Miniconda3-latest-Linux-x86_64.sh +# Add Miniconda to PATH +ENV PATH="/opt/miniconda/bin:${PATH}" +# Set working directory +WORKDIR /workspace +# Install git, unzip and other dependencies +RUN apt-get install -y git unzip libgl1-mesa-glx libglib2.0-0 +# Clone the Phishpedia project from GitHub into the container +RUN git clone https://github.com/lindsey98/Phishpedia.git /workspace/Phishpedia +# Change to the project directory and run setup.sh to configure the environment +WORKDIR /workspace/Phishpedia +# Install dos2unix +RUN apt-get install -y dos2unix +# Convert setup.sh to Unix format and RUN it +RUN dos2unix setup.sh +RUN chmod +x setup.sh +RUN bash setup.sh || true +# Install Flask and Flask-CORS +RUN /opt/miniconda/envs/phishpedia/bin/pip install "werkzeug>=2.3.7" "flask>=2.3.3" "flask-cors>=4.0.0" +# Ensure Conda is initialized and phishpedia environment is activated by default +RUN echo "source /opt/miniconda/etc/profile.d/conda.sh && conda activate phishpedia" >> ~/.bashrc +# Set the default command to execute when the container starts +CMD ["bash", "-c", "source /opt/miniconda/etc/profile.d/conda.sh && conda activate phishpedia && cd /workspace/Phishpedia && python app.py"] +# Expose the port that the application may use (assuming app.py uses port 5000) +EXPOSE 5000 +# Add health check +HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:5000/ || exit 1 \ No newline at end of file diff --git a/app.py b/app.py index ff95865..a01c271 100644 --- a/app.py +++ b/app.py @@ -69,5 +69,20 @@ def analyze(): return jsonify("ERROR"), 500 +def is_running_in_docker(): + """检查是否在 Docker 环境中运行""" + # 方法1:检查 /.dockerenv 文件是否存在 + docker_env = os.path.exists('/.dockerenv') + # 方法2:检查 cgroup 中是否包含 docker 字符串 + try: + with open('/proc/1/cgroup', 'r') as f: + return docker_env or 'docker' in f.read() + except: + return docker_env + + if __name__ == '__main__': - app.run(debug=False) + if is_running_in_docker(): + app.run(host='0.0.0.0', port=5000, debug=False) + else: + app.run(debug=False) From 41df37f3e7d968e4695691df8ea8cdcd58e79a29 Mon Sep 17 00:00:00 2001 From: sk_han Date: Sat, 28 Dec 2024 15:34:36 +0800 Subject: [PATCH 29/42] =?UTF-8?q?=E8=A7=A3=E5=86=B3Lint=E6=8A=A5=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.py b/app.py index a01c271..60fe918 100644 --- a/app.py +++ b/app.py @@ -77,7 +77,7 @@ def is_running_in_docker(): try: with open('/proc/1/cgroup', 'r') as f: return docker_env or 'docker' in f.read() - except: + except (IOError, OSError): # 明确指定可能的异常类型 return docker_env From 9a084bd5d75ec3438740781765a80ed5f72dfab9 Mon Sep 17 00:00:00 2001 From: Weiyu-Kong <1625827540@qq.com> Date: Sat, 28 Dec 2024 15:50:19 +0800 Subject: [PATCH 30/42] fix to pass lint check --- WEBtool/phishpedia_web.py | 32 +++++++++++++++++++++----------- WEBtool/readme.md | 1 + WEBtool/utils_web.py | 5 ++++- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/WEBtool/phishpedia_web.py b/WEBtool/phishpedia_web.py index 5e80536..fc9e3ec 100644 --- a/WEBtool/phishpedia_web.py +++ b/WEBtool/phishpedia_web.py @@ -1,10 +1,9 @@ import os import sys -from flask import request, Flask, jsonify,render_template,send_from_directory +import shutil +from flask import request, Flask, jsonify, render_template, send_from_directory from flask_cors import CORS -from utils_web import * - -sys.path.append('..') +from utils_web import allowed_file, convert_to_base64, domain_map_add, domain_map_delete, check_port_inuse, initial_upload_folder from configs import load_config from phishpedia import PhishpediaWrapper @@ -18,11 +17,13 @@ app.config['FILE_TREE_ROOT'] = '../models/expand_targetlist' # 主目录路径 app.config['DOMAIN_MAP_PATH'] = '../models/domain_map.pkl' + @app.route('/') def index(): """渲染主页面""" return render_template('index.html') + @app.route('/upload', methods=['POST']) def upload_file(): """处理文件上传请求""" @@ -41,11 +42,13 @@ def upload_file(): return jsonify({'error': 'Invalid file type'}), 400 + @app.route('/uploads/') def uploaded_file(filename): """提供上传文件的访问路径""" return send_from_directory(app.config['UPLOAD_FOLDER'], filename) + @app.route('/clear_upload', methods=['POST']) def delete_image(): data = request.get_json() @@ -89,15 +92,16 @@ def detect(): # 返回检测结果 result = { - 'result': result, # 检测结果 - 'matched_brand':pred_target, # 匹配到的品牌 - 'correct_domain':matched_domain, # 正确的域名 - 'confidence': round(float(siamese_conf),3), # 置信度,直接返回百分比 - 'detection_time': round(float(logo_recog_time)+float(logo_match_time),3), # 检测时间 + 'result': result, # 检测结果 + 'matched_brand': pred_target, # 匹配到的品牌 + 'correct_domain': matched_domain, # 正确的域名 + 'confidence': round(float(siamese_conf), 3), # 置信度,直接返回百分比 + 'detection_time': round(float(logo_recog_time) + float(logo_match_time), 3), # 检测时间 'logo_extraction': plot_base64 # logo标注结果,直接返回图像 } return jsonify(result) + @app.route('/get-directory', methods=['GET']) def get_file_tree(): """ @@ -132,6 +136,7 @@ def build_file_tree(path): file_tree = build_file_tree(root_path) return jsonify({'file_tree': file_tree}), 200 + @app.route('/view-file', methods=['GET']) def view_file(): file_name = request.args.get('file') @@ -172,6 +177,7 @@ def add_logo(): return jsonify({'success': False, 'error': 'Invalid file type'}), 400 + @app.route('/del-logo', methods=['POST']) def del_logo(): directory = request.form.get('directory') @@ -192,6 +198,7 @@ def del_logo(): except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 + @app.route('/add-brand', methods=['POST']) def add_brand(): brand_name = request.form.get('brandName') @@ -212,6 +219,7 @@ def add_brand(): except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 + @app.route('/del-brand', methods=['POST']) def del_brand(): directory = request.json.get('directory') @@ -231,8 +239,10 @@ def del_brand(): except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 + @app.route('/reload-model', methods=['POST']) def reload_model(): + global phishpedia_cls try: load_config(reload_targetlist=True) # Reinitialize Phishpedia @@ -242,7 +252,6 @@ def reload_model(): return jsonify({'success': False, 'error': str(e)}), 500 - if __name__ == "__main__": ip_address = '0.0.0.0' port = 5000 @@ -254,4 +263,5 @@ def reload_model(): initial_upload_folder(app.config['UPLOAD_FOLDER']) - app.run(host=ip_address, port=port) \ No newline at end of file + app.run(host=ip_address, port=port) + \ No newline at end of file diff --git a/WEBtool/readme.md b/WEBtool/readme.md index 1fa9863..5d402ec 100644 --- a/WEBtool/readme.md +++ b/WEBtool/readme.md @@ -16,6 +16,7 @@ Before using, make sure all necessary dependencies are installed: Run the following command in the web tool directory: ```bash +~Phishpedia/WEBtool$ export PYTHONPATH=.. ~Phishpedia/WEBtool$ python phishpedia_gui.py ``` diff --git a/WEBtool/utils_web.py b/WEBtool/utils_web.py index 74bdfd8..6988e2f 100644 --- a/WEBtool/utils_web.py +++ b/WEBtool/utils_web.py @@ -7,6 +7,7 @@ import io from PIL import Image import cv2 + def check_port_inuse(port, host): try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -19,6 +20,7 @@ def check_port_inuse(port, host): if s: s.close() + def allowed_file(filename): return '.' in filename and \ filename.rsplit('.', 1)[1].lower() in {'png', 'jpg', 'jpeg'} @@ -86,4 +88,5 @@ def domain_map_delete(brand_name, domain_map_path): # Save updated mapping with open(domain_map_path, 'wb') as f: - pickle.dump(domain_map, f) \ No newline at end of file + pickle.dump(domain_map, f) + \ No newline at end of file From 0404dfcd33f6659672751d1018f95959c69bed7c Mon Sep 17 00:00:00 2001 From: Weiyu-Kong <1625827540@qq.com> Date: Sat, 28 Dec 2024 16:17:34 +0800 Subject: [PATCH 31/42] fix to pass lint check and CodeQL warning --- WEBtool/phishpedia_web.py | 37 +++++++++++++++++++++++-------------- WEBtool/utils_web.py | 2 +- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/WEBtool/phishpedia_web.py b/WEBtool/phishpedia_web.py index fc9e3ec..95d4ed4 100644 --- a/WEBtool/phishpedia_web.py +++ b/WEBtool/phishpedia_web.py @@ -1,5 +1,4 @@ import os -import sys import shutil from flask import request, Flask, jsonify, render_template, send_from_directory from flask_cors import CORS @@ -37,6 +36,7 @@ def upload_file(): if file and allowed_file(file.filename): filename = file.filename file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) + file_path = os.path.normpath(file_path) file.save(file_path) return jsonify({'success': True, 'imageUrl': f'/uploads/{filename}'}), 200 @@ -61,10 +61,11 @@ def delete_image(): # 假设 image_url 是相对于静态目录的路径 filename = image_url.split('/')[-1] image_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) + image_path = os.path.normpath(image_path) os.remove(image_path) - return jsonify({'success': True}) - except Exception as e: - return jsonify({'success': False, 'error': str(e)}), 500 + return jsonify({'success': True}), 200 + except Exception: + return jsonify({'success': False}), 500 @app.route('/detect', methods=['POST']) @@ -75,6 +76,7 @@ def detect(): filename = imageUrl.split('/')[-1] screenshot_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) + screenshot_path = os.path.normpath(screenshot_path) phish_category, pred_target, matched_domain, plotvis, siamese_conf, pred_boxes, logo_recog_time, logo_match_time = phishpedia_cls.test_orig_phishpedia( url, screenshot_path, None) @@ -112,6 +114,7 @@ def build_file_tree(path): try: for entry in os.listdir(path): entry_path = os.path.join(path, entry) + entry_path = os.path.normpath(entry_path) if os.path.isdir(entry_path): tree.append({ 'name': entry, @@ -141,7 +144,7 @@ def build_file_tree(path): def view_file(): file_name = request.args.get('file') file_path = os.path.join(app.config['FILE_TREE_ROOT'], file_name) - print(file_name) + file_path = os.path.normpath(file_path) if not os.path.exists(file_path): return jsonify({'error': 'File not found'}), 404 @@ -167,11 +170,13 @@ def add_logo(): return jsonify({'success': False, 'error': 'No directory specified'}), 400 directory_path = os.path.join(app.config['FILE_TREE_ROOT'], directory) + directory_path = os.path.normpath(directory_path) if not os.path.exists(directory_path): return jsonify({'success': False, 'error': 'Directory does not exist'}), 400 file_path = os.path.join(directory_path, logo.filename) + file_path = os.path.normpath(file_path) logo.save(file_path) return jsonify({'success': True, 'message': 'Logo added successfully'}), 200 @@ -187,7 +192,9 @@ def del_logo(): return jsonify({'success': False, 'error': 'Directory and filename must be specified'}), 400 directory_path = os.path.join(app.config['FILE_TREE_ROOT'], directory) + directory_path = os.path.normpath(directory_path) file_path = os.path.join(directory_path, filename) + file_path = os.path.normpath(file_path) if not os.path.exists(file_path): return jsonify({'success': False, 'error': 'File does not exist'}), 400 @@ -195,8 +202,8 @@ def del_logo(): try: os.remove(file_path) return jsonify({'success': True, 'message': 'Logo deleted successfully'}), 200 - except Exception as e: - return jsonify({'success': False, 'error': str(e)}), 500 + except Exception: + return jsonify({'success': False}), 500 @app.route('/add-brand', methods=['POST']) @@ -209,6 +216,8 @@ def add_brand(): # 创建品牌目录 brand_directory_path = os.path.join(app.config['FILE_TREE_ROOT'], brand_name) + brand_directory_path = os.path.normpath(brand_directory_path) + if os.path.exists(brand_directory_path): return jsonify({'success': False, 'error': 'Brand already exists'}), 400 @@ -216,8 +225,8 @@ def add_brand(): os.makedirs(brand_directory_path) domain_map_add(brand_name, brand_domain, app.config['DOMAIN_MAP_PATH']) return jsonify({'success': True, 'message': 'Brand added successfully'}), 200 - except Exception as e: - return jsonify({'success': False, 'error': str(e)}), 500 + except Exception: + return jsonify({'success': False}), 500 @app.route('/del-brand', methods=['POST']) @@ -228,6 +237,7 @@ def del_brand(): return jsonify({'success': False, 'error': 'Directory must be specified'}), 400 directory_path = os.path.join(app.config['FILE_TREE_ROOT'], directory) + directory_path = os.path.normpath(directory_path) if not os.path.exists(directory_path): return jsonify({'success': False, 'error': 'Directory does not exist'}), 400 @@ -236,8 +246,8 @@ def del_brand(): shutil.rmtree(directory_path) domain_map_delete(directory, app.config['DOMAIN_MAP_PATH']) return jsonify({'success': True, 'message': 'Brand deleted successfully'}), 200 - except Exception as e: - return jsonify({'success': False, 'error': str(e)}), 500 + except Exception: + return jsonify({'success': False}), 500 @app.route('/reload-model', methods=['POST']) @@ -248,8 +258,8 @@ def reload_model(): # Reinitialize Phishpedia phishpedia_cls = PhishpediaWrapper() return jsonify({'success': True, 'message': 'Brand deleted successfully'}), 200 - except Exception as e: - return jsonify({'success': False, 'error': str(e)}), 500 + except Exception: + return jsonify({'success': False}), 500 if __name__ == "__main__": @@ -264,4 +274,3 @@ def reload_model(): initial_upload_folder(app.config['UPLOAD_FOLDER']) app.run(host=ip_address, port=port) - \ No newline at end of file diff --git a/WEBtool/utils_web.py b/WEBtool/utils_web.py index 6988e2f..d2769b2 100644 --- a/WEBtool/utils_web.py +++ b/WEBtool/utils_web.py @@ -8,6 +8,7 @@ from PIL import Image import cv2 + def check_port_inuse(port, host): try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -89,4 +90,3 @@ def domain_map_delete(brand_name, domain_map_path): # Save updated mapping with open(domain_map_path, 'wb') as f: pickle.dump(domain_map, f) - \ No newline at end of file From 583a8b0e64458ff8975b8db159252b5cceafe753 Mon Sep 17 00:00:00 2001 From: Weiyu-Kong <1625827540@qq.com> Date: Sat, 28 Dec 2024 17:08:33 +0800 Subject: [PATCH 32/42] add filename check to pass CodeQL check --- WEBtool/phishpedia_web.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/WEBtool/phishpedia_web.py b/WEBtool/phishpedia_web.py index 95d4ed4..d684c83 100644 --- a/WEBtool/phishpedia_web.py +++ b/WEBtool/phishpedia_web.py @@ -35,6 +35,12 @@ def upload_file(): if file and allowed_file(file.filename): filename = file.filename + if filename.count('.') > 1: + return jsonify({'error': 'Invalid file name'}), 400 + elif any(sep in filename for sep in (os.sep, os.altsep)): + return jsonify({'error': 'Invalid file name'}), 400 + elif '..' in filename: + return jsonify({'error': 'Invalid file name'}), 400 file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) file_path = os.path.normpath(file_path) file.save(file_path) From 5da9748602aa9cf799b11c91f48b95754288617b Mon Sep 17 00:00:00 2001 From: Weiyu-Kong <1625827540@qq.com> Date: Sat, 28 Dec 2024 17:11:54 +0800 Subject: [PATCH 33/42] add path check --- WEBtool/phishpedia_web.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/WEBtool/phishpedia_web.py b/WEBtool/phishpedia_web.py index d684c83..a707b00 100644 --- a/WEBtool/phishpedia_web.py +++ b/WEBtool/phishpedia_web.py @@ -43,6 +43,9 @@ def upload_file(): return jsonify({'error': 'Invalid file name'}), 400 file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) file_path = os.path.normpath(file_path) + if not file_path.startswith(app.config['UPLOAD_FOLDER']): + return jsonify({'error': 'Invalid file path'}), 400 + file.save(file_path) return jsonify({'success': True, 'imageUrl': f'/uploads/{filename}'}), 200 From bac580371b561fdfb8221281f7fc45388a4e3e8a Mon Sep 17 00:00:00 2001 From: Weiyu-Kong <1625827540@qq.com> Date: Sat, 28 Dec 2024 17:22:56 +0800 Subject: [PATCH 34/42] add path check for other paths --- WEBtool/phishpedia_web.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/WEBtool/phishpedia_web.py b/WEBtool/phishpedia_web.py index a707b00..ded6459 100644 --- a/WEBtool/phishpedia_web.py +++ b/WEBtool/phishpedia_web.py @@ -45,7 +45,6 @@ def upload_file(): file_path = os.path.normpath(file_path) if not file_path.startswith(app.config['UPLOAD_FOLDER']): return jsonify({'error': 'Invalid file path'}), 400 - file.save(file_path) return jsonify({'success': True, 'imageUrl': f'/uploads/{filename}'}), 200 @@ -71,6 +70,8 @@ def delete_image(): filename = image_url.split('/')[-1] image_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) image_path = os.path.normpath(image_path) + if not image_path.startswith(app.config['UPLOAD_FOLDER']): + return jsonify({'success': False, 'error': 'Invalid file path'}), 400 os.remove(image_path) return jsonify({'success': True}), 200 except Exception: @@ -86,8 +87,10 @@ def detect(): filename = imageUrl.split('/')[-1] screenshot_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) screenshot_path = os.path.normpath(screenshot_path) + if not screenshot_path.startswith(app.config['UPLOAD_FOLDER']): + return jsonify({'success': False, 'error': 'Invalid file path'}), 400 - phish_category, pred_target, matched_domain, plotvis, siamese_conf, pred_boxes, logo_recog_time, logo_match_time = phishpedia_cls.test_orig_phishpedia( + phish_category, pred_target, matched_domain, plotvis, siamese_conf, _, logo_recog_time, logo_match_time = phishpedia_cls.test_orig_phishpedia( url, screenshot_path, None) # 处理检测结果 @@ -124,6 +127,8 @@ def build_file_tree(path): for entry in os.listdir(path): entry_path = os.path.join(path, entry) entry_path = os.path.normpath(entry_path) + if not entry_path.startswith(path): + continue if os.path.isdir(entry_path): tree.append({ 'name': entry, @@ -154,6 +159,8 @@ def view_file(): file_name = request.args.get('file') file_path = os.path.join(app.config['FILE_TREE_ROOT'], file_name) file_path = os.path.normpath(file_path) + if not file_path.startswith(app.config['FILE_TREE_ROOT']): + return jsonify({'error': 'Invalid file path'}), 400 if not os.path.exists(file_path): return jsonify({'error': 'File not found'}), 404 @@ -180,12 +187,16 @@ def add_logo(): directory_path = os.path.join(app.config['FILE_TREE_ROOT'], directory) directory_path = os.path.normpath(directory_path) + if not directory_path.startswith(app.config['FILE_TREE_ROOT']): + return jsonify({'success': False, 'error': 'Invalid directory path'}), 400 if not os.path.exists(directory_path): return jsonify({'success': False, 'error': 'Directory does not exist'}), 400 file_path = os.path.join(directory_path, logo.filename) file_path = os.path.normpath(file_path) + if not file_path.startswith(directory_path): + return jsonify({'success': False, 'error': 'Invalid file path'}), 400 logo.save(file_path) return jsonify({'success': True, 'message': 'Logo added successfully'}), 200 @@ -202,8 +213,12 @@ def del_logo(): directory_path = os.path.join(app.config['FILE_TREE_ROOT'], directory) directory_path = os.path.normpath(directory_path) + if not directory_path.startswith(app.config['FILE_TREE_ROOT']): + return jsonify({'success': False, 'error': 'Invalid directory path'}), 400 file_path = os.path.join(directory_path, filename) file_path = os.path.normpath(file_path) + if not file_path.startswith(directory_path): + return jsonify({'success': False, 'error': 'Invalid file path'}), 400 if not os.path.exists(file_path): return jsonify({'success': False, 'error': 'File does not exist'}), 400 @@ -226,6 +241,8 @@ def add_brand(): # 创建品牌目录 brand_directory_path = os.path.join(app.config['FILE_TREE_ROOT'], brand_name) brand_directory_path = os.path.normpath(brand_directory_path) + if not brand_directory_path.startswith(app.config['FILE_TREE_ROOT']): + return jsonify({'success': False, 'error': 'Invalid brand directory path'}), 400 if os.path.exists(brand_directory_path): return jsonify({'success': False, 'error': 'Brand already exists'}), 400 @@ -247,6 +264,8 @@ def del_brand(): directory_path = os.path.join(app.config['FILE_TREE_ROOT'], directory) directory_path = os.path.normpath(directory_path) + if not directory_path.startswith(app.config['FILE_TREE_ROOT']): + return jsonify({'success': False, 'error': 'Invalid directory path'}), 400 if not os.path.exists(directory_path): return jsonify({'success': False, 'error': 'Directory does not exist'}), 400 From 8b36ff886b2d7651f6db4d54ffc468c10dc20357 Mon Sep 17 00:00:00 2001 From: lindsey98 <51521323+lindsey98@users.noreply.github.com> Date: Sun, 29 Dec 2024 08:54:40 +0800 Subject: [PATCH 35/42] Update README.md --- README.md | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 34b45dc..f465c4f 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ - Existing reference-based phishing detectors: - :x: Lack of **interpretability**, only give binary decision (legit or phish) - :x: **Not robust against distribution shift**, because the classifier is biased towards the phishing training set - - :x: Lack of a large-scale phishing benchmark dataset + - :x: **Lack of a large-scale phishing benchmark** dataset - The contributions of our paper: - :white_check_mark: We propose a phishing identification system Phishpedia, which has high identification accuracy and low runtime overhead, outperforming the relevant state-of-the-art identification approaches. @@ -59,30 +59,26 @@ ``` ## Instructions -Requirements: -- Anaconda installed, please refer to the official installation guide: https://docs.anaconda.com/free/anaconda/install/index.html -1. Create a local clone of Phishpedia -```bash -git clone https://github.com/lindsey98/Phishpedia.git -``` +Prerequisite: [Anaconda installed](https://docs.anaconda.com/free/anaconda/install/index.html) -2. Setup the phishpedia conda environment. +

+ Running Inference from the Command Line +Step 1. Create a local clone of Phishpedia, and setup the phishpedia conda environment. In this step, we would be installing the core dependencies of Phishpedia such as pytorch, and detectron2. In addition, we would also download the model checkpoints and brand reference list. This step may take some time. ```bash +git clone https://github.com/lindsey98/Phishpedia.git chmod +x ./setup.sh -export ENV_NAME="phishpedia" ./setup.sh ``` - -3. +Step 2. Activate conda environment _phishpedia_: ```bash conda activate phishpedia ``` -4. Run in bash +Step 3. Run in bash ```bash python phishpedia.py --folder ``` @@ -98,7 +94,23 @@ test_site_2 |__ shot.png (Save the screenshot) ...... ``` - +
+ +
+ Running Phishpedia as a GUI tool (PyQt5-based) + Refer to (GUItool/)[GUItool/] +
+ +
+ Running Phishpedia as a GUI tool (web-browser-based) + Refer to (WEBtool/)[WEBtool/] +
+ +
+ Running Phishpedia as a Chrome plugin + Refer to (Plugin_for_Chrome/)[Plugin_for_Chrome/] +
+ ## Miscellaneous - In our paper, we also implement several phishing detection and identification baselines, see [here](https://github.com/lindsey98/PhishingBaseline) - The logo targetlist described in our paper includes 181 brands, we have further expanded the targetlist to include 277 brands in this code repository From efe5a792684507b0c24db171176632812bbbdc82 Mon Sep 17 00:00:00 2001 From: lindsey98 <51521323+lindsey98@users.noreply.github.com> Date: Sun, 29 Dec 2024 08:59:13 +0800 Subject: [PATCH 36/42] Update README.md --- README.md | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index f465c4f..893631c 100644 --- a/README.md +++ b/README.md @@ -64,21 +64,24 @@ Prerequisite: [Anaconda installed](https://docs.anaconda.com/free/anaconda/insta
Running Inference from the Command Line -Step 1. Create a local clone of Phishpedia, and setup the phishpedia conda environment. -In this step, we would be installing the core dependencies of Phishpedia such as pytorch, and detectron2. + +- Step 1. Create a local clone of Phishpedia, and setup the phishpedia conda environment. +In this step, we would be installing the core dependencies of Phishpedia such as pytorch, and detectron2. In addition, we would also download the model checkpoints and brand reference list. This step may take some time. ```bash git clone https://github.com/lindsey98/Phishpedia.git +cd Phishpedia chmod +x ./setup.sh ./setup.sh ``` -Step 2. Activate conda environment _phishpedia_: + +- Step 2. Activate conda environment _phishpedia_: ```bash conda activate phishpedia ``` -Step 3. Run in bash +- Step 3. Run in bash ```bash python phishpedia.py --folder ``` @@ -98,17 +101,20 @@ test_site_2
Running Phishpedia as a GUI tool (PyQt5-based) - Refer to (GUItool/)[GUItool/] + + Refer to [GUItool/](GUItool/])
Running Phishpedia as a GUI tool (web-browser-based) - Refer to (WEBtool/)[WEBtool/] + + Refer to [WEBtool/](WEBtool/)
Running Phishpedia as a Chrome plugin - Refer to (Plugin_for_Chrome/)[Plugin_for_Chrome/] + + Refer to [Plugin_for_Chrome/](Plugin_for_Chrome/)
## Miscellaneous From 1370e7ef40736e5bc55f31725cc951d42bfb2e53 Mon Sep 17 00:00:00 2001 From: lindsey98 <51521323+lindsey98@users.noreply.github.com> Date: Sun, 29 Dec 2024 08:59:52 +0800 Subject: [PATCH 37/42] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 893631c..aa74ea4 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ test_site_2
Running Phishpedia as a GUI tool (PyQt5-based) - Refer to [GUItool/](GUItool/]) + Refer to [GUItool/](GUItool/)
From c461e2d650270fc195472c02d98ba1995171c1f1 Mon Sep 17 00:00:00 2001 From: lindsey98 <51521323+lindsey98@users.noreply.github.com> Date: Sun, 29 Dec 2024 09:04:34 +0800 Subject: [PATCH 38/42] Update README.md --- Plugin_for_Chrome/README.md | 56 +++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/Plugin_for_Chrome/README.md b/Plugin_for_Chrome/README.md index 806e94e..74b5cea 100644 --- a/Plugin_for_Chrome/README.md +++ b/Plugin_for_Chrome/README.md @@ -1,5 +1,61 @@ # Plugin_for_Chrome +## Project Overview + +`Plugin_for_Chrome` is a Chrome extension project designed to detect phishing websites. The extension automatically retrieves the current webpage's URL and a screenshot when the user presses a predefined hotkey or clicks the extension button, then sends this information to the server for phishing detection. The server utilizes the Flask framework, loads the Phishpedia model for identification, and returns the detection results. + +## Directory Structure + +``` +Plugin_for_Chrome/ +├── client/ +│ ├── background.js # Handles the extension's background logic, including hotkeys and button click events. +│ ├── manifest.json # Configuration file for the Chrome extension. +│ └── popup/ +│ ├── popup.html # HTML file for the extension's popup page. +│ ├── popup.js # JavaScript file for the extension's popup page. +│ └── popup.css # CSS file for the extension's popup page. +└── server/ + └── app.py # Main program for the Flask server, handling client requests and invoking the Phishpedia model for detection. +``` + +## Installation and Usage + +### Frontend + +1. Open the Chrome browser and navigate to `chrome://extensions/`. +2. Enable Developer Mode. +3. Click on "Load unpacked" and select the `Plugin_for_Chrome` directory. + +### Backend + +1. Navigate to the `server` directory: + ```sh + cd Plugin_for_Chrome/server + ``` +2. Install the required dependencies: + ```sh + pip install flask flask_cors + ``` +3. Run the Flask server: + ```sh + python app.py + ``` +## Using the Extension + +In the Chrome browser, press the hotkey `Ctrl+Shift+H` or click the extension button. +The extension will automatically capture the current webpage's URL and a screenshot, then send them to the server for analysis. +The server will return the detection results, and the extension will display whether the webpage is a phishing site along with the corresponding legitimate website. + +## Notes + +Ensure that the server is running locally and listening on the default port 5000. +The extension and the server must operate within the same network environment. + +## Contributing + +Feel free to submit issues and contribute code! + ## 项目简介 `Plugin_for_Chrome` 是一个用于检测钓鱼网站的Chrome插件项目。该插件可以在用户按下设置好的快捷键或者点击插件按钮后,自动获取当前网页的网址以及截图,并将其发送到服务端进行钓鱼网站检测。服务端使用Flask框架,加载Phishpedia模型进行识别,并返回检测结果。 From ceae60efe64bf2d7c2f9ed81d8a76e5851138005 Mon Sep 17 00:00:00 2001 From: honeyxu1108 Date: Sun, 29 Dec 2024 22:27:29 +0800 Subject: [PATCH 39/42] finally fix some hard-code problem --- logo_matching.py | 42 +++++++++++++++++++++++++++--------------- logo_recog.py | 15 +++++++++++---- setup.sh | 1 + 3 files changed, 39 insertions(+), 19 deletions(-) diff --git a/logo_matching.py b/logo_matching.py index 9d1bdf1..755b399 100644 --- a/logo_matching.py +++ b/logo_matching.py @@ -88,24 +88,29 @@ def cache_reference_list(model, targetlist_path: str, grayscale=False): :return file_name_list: targetlist paths ''' - # Prediction for targetlists + # Prediction for targetlists logo_feat_list = [] file_name_list = [] - for target in tqdm(os.listdir(targetlist_path)): + target_list = os.listdir(targetlist_path) + for target in tqdm(target_list): if target.startswith('.'): # skip hidden files continue - for logo_path in os.listdir(os.path.join(targetlist_path, target)): - if logo_path.endswith('.png') or logo_path.endswith('.jpeg') or logo_path.endswith( - '.jpg') or logo_path.endswith('.PNG') \ - or logo_path.endswith('.JPG') or logo_path.endswith('.JPEG'): - if logo_path.startswith('loginpage') or logo_path.startswith('homepage'): # skip homepage/loginpage + logo_list = os.listdir(os.path.join(targetlist_path, target)) + for logo_path in logo_list: + # List of valid image extensions + valid_extensions = ['.png', '.jpeg', '.jpg', 'PNG','.JPG', '.JPEG'] + if any(logo_path.endswith(ext) for ext in valid_extensions): + skip_prefixes = ['loginpage', 'homepage'] + if any(logo_path.startswith(prefix) for prefix in skip_prefixes): # skip homepage/loginpage + continue + try: + logo_feat_list.append(get_embedding(img=os.path.join(targetlist_path, target, logo_path), + model=model, grayscale=grayscale)) + file_name_list.append(str(os.path.join(targetlist_path, target, logo_path))) + except OSError: + print(f"Error opening image: {os.path.join(targetlist_path, target, logo_path)}") continue - logo_feat_list.append(get_embedding(img=os.path.join(targetlist_path, target, logo_path), - model=model, grayscale=grayscale)) - file_name_list.append(str(os.path.join(targetlist_path, target, logo_path))) - - return np.asarray(logo_feat_list), np.asarray(file_name_list) @torch.no_grad() @@ -133,9 +138,16 @@ def get_embedding(img, model, grayscale=False): ## Resize the image while keeping the original aspect ratio pad_color = 255 if grayscale else (255, 255, 255) - img = ImageOps.expand(img, ( - (max(img.size) - img.size[0]) // 2, (max(img.size) - img.size[1]) // 2, - (max(img.size) - img.size[0]) // 2, (max(img.size) - img.size[1]) // 2), fill=pad_color) + img = ImageOps.expand( + img, + ( + (max(img.size) - img.size[0]) // 2, + (max(img.size) - img.size[1]) // 2, + (max(img.size) - img.size[0]) // 2, + (max(img.size) - img.size[1]) // 2 + ), + fill=pad_color + ) img = img.resize((img_size, img_size)) diff --git a/logo_recog.py b/logo_recog.py index 0297995..bd35fff 100644 --- a/logo_recog.py +++ b/logo_recog.py @@ -24,8 +24,8 @@ def pred_rcnn(im, predictor): outputs = predictor(im) instances = outputs['instances'] - pred_classes = instances.pred_classes # tensor - pred_boxes = instances.pred_boxes # Boxes object + pred_classes = instances.pred_classes # tensor + pred_boxes = instances.pred_boxes # Boxes object logo_boxes = pred_boxes[pred_classes == 1].tensor @@ -52,6 +52,13 @@ def config_rcnn(cfg_path, weights_path, conf_threshold): predictor = DefaultPredictor(cfg) return predictor +COLORS = { + 0: (255, 255, 0), # logo + 1: (36, 255, 12), # input + 2: (0, 255, 255), # button + 3: (0, 0, 255), # label + 4: (255, 0, 0) # block +} def vis(img_path, pred_boxes): ''' @@ -71,8 +78,8 @@ def vis(img_path, pred_boxes): # draw rectangle for j, box in enumerate(pred_boxes): if j == 0: - cv2.rectangle(check, (int(box[0]), int(box[1])), (int(box[2]), int(box[3])), (255, 255, 0), 2) + cv2.rectangle(check, (int(box[0]), int(box[1])), (int(box[2]), int(box[3])), COLORS['0'], 2) else: - cv2.rectangle(check, (int(box[0]), int(box[1])), (int(box[2]), int(box[3])), (36, 255, 12), 2) + cv2.rectangle(check, (int(box[0]), int(box[1])), (int(box[2]), int(box[3])), COLORS['1'], 2) return check diff --git a/setup.sh b/setup.sh index 1957d72..0fdd141 100755 --- a/setup.sh +++ b/setup.sh @@ -6,6 +6,7 @@ set -e # Function to display error messages and exit error_exit() { echo "$1" >&2 + echo "$(date): $1" >> error.log # Log error to a file for debugging exit 1 } From 3df95fbcefd1a7848e0eea0ffcabfd276a6cddcf Mon Sep 17 00:00:00 2001 From: honeyxu1108 Date: Mon, 30 Dec 2024 10:06:11 +0800 Subject: [PATCH 40/42] fix pylint error --- logo_matching.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/logo_matching.py b/logo_matching.py index 755b399..41dd1fd 100644 --- a/logo_matching.py +++ b/logo_matching.py @@ -99,7 +99,7 @@ def cache_reference_list(model, targetlist_path: str, grayscale=False): logo_list = os.listdir(os.path.join(targetlist_path, target)) for logo_path in logo_list: # List of valid image extensions - valid_extensions = ['.png', '.jpeg', '.jpg', 'PNG','.JPG', '.JPEG'] + valid_extensions = ['.png', 'PNG', '.jpeg', '.jpg', '.JPG', '.JPEG'] if any(logo_path.endswith(ext) for ext in valid_extensions): skip_prefixes = ['loginpage', 'homepage'] if any(logo_path.startswith(prefix) for prefix in skip_prefixes): # skip homepage/loginpage @@ -185,17 +185,17 @@ def pred_brand(model, domain_map, logo_feat_list, file_name_list, shot_path: str print('Screenshot cannot be open') return None, None, None - ## get predicted box --> crop from screenshot + # get predicted box --> crop from screenshot cropped = img.crop((gt_bbox[0], gt_bbox[1], gt_bbox[2], gt_bbox[3])) img_feat = get_embedding(cropped, model, grayscale=grayscale) - ## get cosine similarity with every protected logo + # get cosine similarity with every protected logo sim_list = logo_feat_list @ img_feat.T # take dot product for every pair of embeddings (Cosine Similarity) pred_brand_list = file_name_list assert len(sim_list) == len(pred_brand_list) - ## get top 3 brands + # get top 3 brands idx = np.argsort(sim_list)[::-1][:3] pred_brand_list = np.array(pred_brand_list)[idx] sim_list = np.array(sim_list)[idx] From 2958a60932f4efadd94fa3465cb99605451d7c13 Mon Sep 17 00:00:00 2001 From: honeyxu1108 Date: Mon, 30 Dec 2024 10:08:47 +0800 Subject: [PATCH 41/42] fix pylint error --- logo_recog.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/logo_recog.py b/logo_recog.py index bd35fff..c1f9251 100644 --- a/logo_recog.py +++ b/logo_recog.py @@ -52,14 +52,16 @@ def config_rcnn(cfg_path, weights_path, conf_threshold): predictor = DefaultPredictor(cfg) return predictor + COLORS = { - 0: (255, 255, 0), # logo - 1: (36, 255, 12), # input - 2: (0, 255, 255), # button - 3: (0, 0, 255), # label - 4: (255, 0, 0) # block + 0: (255, 255, 0), # logo + 1: (36, 255, 12), # input + 2: (0, 255, 255), # button + 3: (0, 0, 255), # label + 4: (255, 0, 0) # block } + def vis(img_path, pred_boxes): ''' Visualize rcnn predictions From 1094b4f5f32a8c1c2681a0c70fead03037b83ca9 Mon Sep 17 00:00:00 2001 From: honeyxu1108 Date: Mon, 30 Dec 2024 10:10:57 +0800 Subject: [PATCH 42/42] fix pylint error --- logo_matching.py | 6 +++--- logo_recog.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/logo_matching.py b/logo_matching.py index 41dd1fd..807f4ed 100644 --- a/logo_matching.py +++ b/logo_matching.py @@ -208,17 +208,17 @@ def pred_brand(model, domain_map, logo_feat_list, file_name_list, shot_path: str for j in range(3): predicted_brand, predicted_domain = None, None - ## If we are trying those lower rank logo, the predicted brand of them should be the same as top1 logo, otherwise might be false positive + # If we are trying those lower rank logo, the predicted brand of them should be the same as top1 logo, otherwise might be false positive if top3_brandlist[j] != top3_brandlist[0]: continue - ## If the largest similarity exceeds threshold + # If the largest similarity exceeds threshold if top3_simlist[j] >= similarity_threshold: predicted_brand = top3_brandlist[j] predicted_domain = top3_domainlist[j] final_sim = top3_simlist[j] - ## Else if not exceed, try resolution alignment, see if can improve + # Else if not exceed, try resolution alignment, see if can improve elif do_resolution_alignment: orig_candidate_logo = Image.open(pred_brand_list[j]) cropped, candidate_logo = resolution_alignment(cropped, orig_candidate_logo) diff --git a/logo_recog.py b/logo_recog.py index c1f9251..42e132e 100644 --- a/logo_recog.py +++ b/logo_recog.py @@ -24,8 +24,8 @@ def pred_rcnn(im, predictor): outputs = predictor(im) instances = outputs['instances'] - pred_classes = instances.pred_classes # tensor - pred_boxes = instances.pred_boxes # Boxes object + pred_classes = instances.pred_classes # tensor + pred_boxes = instances.pred_boxes # Boxes object logo_boxes = pred_boxes[pred_classes == 1].tensor