初始化提交
CyberMatrix 量子安全分析引擎
110
README.md
Normal file
@ -0,0 +1,110 @@
|
||||
# CyberMatrix 量子安全分析引擎
|
||||
|
||||
CyberMatrix 是一个基于 AI 的代码安全分析工具,专注于自动化检测和分析代码中的潜在安全漏洞。采用赛博朋克风格的现代化界面,提供直观的安全分析体验。
|
||||
|
||||

|
||||
|
||||
## 核心功能
|
||||
|
||||
### 1. 深度安全分析
|
||||
- 自动识别代码中的安全风险点
|
||||
- 支持检测 SQL 注入、XSS、CSRF、文件上传漏洞等常见安全问题
|
||||
- 基于 CVSS 评分标准评估风险等级
|
||||
- 提供详细的漏洞位置和描述
|
||||
|
||||

|
||||
|
||||
### 2. Webshell 检测
|
||||
- 智能识别 PHP/JSP/ASP 等 WebShell 特征
|
||||
- 检测内存马和无文件落地木马
|
||||
- 分析可疑的代码执行和文件操作
|
||||
- 识别混淆编码和加密规避技术
|
||||
|
||||

|
||||
|
||||
### 3. 智能分析引擎
|
||||
- 基于大语言模型的智能代码理解
|
||||
- 支持多种编程语言的代码分析
|
||||
- 极低的误报率和高准确度
|
||||
- 持续学习和优化的检测能力
|
||||
|
||||
## 界面特点
|
||||
|
||||
- 赛博朋克风格界面设计
|
||||
- 实时进度展示和分析反馈
|
||||
- 直观的文件树浏览
|
||||
- 智能的颜色标记系统
|
||||
- 流畅的动画效果
|
||||
|
||||
<div align="center">
|
||||
<img src="assets/image-20250222012315255.png" width="45%" alt="常态界面"/>
|
||||
<img src="assets/image-20250222012232207.png" width="45%" alt="扫描状态界面"/>
|
||||
</div>
|
||||
|
||||
## 技术栈
|
||||
|
||||
- JavaFX + JFoenix:现代化 UI 框架
|
||||
- Ollama:本地化 AI 模型
|
||||
- AnimateFX:界面动画效果
|
||||
- Jackson:JSON 处理
|
||||
- OkHttp:网络请求
|
||||
|
||||
## 使用方法
|
||||
|
||||
1. 点击"初始化量子扫描目标"选择要分析的代码目录
|
||||
2. 选择分析模式(深度安全分析/Webshell检测)
|
||||
3. 可选择是否包含静态资源分析
|
||||
4. 点击"启动量子安全分析"开始扫描
|
||||
5. 实时查看分析结果和进度
|
||||
|
||||
<div align="center">
|
||||
<table>
|
||||
<tr>
|
||||
<td><img src="assets/image-20250222012445385.png" alt="选择目标"/></td>
|
||||
<td><img src="assets/image-20250222012838474.png" alt="选择模式"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1. 选择扫描目标</td>
|
||||
<td>2. 选择分析模式</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="assets/image-20250222012916552.png" alt="开始扫描"/></td>
|
||||
<td><img src="assets/image-20250222012232207.png" alt="查看结果"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>3. 启动扫描</td>
|
||||
<td>4. 查看分析结果</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
## 配置要求
|
||||
|
||||
- Java 8 或更高版本
|
||||
- Ollama 本地服务
|
||||
- 建议系统内存 8GB 以上
|
||||
|
||||
## 安装说明
|
||||
|
||||
1. 确保已安装 Java 8 运行环境
|
||||
2. 下载并安装 Ollama
|
||||
3. 配置 config.yaml 文件
|
||||
4. 运行启动脚本
|
||||
|
||||
## 开发环境搭建
|
||||
|
||||
1. Clone 项目代码
|
||||
2. 使用 Maven 导入依赖
|
||||
3. 配置 JDK 8
|
||||
4. 运行 CyberScannerApp 主类
|
||||
|
||||
## 参考项目
|
||||
|
||||
本项目受以下开源项目启发:
|
||||
- [DeepSeekSelfTool](https://github.com/ChinaRan0/DeepSeekSelfTool) - 基于 DeepSeek 的代码安全分析工具
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
BIN
assets/image-20250222011558648.png
Normal file
|
After Width: | Height: | Size: 179 KiB |
BIN
assets/image-20250222012127096.png
Normal file
|
After Width: | Height: | Size: 598 KiB |
BIN
assets/image-20250222012232207.png
Normal file
|
After Width: | Height: | Size: 571 KiB |
BIN
assets/image-20250222012315255.png
Normal file
|
After Width: | Height: | Size: 176 KiB |
BIN
assets/image-20250222012445385.png
Normal file
|
After Width: | Height: | Size: 199 KiB |
BIN
assets/image-20250222012838474.png
Normal file
|
After Width: | Height: | Size: 256 KiB |
BIN
assets/image-20250222012916552.png
Normal file
|
After Width: | Height: | Size: 238 KiB |
BIN
assets/image-20250222014422442.png
Normal file
|
After Width: | Height: | Size: 425 KiB |
53
dependency-reduced-pom.xml
Normal file
@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.cyberscanner</groupId>
|
||||
<artifactId>cyber-scanner</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.8.1</version>
|
||||
<configuration>
|
||||
<source>1.8</source>
|
||||
<target>1.8</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.2.4</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<transformers>
|
||||
<transformer>
|
||||
<mainClass>com.cyberscanner.CyberScannerApp</mainClass>
|
||||
</transformer>
|
||||
</transformers>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.itextpdf</groupId>
|
||||
<artifactId>itext7-core</artifactId>
|
||||
<version>7.2.5</version>
|
||||
<type>pom</type>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<properties>
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<javafx.version>8.0.202</javafx.version>
|
||||
<maven.compiler.target>1.8</maven.compiler.target>
|
||||
</properties>
|
||||
</project>
|
||||
109
pom.xml
Normal file
@ -0,0 +1,109 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.cyberscanner</groupId>
|
||||
<artifactId>cyber-scanner</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
<maven.compiler.target>1.8</maven.compiler.target>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.jfoenix</groupId>
|
||||
<artifactId>jfoenix</artifactId>
|
||||
<version>8.0.10</version>
|
||||
</dependency>
|
||||
|
||||
<!-- YAML 处理 -->
|
||||
<dependency>
|
||||
<groupId>org.yaml</groupId>
|
||||
<artifactId>snakeyaml</artifactId>
|
||||
<version>1.33</version>
|
||||
</dependency>
|
||||
|
||||
<!-- HTTP 客户端 -->
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
<version>3.14.9</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JSON 处理 -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>2.12.7.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 动画效果库 -->
|
||||
<dependency>
|
||||
<groupId>io.github.typhon0</groupId>
|
||||
<artifactId>AnimateFX</artifactId>
|
||||
<version>1.2.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 日志框架 -->
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<version>1.2.9</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.8.1</version>
|
||||
<configuration>
|
||||
<source>1.8</source>
|
||||
<target>1.8</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.2.4</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/*.SF</exclude>
|
||||
<exclude>META-INF/*.DSA</exclude>
|
||||
<exclude>META-INF/*.RSA</exclude>
|
||||
<exclude>config.yaml</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
<transformers>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<mainClass>com.cyberscanner.CyberScannerApp</mainClass>
|
||||
</transformer>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
|
||||
<resource>META-INF/services/javax.annotation.processing.Processor</resource>
|
||||
</transformer>
|
||||
</transformers>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@ -0,0 +1,7 @@
|
||||
package com.cyberscanner;
|
||||
|
||||
import javafx.scene.paint.Color;
|
||||
|
||||
public interface ColoredMessageCallback {
|
||||
void updateMessage(String message, Color color);
|
||||
}
|
||||
348
src/main/java/com/cyberscanner/CyberScannerApp.java
Normal file
@ -0,0 +1,348 @@
|
||||
package com.cyberscanner;
|
||||
|
||||
import animatefx.animation.Bounce;
|
||||
import animatefx.animation.FadeIn;
|
||||
import animatefx.animation.Pulse;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.scene.effect.DropShadow;
|
||||
import javafx.scene.effect.Glow;
|
||||
import javafx.util.Duration;
|
||||
import com.jfoenix.controls.*;
|
||||
import javafx.application.Application;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.DirectoryChooser;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
public class CyberScannerApp extends Application {
|
||||
private ScanTask currentTask;
|
||||
|
||||
private JFXTextArea resultDisplay;
|
||||
private JFXButton scanButton;
|
||||
private JFXButton selectButton;
|
||||
private Label pathLabel;
|
||||
private TreeView<File> fileTree;
|
||||
private JFXRadioButton auditRadio;
|
||||
private JFXRadioButton webshellRadio;
|
||||
private JFXCheckBox auditJsCheckbox;
|
||||
private StatusBar statusBar;
|
||||
private JFXProgressBar progressBar;
|
||||
|
||||
@Override
|
||||
public void start(Stage primaryStage) {
|
||||
primaryStage.setOnCloseRequest(event -> {
|
||||
if (currentTask != null) {
|
||||
currentTask.stop();
|
||||
}
|
||||
});
|
||||
primaryStage.setTitle("CyberMatrix - 量子安全分析引擎");
|
||||
primaryStage.setMinWidth(1280);
|
||||
primaryStage.setMinHeight(720);
|
||||
|
||||
// 创建主布局
|
||||
BorderPane mainLayout = new BorderPane();
|
||||
mainLayout.getStyleClass().add("main-layout");
|
||||
mainLayout.setPrefSize(1280, 720);
|
||||
|
||||
// 左侧面板
|
||||
VBox leftPanel = createLeftPanel();
|
||||
leftPanel.setMinWidth(300);
|
||||
leftPanel.setPrefWidth(300);
|
||||
leftPanel.setMaxWidth(400);
|
||||
VBox.setVgrow(fileTree, Priority.ALWAYS);
|
||||
mainLayout.setLeft(leftPanel);
|
||||
|
||||
// 右侧结果显示区
|
||||
resultDisplay = new JFXTextArea();
|
||||
resultDisplay.getStyleClass().add("result-display");
|
||||
resultDisplay.setEditable(false);
|
||||
resultDisplay.setWrapText(true);
|
||||
BorderPane.setMargin(resultDisplay, new Insets(10));
|
||||
VBox.setVgrow(resultDisplay, Priority.ALWAYS);
|
||||
mainLayout.setCenter(resultDisplay);
|
||||
|
||||
// 进度条
|
||||
progressBar = new JFXProgressBar();
|
||||
progressBar.setProgress(0);
|
||||
progressBar.getStyleClass().add("progress-bar");
|
||||
progressBar.setPrefWidth(Double.MAX_VALUE);
|
||||
|
||||
// 状态栏
|
||||
statusBar = new StatusBar();
|
||||
statusBar.getStyleClass().add("status-bar");
|
||||
|
||||
// 将进度条和状态栏放在底部
|
||||
VBox bottomBox = new VBox(5);
|
||||
bottomBox.setPadding(new Insets(5));
|
||||
bottomBox.getChildren().addAll(progressBar, statusBar);
|
||||
mainLayout.setBottom(bottomBox);
|
||||
|
||||
// 场景和样式
|
||||
Scene scene = new Scene(mainLayout);
|
||||
scene.getStylesheets().add(getClass().getResource("/styles/cyber-theme.css").toExternalForm());
|
||||
|
||||
primaryStage.setScene(scene);
|
||||
primaryStage.show();
|
||||
|
||||
// 添加启动动画序列
|
||||
new FadeIn(mainLayout).play();
|
||||
new Pulse(selectButton).play();
|
||||
|
||||
// 为pathLabel添加发光效果动画
|
||||
Glow glow = new Glow(0);
|
||||
pathLabel.setEffect(glow);
|
||||
Timeline glowTimeline = new Timeline(
|
||||
new KeyFrame(Duration.ZERO, new KeyValue(glow.levelProperty(), 0)),
|
||||
new KeyFrame(Duration.seconds(1), new KeyValue(glow.levelProperty(), 0.8)),
|
||||
new KeyFrame(Duration.seconds(2), new KeyValue(glow.levelProperty(), 0))
|
||||
);
|
||||
glowTimeline.setCycleCount(Timeline.INDEFINITE);
|
||||
glowTimeline.play();
|
||||
|
||||
// 添加周期性动画效果
|
||||
Timeline pulseTimeline = new Timeline(
|
||||
new KeyFrame(Duration.seconds(2),
|
||||
new KeyValue(leftPanel.effectProperty(),
|
||||
new DropShadow(10, Color.valueOf("#4d4dff"))))
|
||||
);
|
||||
pulseTimeline.setAutoReverse(true);
|
||||
pulseTimeline.setCycleCount(Timeline.INDEFINITE);
|
||||
pulseTimeline.play();
|
||||
}
|
||||
|
||||
private VBox createLeftPanel() {
|
||||
VBox leftPanel = new VBox(10);
|
||||
leftPanel.getStyleClass().add("left-panel");
|
||||
leftPanel.setPadding(new Insets(10));
|
||||
|
||||
// 目录选择按钮
|
||||
selectButton = new JFXButton("🌐 初始化量子扫描目标");
|
||||
selectButton.getStyleClass().add("cyber-button");
|
||||
selectButton.setOnAction(e -> selectDirectory());
|
||||
|
||||
// 路径显示标签
|
||||
pathLabel = new Label("等待目标初始化...");
|
||||
pathLabel.getStyleClass().add("path-label");
|
||||
|
||||
// 模式选择组
|
||||
VBox modeBox = createModeSelectionBox();
|
||||
|
||||
// 文件树
|
||||
fileTree = new TreeView<>();
|
||||
fileTree.getStyleClass().add("file-tree");
|
||||
VBox.setVgrow(fileTree, Priority.ALWAYS);
|
||||
|
||||
// 扫描按钮
|
||||
scanButton = new JFXButton("⚡ 启动量子安全分析");
|
||||
scanButton.getStyleClass().add("scan-button");
|
||||
scanButton.setDisable(true);
|
||||
scanButton.setOnAction(e -> startScan());
|
||||
|
||||
leftPanel.getChildren().addAll(
|
||||
selectButton,
|
||||
pathLabel,
|
||||
modeBox,
|
||||
auditJsCheckbox,
|
||||
fileTree,
|
||||
scanButton
|
||||
);
|
||||
|
||||
return leftPanel;
|
||||
}
|
||||
|
||||
private VBox createModeSelectionBox() {
|
||||
VBox modeBox = new VBox(5);
|
||||
modeBox.getStyleClass().add("mode-box");
|
||||
|
||||
Label modeLabel = new Label("🔧 分析模式");
|
||||
modeLabel.getStyleClass().add("mode-label");
|
||||
|
||||
ToggleGroup modeGroup = new ToggleGroup();
|
||||
auditRadio = new JFXRadioButton("深度安全分析");
|
||||
webshellRadio = new JFXRadioButton("Webshell检测");
|
||||
|
||||
auditRadio.setToggleGroup(modeGroup);
|
||||
webshellRadio.setToggleGroup(modeGroup);
|
||||
auditRadio.setSelected(true);
|
||||
|
||||
auditJsCheckbox = new JFXCheckBox("包含静态资源分析");
|
||||
auditJsCheckbox.setSelected(true);
|
||||
|
||||
modeBox.getChildren().addAll(modeLabel, auditRadio, webshellRadio);
|
||||
return modeBox;
|
||||
}
|
||||
|
||||
private void selectDirectory() {
|
||||
DirectoryChooser directoryChooser = new DirectoryChooser();
|
||||
directoryChooser.setTitle("选择代码矩阵接入点");
|
||||
File selectedDirectory = directoryChooser.showDialog(null);
|
||||
|
||||
if (selectedDirectory != null) {
|
||||
pathLabel.setText("📂 目标:" + selectedDirectory.getName());
|
||||
updateFileTree(selectedDirectory);
|
||||
scanButton.setDisable(false);
|
||||
statusBar.setStatus("✅ 目标初始化完成");
|
||||
|
||||
// 添加动画效果
|
||||
new Pulse(scanButton).play();
|
||||
}
|
||||
}
|
||||
|
||||
private int totalFileCount = 0;
|
||||
|
||||
private void updateFileTree(File root) {
|
||||
TreeItem<File> rootItem = new TreeItem<>(root);
|
||||
rootItem.setExpanded(true);
|
||||
fileTree.setRoot(rootItem);
|
||||
totalFileCount = 0;
|
||||
populateFileTree(rootItem);
|
||||
|
||||
// 设置单元格工厂来自定义显示
|
||||
fileTree.setCellFactory(tv -> new TreeCell<File>() {
|
||||
@Override
|
||||
protected void updateItem(File item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (empty || item == null) {
|
||||
setText(null);
|
||||
} else {
|
||||
// 只显示文件或目录名,而不是完整路径
|
||||
setText(item.getName());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void populateFileTree(TreeItem<File> item) {
|
||||
File[] files = item.getValue().listFiles();
|
||||
if (files != null) {
|
||||
Arrays.sort(files, (f1, f2) -> {
|
||||
// 目录优先,然后按名称排序
|
||||
if (f1.isDirectory() && !f2.isDirectory()) {
|
||||
return -1;
|
||||
} else if (!f1.isDirectory() && f2.isDirectory()) {
|
||||
return 1;
|
||||
} else {
|
||||
return f1.getName().compareToIgnoreCase(f2.getName());
|
||||
}
|
||||
});
|
||||
|
||||
for (File file : files) {
|
||||
TreeItem<File> fileItem = new TreeItem<>(file);
|
||||
item.getChildren().add(fileItem);
|
||||
if (!file.isDirectory()) {
|
||||
totalFileCount++;
|
||||
}
|
||||
if (file.isDirectory()) {
|
||||
populateFileTree(fileItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void startScan() {
|
||||
if (currentTask != null) {
|
||||
currentTask.stop();
|
||||
}
|
||||
if (fileTree.getRoot() == null) {
|
||||
showAlert("警告", "请先选择代码目录!");
|
||||
return;
|
||||
}
|
||||
|
||||
scanButton.setDisable(true);
|
||||
String initMsg = auditRadio.isSelected() ?
|
||||
"🚀 正在启动深度安全分析引擎..." :
|
||||
"🕵️ 正在启动Webshell检测引擎...";
|
||||
|
||||
resultDisplay.setText(initMsg + "\n" + String.join("", Collections.nCopies(50, "▮")) + "\n");
|
||||
|
||||
// 创建并启动扫描任务
|
||||
currentTask = new ScanTask(
|
||||
fileTree.getRoot().getValue(),
|
||||
auditRadio.isSelected(),
|
||||
auditJsCheckbox.isSelected(),
|
||||
this::updateProgress,
|
||||
this::updateStatus,
|
||||
this::showResults
|
||||
);
|
||||
new Thread(currentTask).start();
|
||||
}
|
||||
|
||||
private void updateProgress(double progress) {
|
||||
javafx.application.Platform.runLater(() -> {
|
||||
progressBar.setProgress(progress);
|
||||
});
|
||||
}
|
||||
|
||||
private void updateStatus(String message, javafx.scene.paint.Color color) {
|
||||
javafx.application.Platform.runLater(() -> {
|
||||
statusBar.setStatus(message);
|
||||
String style = String.format("-fx-text-fill: %s;", color.toString().replace("0x", "#"));
|
||||
Text text = new Text("⚡ " + message + "\n");
|
||||
text.setStyle(style);
|
||||
resultDisplay.appendText(text.getText());
|
||||
resultDisplay.setStyle(style);
|
||||
});
|
||||
}
|
||||
|
||||
private void showResults(String report) {
|
||||
javafx.application.Platform.runLater(() -> {
|
||||
scanButton.setDisable(false);
|
||||
|
||||
if (auditRadio.isSelected()) {
|
||||
String header = "\n📊 深度安全分析完成!统计信息:\n";
|
||||
|
||||
// 解析报告并生成统计信息
|
||||
String[] lines = report.split("\n");
|
||||
int highRiskIssues = 0;
|
||||
int mediumRiskIssues = 0;
|
||||
|
||||
for (String line : lines) {
|
||||
if (line.contains("[高危]")) highRiskIssues++;
|
||||
if (line.contains("[中危]")) mediumRiskIssues++;
|
||||
}
|
||||
|
||||
int totalIssues = highRiskIssues + mediumRiskIssues;
|
||||
StringBuilder statisticsBuilder = new StringBuilder();
|
||||
statisticsBuilder.append(String.format(
|
||||
"总扫描文件数:%d\n" +
|
||||
"高危问题数:%d\n" +
|
||||
"中危问题数:%d\n" +
|
||||
"问题总计:%d\n",
|
||||
totalFileCount, highRiskIssues, mediumRiskIssues, totalIssues
|
||||
));
|
||||
|
||||
resultDisplay.appendText(header + statisticsBuilder.toString());
|
||||
} else {
|
||||
resultDisplay.appendText("\n🎯 Webshell检测完成!");
|
||||
}
|
||||
|
||||
statusBar.setStatus("✅ 扫描完成");
|
||||
|
||||
// 添加完成动画
|
||||
new Bounce(resultDisplay).play();
|
||||
});
|
||||
}
|
||||
|
||||
private void showAlert(String title, String content) {
|
||||
Alert alert = new Alert(Alert.AlertType.WARNING);
|
||||
alert.setTitle(title);
|
||||
alert.setHeaderText(null);
|
||||
alert.setContentText(content);
|
||||
alert.showAndWait();
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
launch(args);
|
||||
}
|
||||
}
|
||||
11
src/main/java/com/cyberscanner/ProgressCallback.java
Normal file
@ -0,0 +1,11 @@
|
||||
package com.cyberscanner;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface ProgressCallback {
|
||||
void updateProgress(double progress);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface MessageCallback {
|
||||
void updateMessage(String message);
|
||||
}
|
||||
301
src/main/java/com/cyberscanner/ScanTask.java
Normal file
@ -0,0 +1,301 @@
|
||||
package com.cyberscanner;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import okhttp3.*;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class ScanTask implements Runnable {
|
||||
private volatile boolean isRunning = true;
|
||||
private final File rootDirectory;
|
||||
private final boolean isAuditMode;
|
||||
private final boolean includeJsFiles;
|
||||
private final ProgressCallback progressCallback;
|
||||
private final ColoredMessageCallback messageCallback;
|
||||
private final Consumer<String> resultCallback;
|
||||
private final OkHttpClient httpClient;
|
||||
private final ObjectMapper objectMapper;
|
||||
private String ollamaHost;
|
||||
private String ollamaModel;
|
||||
|
||||
public ScanTask(File rootDirectory, boolean isAuditMode, boolean includeJsFiles,
|
||||
ProgressCallback progressCallback, ColoredMessageCallback messageCallback, Consumer<String> resultCallback) {
|
||||
this.rootDirectory = rootDirectory;
|
||||
this.isAuditMode = isAuditMode;
|
||||
this.includeJsFiles = includeJsFiles;
|
||||
this.progressCallback = progressCallback;
|
||||
this.messageCallback = messageCallback;
|
||||
this.resultCallback = resultCallback;
|
||||
this.httpClient = new OkHttpClient.Builder()
|
||||
.connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
|
||||
.writeTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
|
||||
.readTimeout(60, java.util.concurrent.TimeUnit.SECONDS)
|
||||
.retryOnConnectionFailure(true)
|
||||
.build();
|
||||
this.objectMapper = new ObjectMapper();
|
||||
loadConfig();
|
||||
}
|
||||
|
||||
private void loadConfig() {
|
||||
try {
|
||||
// 首先尝试从jar包同级目录读取config.yaml
|
||||
File externalConfig = new File(new File(getClass().getProtectionDomain()
|
||||
.getCodeSource().getLocation().toURI()).getParent(), "config.yaml");
|
||||
|
||||
InputStream inputStream;
|
||||
if (externalConfig.exists()) {
|
||||
inputStream = new FileInputStream(externalConfig);
|
||||
} else {
|
||||
// 如果外部配置不存在,尝试从jar包内部读取
|
||||
inputStream = getClass().getResourceAsStream("/config.yaml");
|
||||
if (inputStream == null) {
|
||||
// 如果两个位置都没有配置文件,显示错误对话框
|
||||
javafx.application.Platform.runLater(() -> {
|
||||
javafx.scene.control.Alert alert = new javafx.scene.control.Alert(javafx.scene.control.Alert.AlertType.ERROR);
|
||||
alert.setTitle("配置错误");
|
||||
alert.setHeaderText("找不到配置文件");
|
||||
alert.setContentText(String.format("请确保在程序目录 %s 下存在 config.yaml 配置文件。\n\n" +
|
||||
"配置文件示例内容:\n" +
|
||||
"api:\n" +
|
||||
" ollama:\n" +
|
||||
" url: http://localhost:11434/api\n" +
|
||||
" model: deepseek-coder",
|
||||
externalConfig.getParent()));
|
||||
alert.showAndWait();
|
||||
System.exit(1);
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Yaml yaml = new Yaml();
|
||||
Map<String, Object> config = yaml.load(inputStream);
|
||||
|
||||
Map<String, Object> api = (Map<String, Object>) config.get("api");
|
||||
Map<String, Object> ollama = (Map<String, Object>) api.get("ollama");
|
||||
String ollamaUrl = (String) ollama.get("url");
|
||||
this.ollamaHost = ollamaUrl.split("/api")[0];
|
||||
this.ollamaModel = (String) ollama.get("model");
|
||||
|
||||
inputStream.close();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
// 显示错误对话框
|
||||
javafx.application.Platform.runLater(() -> {
|
||||
javafx.scene.control.Alert alert = new javafx.scene.control.Alert(javafx.scene.control.Alert.AlertType.ERROR);
|
||||
alert.setTitle("配置错误");
|
||||
alert.setHeaderText("配置文件读取失败");
|
||||
alert.setContentText("请检查config.yaml文件格式是否正确。\n\n错误信息:" + e.getMessage());
|
||||
alert.showAndWait();
|
||||
System.exit(1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
isRunning = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Map<String, String> filesContent = scanCodeFiles(rootDirectory);
|
||||
List<String> results = new ArrayList<>();
|
||||
int totalFiles = filesContent.size(); // 使用实际扫描的文件数量
|
||||
int processedFiles = 0;
|
||||
int detectedFiles = 0; // 新增:检测到问题的文件数量
|
||||
|
||||
// 初始化进度条
|
||||
javafx.application.Platform.runLater(() -> {
|
||||
progressCallback.updateProgress(0.0);
|
||||
});
|
||||
|
||||
for (Map.Entry<String, String> entry : filesContent.entrySet()) {
|
||||
if (!isRunning) {
|
||||
messageCallback.updateMessage("⚠️ 扫描任务已中断", javafx.scene.paint.Color.web("#FFB86C"));
|
||||
return;
|
||||
}
|
||||
String filepath = entry.getKey();
|
||||
String content = entry.getValue();
|
||||
|
||||
try {
|
||||
String filename = new File(filepath).getName();
|
||||
messageCallback.updateMessage(
|
||||
isAuditMode ?
|
||||
String.format("🔍 分析 %s... (%d/%d)", filename, processedFiles + 1, totalFiles) :
|
||||
String.format("🕵️ 扫描 %s... (%d/%d)", filename, processedFiles + 1, totalFiles),
|
||||
javafx.scene.paint.Color.DODGERBLUE);
|
||||
|
||||
String prompt = createPrompt(content);
|
||||
String response = callOllamaAPI(prompt);
|
||||
String result = processResponse(response);
|
||||
|
||||
// 如果检测到问题(包含[高危]标记),增加检测文件计数
|
||||
if (!isAuditMode && result.contains("[高危]") && !results.stream().anyMatch(r -> r.contains(filepath))) {
|
||||
detectedFiles++;
|
||||
}
|
||||
|
||||
// 根据扫描结果中的风险等级设置不同的颜色
|
||||
String formattedResult = String.format("%s %s\n%s\n%s",
|
||||
isAuditMode ? "📄" : "📁",
|
||||
filepath,
|
||||
result,
|
||||
String.join("", Collections.nCopies(50, "━")));
|
||||
|
||||
// 根据结果内容设置不同的颜色
|
||||
javafx.scene.paint.Color messageColor;
|
||||
if (result.contains("[高危]")) {
|
||||
messageColor = javafx.scene.paint.Color.web("#FF4444");
|
||||
} else if (result.contains("[中危]")) {
|
||||
messageColor = javafx.scene.paint.Color.web("#FFB86C");
|
||||
} else if (result.contains("[低危]")) {
|
||||
messageColor = javafx.scene.paint.Color.web("#50FA7B");
|
||||
} else {
|
||||
messageColor = javafx.scene.paint.Color.web("#8BE9FD");
|
||||
}
|
||||
|
||||
messageCallback.updateMessage(formattedResult, messageColor);
|
||||
results.add(formattedResult);
|
||||
|
||||
// 更新进度
|
||||
processedFiles++;
|
||||
double progress = (double) processedFiles / totalFiles;
|
||||
javafx.application.Platform.runLater(() -> {
|
||||
progressCallback.updateProgress(progress);
|
||||
});
|
||||
} catch (Exception e) {
|
||||
String errorMessage = String.format("❌ 错误:%s\n%s", filepath, e.getMessage());
|
||||
messageCallback.updateMessage(errorMessage, javafx.scene.paint.Color.web("#6272A4"));
|
||||
results.add(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// Webshell检测模式下不在这里添加统计信息,统计信息由UI层处理
|
||||
resultCallback.accept(String.join("\n", results));
|
||||
}
|
||||
|
||||
private Map<String, String> scanCodeFiles(File directory) {
|
||||
Map<String, String> codeFiles = new HashMap<>();
|
||||
List<String> allowedExt = new ArrayList<>(Arrays.asList(
|
||||
".php", ".jsp", ".jspx", ".asp", ".aspx", ".js", ".html", ".py", ".java"
|
||||
));
|
||||
|
||||
if (!includeJsFiles) {
|
||||
allowedExt.remove(".js");
|
||||
allowedExt.remove(".html");
|
||||
}
|
||||
|
||||
scanDirectory(directory, codeFiles, allowedExt);
|
||||
return codeFiles;
|
||||
}
|
||||
|
||||
private void scanDirectory(File directory, Map<String, String> codeFiles, List<String> allowedExt) {
|
||||
File[] files = directory.listFiles();
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
if (file.isDirectory()) {
|
||||
scanDirectory(file, codeFiles, allowedExt);
|
||||
} else {
|
||||
String extension = getFileExtension(file);
|
||||
if (allowedExt.contains(extension.toLowerCase())) {
|
||||
try {
|
||||
String content = readFile(file);
|
||||
// 使用相对路径存储文件
|
||||
String relativePath = rootDirectory.toPath().relativize(file.toPath()).toString();
|
||||
codeFiles.put(relativePath, content);
|
||||
} catch (IOException e) {
|
||||
String relativePath = rootDirectory.toPath().relativize(file.toPath()).toString();
|
||||
codeFiles.put(relativePath, "无法读取文件内容");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getFileExtension(File file) {
|
||||
String name = file.getName();
|
||||
int lastIndexOf = name.lastIndexOf(".");
|
||||
return lastIndexOf == -1 ? "" : name.substring(lastIndexOf);
|
||||
}
|
||||
|
||||
private String readFile(File file) throws IOException {
|
||||
StringBuilder content = new StringBuilder();
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
content.append(line).append("\n");
|
||||
}
|
||||
}
|
||||
return content.toString();
|
||||
}
|
||||
|
||||
private String createPrompt(String content) {
|
||||
if (isAuditMode) {
|
||||
return String.format("【强制指令】你是一个专业的安全审计AI,请按以下要求分析代码:\n\n" +
|
||||
"1. 漏洞分析流程:\n" +
|
||||
" 1.1 识别潜在风险点(SQL操作、文件操作、用户输入点、文件上传漏洞、CSRF、SSRF、XSS、RCE、OWASP top10等漏洞)\n" +
|
||||
" 1.2 验证漏洞可利用性\n" +
|
||||
" 1.3 按CVSS评分标准评估风险等级\n\n" +
|
||||
"2. 输出规则:\n" +
|
||||
" - 仅输出确认存在的高危/中危漏洞\n" +
|
||||
" - 使用严格格式:[风险等级] 类型 - 位置:行号 - 50字内描述\n" +
|
||||
" - 禁止解释漏洞原理\n" +
|
||||
" - 禁止给出修复建议\n" +
|
||||
" - 如果有可能,给出POC(HTTP请求数据包)\n\n" +
|
||||
"3. 输出示例(除此外不要有任何输出):\n" +
|
||||
" [高危] SQL注入 - user_login.php:32 - 未过滤的$_GET参数直接拼接SQL查询\n" +
|
||||
" [POC]\nPOST /login.php HTTP/1.1\n" +
|
||||
" Host: example.com\n" +
|
||||
" Content-Type: application/x-www-form-urlencoded\n" +
|
||||
" [中危] XSS - comment.jsp:15 - 未转义的userInput输出到HTML\n" +
|
||||
" [POC]\nPOST /login.php HTTP/1.1\n" +
|
||||
" Host: example.com\n" +
|
||||
" Content-Type: application/x-www-form-urlencoded\n\n" +
|
||||
"4. 当前代码(仅限分析):\n%s", content.substring(0, Math.min(content.length(), 3000)));
|
||||
} else {
|
||||
return String.format("【Webshell检测指令】请严格按以下步骤分析代码:\n\n" +
|
||||
"1. 检测要求: \n" +
|
||||
" 请分析以下文件内容是否为WebShell或内存马。要求:\n" +
|
||||
" 1. 检查PHP/JSP/ASP等WebShell特征(如加密函数、执行系统命令、文件操作)\n" +
|
||||
" 2. 识别内存马特征(如无文件落地、进程注入、异常网络连接)\n" +
|
||||
" 3. 分析代码中的可疑功能(如命令执行、文件上传、信息收集)\n" +
|
||||
" 4. 检查混淆编码、加密手段等规避技术\n\n" +
|
||||
"2. 判断规则:\n" +
|
||||
" - 仅当确认恶意性时报告\n" +
|
||||
" - 输出格式:🔴 [高危] Webshell - 文件名:行号 - 检测到[特征1+特征2+...]\n\n" +
|
||||
"3. 输出示例(严格按照此格式输出,不要有任何的补充,如果未检测到危险,则不输出,除此之外,不要有任何输出):\n" +
|
||||
" 🔴 [高危] Webshell - malicious.php:8 - 检测到[system执行+base64解码+错误抑制]\n\n" +
|
||||
"4. 待分析代码:\n%s", content.substring(0, Math.min(content.length(), 3000)));
|
||||
}
|
||||
}
|
||||
|
||||
private String callOllamaAPI(String prompt) throws IOException {
|
||||
MediaType JSON = MediaType.get("application/json; charset=utf-8");
|
||||
Map<String, Object> requestMap = new HashMap<>();
|
||||
requestMap.put("model", ollamaModel);
|
||||
requestMap.put("prompt", prompt);
|
||||
requestMap.put("stream", false);
|
||||
RequestBody body = RequestBody.create(JSON, objectMapper.writeValueAsString(requestMap));
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(ollamaHost + "/api/generate")
|
||||
.post(body)
|
||||
.build();
|
||||
|
||||
try (Response response = httpClient.newCall(request).execute()) {
|
||||
if (!response.isSuccessful()) throw new IOException("请求失败: " + response);
|
||||
JsonNode jsonResponse = objectMapper.readTree(response.body().string());
|
||||
return jsonResponse.get("response").asText();
|
||||
}
|
||||
}
|
||||
|
||||
private String processResponse(String response) {
|
||||
return response.replaceAll("<think>.*?</think>", "");
|
||||
}
|
||||
}
|
||||
97
src/main/java/com/cyberscanner/StatusBar.java
Normal file
@ -0,0 +1,97 @@
|
||||
package com.cyberscanner;
|
||||
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ProgressBar;
|
||||
import javafx.scene.effect.Glow;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.util.Duration;
|
||||
|
||||
public class StatusBar extends HBox {
|
||||
private final Label statusLabel;
|
||||
private final Label progressLabel;
|
||||
private final ProgressBar progressBar;
|
||||
private Timeline progressTimeline;
|
||||
|
||||
public StatusBar() {
|
||||
setSpacing(10);
|
||||
setPadding(new Insets(5));
|
||||
getStyleClass().add("status-bar");
|
||||
|
||||
// 创建状态文本标签
|
||||
statusLabel = new Label("就绪");
|
||||
statusLabel.setFont(Font.font("System", 14));
|
||||
statusLabel.setTextFill(Color.valueOf("#4d4dff"));
|
||||
statusLabel.getStyleClass().add("status-label");
|
||||
statusLabel.setWrapText(true);
|
||||
statusLabel.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
// 添加发光效果
|
||||
Glow glow = new Glow(0.6);
|
||||
statusLabel.setEffect(glow);
|
||||
|
||||
// 创建进度条
|
||||
progressBar = new ProgressBar();
|
||||
progressBar.setProgress(0);
|
||||
progressBar.setPrefWidth(200);
|
||||
progressBar.getStyleClass().add("status-progress");
|
||||
progressBar.setVisible(false);
|
||||
|
||||
// 创建进度百分比标签
|
||||
progressLabel = new Label("0%");
|
||||
progressLabel.setFont(Font.font("System", 12));
|
||||
progressLabel.setTextFill(Color.valueOf("#4d4dff"));
|
||||
progressLabel.setVisible(false);
|
||||
|
||||
// 设置布局
|
||||
getChildren().addAll(statusLabel, progressBar, progressLabel);
|
||||
HBox.setHgrow(statusLabel, Priority.ALWAYS);
|
||||
}
|
||||
|
||||
public void setStatus(String text) {
|
||||
statusLabel.setText(text);
|
||||
}
|
||||
|
||||
public void setProgress(double progress) {
|
||||
// 确保进度条和百分比标签可见
|
||||
if (!progressBar.isVisible()) {
|
||||
progressBar.setVisible(true);
|
||||
progressLabel.setVisible(true);
|
||||
}
|
||||
|
||||
// 取消之前的动画(如果存在)
|
||||
if (progressTimeline != null) {
|
||||
progressTimeline.stop();
|
||||
}
|
||||
|
||||
// 创建平滑动画
|
||||
progressTimeline = new Timeline(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(progressBar.progressProperty(), progressBar.getProgress())),
|
||||
new KeyFrame(Duration.millis(500),
|
||||
new KeyValue(progressBar.progressProperty(), progress))
|
||||
);
|
||||
|
||||
progressTimeline.setOnFinished(event -> {
|
||||
// 更新百分比标签
|
||||
int percentage = (int) (progress * 100);
|
||||
progressLabel.setText(percentage + "%");
|
||||
});
|
||||
|
||||
progressTimeline.play();
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
statusLabel.setText("就绪");
|
||||
progressBar.setProgress(0);
|
||||
progressLabel.setText("0%");
|
||||
progressBar.setVisible(false);
|
||||
progressLabel.setVisible(false);
|
||||
}
|
||||
}
|
||||
42
src/main/resources/config.yaml
Normal file
@ -0,0 +1,42 @@
|
||||
# API配置
|
||||
api:
|
||||
type: ollama # 可选值: "deepseek" 或 "ollama"
|
||||
|
||||
# DeepSeek API配置
|
||||
deepseek:
|
||||
# 官方默认API地址: "https://api.deepseek.com/v1/chat/completions"
|
||||
# 硅基流动:https://api.siliconflow.cn/v1/chat/completions
|
||||
url: ""
|
||||
api_key: ""
|
||||
# DeepSeek模型名称,官方默认模型: "deepseek-chat"
|
||||
# 硅基流动:deepseek-ai/DeepSeek-V3
|
||||
model: ""
|
||||
|
||||
# Ollama API配置
|
||||
ollama:
|
||||
url: "http://x.x.x.x/api/chat" # Ollama API地址
|
||||
model: "qwen2.5:7b" # Ollama模型名称
|
||||
|
||||
# 主题配色方案
|
||||
themes:
|
||||
dark:
|
||||
main_bg: "#1a1a2e"
|
||||
secondary_bg: "#16213e"
|
||||
text_color: "#e4e4e4"
|
||||
accent_color: "#4d4dff"
|
||||
border_color: "#7b2cbf"
|
||||
button_hover: "#00b4d8"
|
||||
button_pressed: "#0096c7"
|
||||
gradient_start: "#2b2b4b"
|
||||
gradient_end: "#1a1a2e"
|
||||
neon_glow: "#4d4dff"
|
||||
highlight: "#7b2cbf"
|
||||
|
||||
light:
|
||||
main_bg: "#f5f5f5"
|
||||
secondary_bg: "#ffffff"
|
||||
text_color: "#333333"
|
||||
accent_color: "#2196f3"
|
||||
border_color: "#e0e0e0"
|
||||
button_hover: "#1976d2"
|
||||
button_pressed: "#1565c0"
|
||||
208
src/main/resources/styles/cyber-theme.css
Normal file
@ -0,0 +1,208 @@
|
||||
.main-layout {
|
||||
-fx-background-color: #282a36;
|
||||
-fx-text-fill: #f8f8f2;
|
||||
}
|
||||
|
||||
.left-panel {
|
||||
-fx-background-color: #44475a;
|
||||
-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.3), 10, 0, 0, 0);
|
||||
-fx-min-width: 300;
|
||||
-fx-pref-width: 300;
|
||||
}
|
||||
|
||||
.cyber-button, .scan-button {
|
||||
-fx-background-color: #6272a4;
|
||||
-fx-text-fill: #f8f8f2;
|
||||
-fx-font-size: 14px;
|
||||
-fx-padding: 10px 20px;
|
||||
-fx-background-radius: 5px;
|
||||
}
|
||||
|
||||
.cyber-button:hover, .scan-button:hover {
|
||||
-fx-background-color: #50fa7b;
|
||||
-fx-text-fill: #282a36;
|
||||
}
|
||||
|
||||
.cyber-button:hover {
|
||||
-fx-background-color: #1a1a2e;
|
||||
-fx-border-color: #00ffff;
|
||||
-fx-effect: dropshadow(gaussian, #00ffff88, 20, 0, 0, 0);
|
||||
}
|
||||
|
||||
.path-label {
|
||||
-fx-text-fill: #cc66ff;
|
||||
-fx-font-size: 10pt;
|
||||
-fx-font-family: 'Consolas';
|
||||
-fx-padding: 5;
|
||||
}
|
||||
|
||||
.mode-box {
|
||||
-fx-spacing: 5;
|
||||
-fx-padding: 10;
|
||||
-fx-border-color: #ff33ff;
|
||||
-fx-border-width: 1;
|
||||
-fx-border-radius: 5;
|
||||
-fx-effect: dropshadow(gaussian, #ff33ff44, 10, 0, 0, 0);
|
||||
}
|
||||
|
||||
.mode-label {
|
||||
-fx-text-fill: #cc66ff;
|
||||
-fx-font-size: 12pt;
|
||||
-fx-font-family: 'Consolas';
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.jfx-radio-button {
|
||||
-fx-text-fill: #cc66ff;
|
||||
-fx-padding: 8;
|
||||
}
|
||||
|
||||
.jfx-radio-button .radio {
|
||||
-fx-background-color: #2b0052;
|
||||
-fx-border-color: #ff33ff;
|
||||
-fx-border-width: 2;
|
||||
}
|
||||
|
||||
.jfx-radio-button:selected .radio .dot {
|
||||
-fx-background-color: #ff33ff;
|
||||
}
|
||||
|
||||
.jfx-check-box {
|
||||
-fx-text-fill: #cc66ff;
|
||||
-fx-padding: 8;
|
||||
}
|
||||
|
||||
.jfx-check-box .box {
|
||||
-fx-background-color: #2b0052;
|
||||
-fx-border-color: #ff33ff;
|
||||
-fx-border-width: 2;
|
||||
}
|
||||
|
||||
.jfx-check-box:selected .box .mark {
|
||||
-fx-background-color: #ff33ff;
|
||||
}
|
||||
|
||||
.file-tree {
|
||||
-fx-background-color: #44475a;
|
||||
}
|
||||
|
||||
.file-tree .tree-cell {
|
||||
-fx-background-color: #44475a;
|
||||
-fx-text-fill: #f8f8f2;
|
||||
}
|
||||
|
||||
.file-tree .tree-cell:selected {
|
||||
-fx-background-color: #6272a4;
|
||||
}
|
||||
|
||||
.file-tree .tree-cell {
|
||||
-fx-background-color: transparent;
|
||||
-fx-text-fill: #cc66ff;
|
||||
-fx-padding: 5;
|
||||
}
|
||||
|
||||
.file-tree .tree-cell:hover {
|
||||
-fx-background-color: #2b0052;
|
||||
}
|
||||
|
||||
.scan-button {
|
||||
-fx-background-color: #4d0099;
|
||||
-fx-text-fill: #ff33ff;
|
||||
-fx-border-color: #ff33ff;
|
||||
-fx-border-width: 2;
|
||||
-fx-padding: 15;
|
||||
-fx-font-size: 16pt;
|
||||
-fx-font-family: 'Consolas';
|
||||
-fx-font-weight: bold;
|
||||
-fx-border-radius: 5;
|
||||
-fx-background-radius: 5;
|
||||
-fx-cursor: hand;
|
||||
-fx-effect: dropshadow(gaussian, #ff33ff44, 20, 0, 0, 0);
|
||||
}
|
||||
|
||||
.scan-button:hover {
|
||||
-fx-background-color: #1a1a2e;
|
||||
-fx-effect: dropshadow(gaussian, #00ffff88, 25, 0, 0, 0);
|
||||
-fx-text-fill: #00ffff;
|
||||
}
|
||||
|
||||
.scan-button:disabled {
|
||||
-fx-background-color: #2b0052;
|
||||
-fx-text-fill: #9933ff;
|
||||
-fx-border-color: #4d0099;
|
||||
-fx-effect: none;
|
||||
}
|
||||
|
||||
.result-display {
|
||||
-fx-font-family: "JetBrains Mono", "Consolas", monospace;
|
||||
-fx-font-size: 14px;
|
||||
-fx-background-color: #282a36;
|
||||
-fx-text-fill: #f8f8f2;
|
||||
-fx-padding: 10px;
|
||||
}
|
||||
|
||||
.result-display .content {
|
||||
-fx-background-color: #282a36;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
-fx-padding: 5px;
|
||||
-fx-background-color: #44475a;
|
||||
-fx-text-fill: #f8f8f2;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
-fx-accent: #50fa7b;
|
||||
}
|
||||
|
||||
.progress-bar .track {
|
||||
-fx-background-color: #44475a;
|
||||
}
|
||||
|
||||
.progress-bar .track {
|
||||
-fx-background-color: #2b0052;
|
||||
}
|
||||
|
||||
.progress-bar .bar {
|
||||
-fx-background-insets: 0;
|
||||
-fx-background-radius: 0;
|
||||
-fx-effect: dropshadow(gaussian, #ff33ff44, 10, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.scroll-bar:vertical,
|
||||
.scroll-bar:horizontal {
|
||||
-fx-background-color: #1a0033;
|
||||
}
|
||||
|
||||
.scroll-bar:vertical .thumb,
|
||||
.scroll-bar:horizontal .thumb {
|
||||
-fx-background-color: #4d0099;
|
||||
-fx-background-radius: 0;
|
||||
}
|
||||
|
||||
.scroll-bar:vertical .thumb:hover,
|
||||
.scroll-bar:horizontal .thumb:hover {
|
||||
-fx-background-color: #6600cc;
|
||||
}
|
||||
|
||||
.scroll-bar .increment-button,
|
||||
.scroll-bar .decrement-button {
|
||||
-fx-background-color: #2b0052;
|
||||
-fx-border-color: #4d0099;
|
||||
}
|
||||
|
||||
.scroll-bar .increment-button:hover,
|
||||
.scroll-bar .decrement-button:hover {
|
||||
-fx-background-color: #4d0099;
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
.cyber-glow {
|
||||
-fx-effect: dropshadow(gaussian, #ff33ff44, 20, 0, 0, 0);
|
||||
-fx-transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.cyber-glow:hover {
|
||||
-fx-effect: dropshadow(gaussian, #ff33ff88, 25, 0, 0, 0);
|
||||
}
|
||||