Repository: lindsey98/Phishpedia
Branch: main
Commit: d030779aad0a
Files: 34
Total size: 130.9 KB
Directory structure:
gitextract_38x9q4gh/
├── .github/
│ └── workflows/
│ ├── codeql.yml
│ ├── lint.yml
│ └── pytest.yml
├── .gitignore
├── LICENSE
├── Plugin_for_Chrome/
│ ├── README.md
│ ├── client/
│ │ ├── background.js
│ │ ├── manifest.json
│ │ └── popup/
│ │ ├── popup.css
│ │ ├── popup.html
│ │ └── popup.js
│ └── server/
│ └── app.py
├── README.md
├── WEBtool/
│ ├── app.py
│ ├── phishpedia_web.py
│ ├── readme.md
│ ├── static/
│ │ ├── css/
│ │ │ ├── sidebar.css
│ │ │ └── style.css
│ │ └── js/
│ │ ├── main.js
│ │ └── sidebar.js
│ ├── templates/
│ │ └── index.html
│ └── utils_web.py
├── configs.py
├── configs.yaml
├── datasets/
│ └── test_sites/
│ └── accounts.g.cdcde.com/
│ ├── html.txt
│ └── info.txt
├── logo_matching.py
├── logo_recog.py
├── models.py
├── phishpedia.py
├── pixi.toml
├── setup.bat
├── setup.sh
└── utils.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/codeql.yml
================================================
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL Advanced"
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
schedule:
- cron: '22 9 * * 2'
jobs:
analyze:
name: Analyze (${{ matrix.language }})
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: python
build-mode: none
# CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step
# to build your code.
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- if: matrix.build-mode == 'manual'
shell: bash
run: |
echo 'If you are using a "manual" build mode for one or more of the' \
'languages you are analyzing, replace this with the commands to build' \
'your code, for example:'
echo ' make bootstrap'
echo ' make release'
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"
================================================
FILE: .github/workflows/lint.yml
================================================
name: flake8 Lint
on: [push, pull_request]
jobs:
flake8-lint:
runs-on: ubuntu-latest
name: Lint
steps:
- name: Check out source repository
uses: actions/checkout@v3
- name: Set up Python environment
uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: flake8 Lint
uses: py-actions/flake8@v2
with:
ignore: "E266,W293,W504,E501"
================================================
FILE: .github/workflows/pytest.yml
================================================
name: Pytest CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
# 第一步:检出代码
- name: Checkout code
uses: actions/checkout@v3
# 第二步:设置 Miniconda
- name: Set up Miniconda
uses: conda-incubator/setup-miniconda@v2
with:
auto-update-conda: true # 自动更新 Conda
python-version: '3.9' # 指定 Python 版
activate-environment: phishpedia
# 保存cache
- name: Cache Conda packages and pip cache
uses: actions/cache@v3
with:
path: |
~/.conda/pkgs # 缓存 Conda 包
~/.cache/pip # 缓存 pip 包
phishpedia/lib/python3.9/site-packages # 可选:缓存虚拟环境的 site-packages
key: ${{ runner.os }}-conda-${{ hashFiles('**/environment.yml', '**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-conda-
# 第三步:升级 pip
- name: Upgrade pip
run: |
python -m pip install --upgrade pip
# 第四步:克隆 Phishpedia 仓库并运行 setup.sh
- name: Clone Phishpedia repo and run setup.sh
run: |
git clone https://github.com/lindsey98/Phishpedia.git
cd Phishpedia
chmod +x ./setup.sh
./setup.sh
# 第五步:安装项目依赖和 pytest
- name: Install dependencies and pytest
run: |
conda run -n phishpedia pip install pytest
conda run -n phishpedia pip install validators
# 步骤 6:运行 Pytest 测试
- name: Run Pytest
run: |
conda run -n phishpedia pytest tests/test_logo_matching.py
conda run -n phishpedia pytest tests/test_logo_recog.py
conda run -n phishpedia pytest tests/test_phishpedia.py
================================================
FILE: .gitignore
================================================
*.zip
*.pkl
*.pth*
venv/
__pycache__/
================================================
FILE: LICENSE
================================================
Creative Commons Legal Code
CC0 1.0 Universal
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
HEREUNDER.
Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:
i. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
likeness depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data
in a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation
thereof, including any amended or successor version of such
directive); and
vii. other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.
4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied,
statutory or otherwise, including without limitation warranties of
title, merchantability, fitness for a particular purpose, non
infringement, or the absence of latent or other defects, accuracy, or
the present or absence of errors, whether or not discoverable, all to
the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without
limitation any person's Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the
Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
this CC0 or use of the Work.
================================================
FILE: Plugin_for_Chrome/README.md
================================================
# 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. Run the Flask server:
```bash
pixi run python -m Plugin_for_Chrome.server.app
```
## 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!
================================================
FILE: Plugin_for_Chrome/client/background.js
================================================
// 处理截图和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;
});
================================================
FILE: Plugin_for_Chrome/client/manifest.json
================================================
{
"manifest_version": 3,
"name": "Phishing Detector",
"version": "1.0",
"description": "Detect phishing websites using screenshot and URL analysis",
"permissions": [
"activeTab",
"scripting",
"storage",
"tabs"
],
"host_permissions": [
"http://localhost:5000/*"
],
"action": {
"default_popup": "popup/popup.html"
},
"background": {
"service_worker": "background.js"
},
"commands": {
"_execute_action": {
"suggested_key": {
"default": "Ctrl+Shift+H",
"mac": "Command+Shift+H"
},
"description": "Analyze current page for phishing"
}
}
}
================================================
FILE: Plugin_for_Chrome/client/popup/popup.css
================================================
.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;
}
================================================
FILE: Plugin_for_Chrome/client/popup/popup.html
================================================
Phishing Detector
分析中...
分析结果:
对应的正版网站:
================================================
FILE: Plugin_for_Chrome/client/popup/popup.js
================================================
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.brand;
}
} else {
status.innerHTML = '✓ 这是一个安全的网站';
legitUrl.classList.add('hidden');
}
} else if (message.type === 'error') {
error.classList.remove('hidden');
error.querySelector('.error-message').textContent = message.data;
}
});
});
================================================
FILE: Plugin_for_Chrome/server/app.py
================================================
from flask import Flask, request, jsonify
from flask_cors import CORS
import base64
from io import BytesIO
from PIL import Image
from datetime import datetime
import os
from phishpedia import PhishpediaWrapper, result_file_write
app = Flask(__name__)
CORS(app)
# 在创建应用时初始化模型
with app.app_context():
current_dir = os.path.dirname(os.path.realpath(__file__))
log_dir = os.path.join(current_dir, 'plugin_logs')
os.makedirs(log_dir, exist_ok=True)
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')
# 解码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)
# 添加结果处理逻辑
result = {
"isPhishing": bool(phish_category),
"brand": pred_target if pred_target else "unknown",
"legitUrl": f"https://{matched_domain[0]}" if matched_domain else "unknown",
"confidence": float(siamese_conf) if siamese_conf is not None else 0.0
}
# 记录日志
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 if matched_domain else ["unknown"],
siamese_conf if siamese_conf is not None else 0.0,
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 if matched_domain else ["unknown"],
siamese_conf if siamese_conf is not None else 0.0,
logo_recog_time, logo_match_time)
if os.path.exists(screenshot_path):
os.remove(screenshot_path)
return jsonify(result)
except Exception as e:
print(f"Error in analyze: {str(e)}")
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
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=False)
================================================
FILE: README.md
================================================
# Phishpedia A Hybrid Deep Learning Based Approach to Visually Identify Phishing Webpages
- This is the official implementation of "Phishpedia: A Hybrid Deep Learning Based Approach to Visually Identify Phishing Webpages" USENIX'21 [link to paper](https://www.usenix.org/conference/usenixsecurity21/presentation/lin), [link to our website](https://sites.google.com/view/phishpedia-site/), [link to our dataset](https://drive.google.com/file/d/12ypEMPRQ43zGRqHGut0Esq2z5en0DH4g/view?usp=drive_link).
- 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
- 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.
- :white_check_mark: We are the first to propose to use **consistency-based method** for phishing detection, in place of the traditional classification-based method. We investigate the consistency between the webpage domain and its brand intention. The detected brand intention provides a **visual explanation** for phishing decision.
- :white_check_mark: Phishpedia is **NOT trained on any phishing dataset**, addressing the potential test-time distribution shift problem.
- :white_check_mark: We release a **30k phishing benchmark dataset**, each website is annotated with its URL, HTML, screenshot, and target brand: https://drive.google.com/file/d/12ypEMPRQ43zGRqHGut0Esq2z5en0DH4g/view?usp=drive_link.
- :white_check_mark: We set up a **phishing monitoring system**, investigating emerging domains fed from CertStream, and we have discovered 1,704 real phishing, out of which 1133 are zero-days not reported by industrial antivirus engine (Virustotal).
## Framework
`Input`: A URL and its screenshot `Output`: Phish/Benign, Phishing target
- Step 1: Enter Deep Object Detection Model, get predicted logos and inputs (inputs are not used for later prediction, just for explanation)
- Step 2: Enter Deep Siamese Model
- If Siamese report no target, `Return Benign, None`
- Else Siamese report a target, `Return Phish, Phishing target`
## Setup
Prerequisite: [Pixi installed](https://pixi.sh/latest/)
For Linux/Mac,
```bash
export KMP_DUPLICATE_LIB_OK=TRUE
git clone https://github.com/lindsey98/Phishpedia.git
cd Phishpedia
pixi install
chmod +x setup.sh
./setup.sh
```
For Windows, in PowerShell,
```bash
git clone https://github.com/lindsey98/Phishpedia.git
cd Phishpedia
pixi install
setup.bat
```
## Running Phishpedia from Command Line
```bash
pixi run python phishpedia.py --folder
```
The testing folder should be in the structure of:
```
test_site_1
|__ info.txt (Write the URL)
|__ shot.png (Save the screenshot)
test_site_2
|__ info.txt (Write the URL)
|__ shot.png (Save the screenshot)
......
```
## Running Phishpedia as a GUI tool (web-browser-based)
See [WEBtool/](WEBtool/)
## Install Phishpedia as a Chrome plugin
See [Plugin_for_Chrome/](Plugin_for_Chrome/)
## Project structure
```
- models/
|___ rcnn_bet365.pth
|___ faster_rcnn.yaml
|___ resnetv2_rgb_new.pth.tar
|___ expand_targetlist/
|___ Adobe/
|___ Amazon/
|___ ......
|___ domain_map.pkl
- logo_recog.py: Deep Object Detection Model
- logo_matching.py: Deep Siamese Model
- configs.yaml: Configuration file
- phishpedia.py: Main script
```
## 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
- For the phish discovery experiment, we obtain feed from [Certstream phish_catcher](https://github.com/x0rz/phishing_catcher), we lower the score threshold to be 40 to process more suspicious websites, readers can refer to their repo for details
- We use Scrapy for website crawling
## Citation
If you find our work useful in your research, please consider citing our paper by:
```bibtex
@inproceedings{lin2021phishpedia,
title={Phishpedia: A Hybrid Deep Learning Based Approach to Visually Identify Phishing Webpages},
author={Lin, Yun and Liu, Ruofan and Divakaran, Dinil Mon and Ng, Jun Yang and Chan, Qing Zhou and Lu, Yiwen and Si, Yuxuan and Zhang, Fan and Dong, Jin Song},
booktitle={30th $\{$USENIX$\}$ Security Symposium ($\{$USENIX$\}$ Security 21)},
year={2021}
}
```
## Contacts
If you have any issues running our code, you can raise an issue or send an email to liu.ruofan16@u.nus.edu, lin_yun@sjtu.edu.cn, and dcsdjs@nus.edu.sg
================================================
FILE: WEBtool/app.py
================================================
from flask import Flask, request, jsonify
from flask_cors import CORS
import base64
from io import BytesIO
from PIL import Image
from datetime import datetime
import os
from phishpedia import PhishpediaWrapper, result_file_write
app = Flask(__name__)
CORS(app)
# 在创建应用时初始化模型
with app.app_context():
current_dir = os.path.dirname(os.path.realpath(__file__))
log_dir = os.path.join(current_dir, 'plugin_logs')
os.makedirs(log_dir, exist_ok=True)
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')
# 解码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)
# 添加结果处理逻辑
result = {
"isPhishing": bool(phish_category),
"brand": pred_target if pred_target else "unknown",
"legitUrl": f"https://{matched_domain[0]}" if matched_domain else "unknown",
"confidence": float(siamese_conf) if siamese_conf is not None else 0.0
}
# 记录日志
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 if matched_domain else ["unknown"],
siamese_conf if siamese_conf is not None else 0.0,
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 if matched_domain else ["unknown"],
siamese_conf if siamese_conf is not None else 0.0,
logo_recog_time, logo_match_time)
if os.path.exists(screenshot_path):
os.remove(screenshot_path)
return jsonify(result)
except Exception as e:
print(f"Error in analyze: {str(e)}")
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
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=False)
================================================
FILE: WEBtool/phishpedia_web.py
================================================
import os
import shutil
from flask import request, Flask, jsonify, render_template, send_from_directory
from flask_cors import CORS
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
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
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)
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
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)
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:
return jsonify({'success': False}), 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)
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, _, 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)
entry_path = os.path.normpath(entry_path)
if not entry_path.startswith(path):
continue
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)
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
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)
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
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)
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
try:
os.remove(file_path)
return jsonify({'success': True, 'message': 'Logo deleted successfully'}), 200
except Exception:
return jsonify({'success': False}), 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)
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
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:
return jsonify({'success': False}), 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)
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
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:
return jsonify({'success': False}), 500
@app.route('/reload-model', methods=['POST'])
def reload_model():
global phishpedia_cls
try:
load_config(reload_targetlist=True)
# Reinitialize Phishpedia
phishpedia_cls = PhishpediaWrapper()
return jsonify({'success': True, 'message': 'Brand deleted successfully'}), 200
except Exception:
return jsonify({'success': False}), 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)
================================================
FILE: WEBtool/readme.md
================================================
# 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.
## How to Run
Run the following command in the web tool directory:
```bash
pixi run python WEBtool/phishpedia_web.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)

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.

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
```
================================================
FILE: WEBtool/static/css/sidebar.css
================================================
/* 侧边栏样式 */
.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;
}
================================================
FILE: WEBtool/static/css/style.css
================================================
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;
}
================================================
FILE: WEBtool/static/js/main.js
================================================
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!
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!