├── README.md ├── assets ├── image-20250222011558648.png ├── image-20250222012127096.png ├── image-20250222012232207.png ├── image-20250222012315255.png ├── image-20250222012445385.png ├── image-20250222012838474.png ├── image-20250222012916552.png └── image-20250222014422442.png ├── dependency-reduced-pom.xml ├── pom.xml └── src └── main ├── java └── com │ └── cyberscanner │ ├── ColoredMessageCallback.java │ ├── CyberScannerApp.java │ ├── ProgressCallback.java │ ├── ScanTask.java │ └── StatusBar.java └── resources ├── config.yaml └── styles └── cyber-theme.css /README.md: -------------------------------------------------------------------------------- 1 | # CyberMatrix 量子安全分析引擎 2 | 3 | CyberMatrix 是一个基于 AI 的代码安全分析工具,专注于自动化检测和分析代码中的潜在安全漏洞。采用赛博朋克风格的现代化界面,提供直观的安全分析体验。 4 | 5 | ![image-20250222011558648](./assets/image-20250222011558648.png) 6 | 7 | ## 核心功能 8 | 9 | ### 1. 深度安全分析 10 | - 自动识别代码中的安全风险点 11 | - 支持检测 SQL 注入、XSS、CSRF、文件上传漏洞等常见安全问题 12 | - 基于 CVSS 评分标准评估风险等级 13 | - 提供详细的漏洞位置和描述 14 | 15 | ![image-20250222014422442](./assets/image-20250222014422442.png) 16 | 17 | ### 2. Webshell 检测 18 | - 智能识别 PHP/JSP/ASP 等 WebShell 特征 19 | - 检测内存马和无文件落地木马 20 | - 分析可疑的代码执行和文件操作 21 | - 识别混淆编码和加密规避技术 22 | 23 | ![image-20250222012127096](./assets/image-20250222012127096.png) 24 | 25 | ### 3. 智能分析引擎 26 | - 基于大语言模型的智能代码理解 27 | - 支持多种编程语言的代码分析 28 | - 极低的误报率和高准确度 29 | - 持续学习和优化的检测能力 30 | 31 | ## 界面特点 32 | 33 | - 赛博朋克风格界面设计 34 | - 实时进度展示和分析反馈 35 | - 直观的文件树浏览 36 | - 智能的颜色标记系统 37 | - 流畅的动画效果 38 | 39 |
40 | 常态界面 41 | 扫描状态界面 42 |
43 | 44 | ## 技术栈 45 | 46 | - JavaFX + JFoenix:现代化 UI 框架 47 | - Ollama:本地化 AI 模型 48 | - AnimateFX:界面动画效果 49 | - Jackson:JSON 处理 50 | - OkHttp:网络请求 51 | 52 | ## 使用方法 53 | 54 | 1. 点击"初始化量子扫描目标"选择要分析的代码目录 55 | 2. 选择分析模式(深度安全分析/Webshell检测) 56 | 3. 可选择是否包含静态资源分析 57 | 4. 点击"启动量子安全分析"开始扫描 58 | 5. 实时查看分析结果和进度 59 | 60 |
61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 |
选择目标选择模式
1. 选择扫描目标2. 选择分析模式
开始扫描查看结果
3. 启动扫描4. 查看分析结果
79 |
80 | 81 | 82 | 83 | ## 配置要求 84 | 85 | - Java 8 或更高版本 86 | - Ollama 本地服务 87 | - 建议系统内存 8GB 以上 88 | 89 | ## 安装说明 90 | 91 | 1. 确保已安装 Java 8 运行环境 92 | 2. 下载并安装 Ollama 93 | 3. 配置 config.yaml 文件 94 | 4. 运行启动脚本 95 | 96 | ## 开发环境搭建 97 | 98 | 1. Clone 项目代码 99 | 2. 使用 Maven 导入依赖 100 | 3. 配置 JDK 8 101 | 4. 运行 CyberScannerApp 主类 102 | 103 | ## 参考项目 104 | 105 | 本项目受以下开源项目启发: 106 | - [DeepSeekSelfTool](https://github.com/ChinaRan0/DeepSeekSelfTool) - 基于 DeepSeek 的代码安全分析工具 107 | 108 | ## 许可证 109 | 110 | MIT License -------------------------------------------------------------------------------- /assets/image-20250222011558648.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/savior-only/CyberMatrix/a4260b3d42b20d2f3b6afa0053a959b94fa0d394/assets/image-20250222011558648.png -------------------------------------------------------------------------------- /assets/image-20250222012127096.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/savior-only/CyberMatrix/a4260b3d42b20d2f3b6afa0053a959b94fa0d394/assets/image-20250222012127096.png -------------------------------------------------------------------------------- /assets/image-20250222012232207.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/savior-only/CyberMatrix/a4260b3d42b20d2f3b6afa0053a959b94fa0d394/assets/image-20250222012232207.png -------------------------------------------------------------------------------- /assets/image-20250222012315255.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/savior-only/CyberMatrix/a4260b3d42b20d2f3b6afa0053a959b94fa0d394/assets/image-20250222012315255.png -------------------------------------------------------------------------------- /assets/image-20250222012445385.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/savior-only/CyberMatrix/a4260b3d42b20d2f3b6afa0053a959b94fa0d394/assets/image-20250222012445385.png -------------------------------------------------------------------------------- /assets/image-20250222012838474.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/savior-only/CyberMatrix/a4260b3d42b20d2f3b6afa0053a959b94fa0d394/assets/image-20250222012838474.png -------------------------------------------------------------------------------- /assets/image-20250222012916552.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/savior-only/CyberMatrix/a4260b3d42b20d2f3b6afa0053a959b94fa0d394/assets/image-20250222012916552.png -------------------------------------------------------------------------------- /assets/image-20250222014422442.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/savior-only/CyberMatrix/a4260b3d42b20d2f3b6afa0053a959b94fa0d394/assets/image-20250222014422442.png -------------------------------------------------------------------------------- /dependency-reduced-pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | com.cyberscanner 5 | cyber-scanner 6 | 1.0-SNAPSHOT 7 | 8 | 9 | 10 | maven-compiler-plugin 11 | 3.8.1 12 | 13 | 1.8 14 | 1.8 15 | 16 | 17 | 18 | maven-shade-plugin 19 | 3.2.4 20 | 21 | 22 | package 23 | 24 | shade 25 | 26 | 27 | 28 | 29 | com.cyberscanner.CyberScannerApp 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | com.itextpdf 41 | itext7-core 42 | 7.2.5 43 | pom 44 | compile 45 | 46 | 47 | 48 | 1.8 49 | UTF-8 50 | 8.0.202 51 | 1.8 52 | 53 | 54 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.cyberscanner 8 | cyber-scanner 9 | 1.0-SNAPSHOT 10 | 11 | 12 | UTF-8 13 | 1.8 14 | 1.8 15 | 16 | 17 | 18 | 19 | com.jfoenix 20 | jfoenix 21 | 8.0.10 22 | 23 | 24 | 25 | 26 | org.yaml 27 | snakeyaml 28 | 1.33 29 | 30 | 31 | 32 | 33 | com.squareup.okhttp3 34 | okhttp 35 | 3.14.9 36 | 37 | 38 | 39 | 40 | com.fasterxml.jackson.core 41 | jackson-databind 42 | 2.12.7.1 43 | 44 | 45 | 46 | 47 | io.github.typhon0 48 | AnimateFX 49 | 1.2.1 50 | 51 | 52 | 53 | 54 | ch.qos.logback 55 | logback-classic 56 | 1.2.9 57 | 58 | 59 | 60 | 61 | 62 | 63 | org.apache.maven.plugins 64 | maven-compiler-plugin 65 | 3.8.1 66 | 67 | 1.8 68 | 1.8 69 | 70 | 71 | 72 | org.apache.maven.plugins 73 | maven-shade-plugin 74 | 3.2.4 75 | 76 | 77 | package 78 | 79 | shade 80 | 81 | 82 | false 83 | 84 | 85 | *:* 86 | 87 | META-INF/*.SF 88 | META-INF/*.DSA 89 | META-INF/*.RSA 90 | config.yaml 91 | 92 | 93 | 94 | 95 | 96 | com.cyberscanner.CyberScannerApp 97 | 98 | 99 | 100 | META-INF/services/javax.annotation.processing.Processor 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /src/main/java/com/cyberscanner/ColoredMessageCallback.java: -------------------------------------------------------------------------------- 1 | package com.cyberscanner; 2 | 3 | import javafx.scene.paint.Color; 4 | 5 | public interface ColoredMessageCallback { 6 | void updateMessage(String message, Color color); 7 | } -------------------------------------------------------------------------------- /src/main/java/com/cyberscanner/CyberScannerApp.java: -------------------------------------------------------------------------------- 1 | package com.cyberscanner; 2 | 3 | import animatefx.animation.Bounce; 4 | import animatefx.animation.FadeIn; 5 | import animatefx.animation.Pulse; 6 | import javafx.animation.KeyFrame; 7 | import javafx.animation.KeyValue; 8 | import javafx.animation.Timeline; 9 | import javafx.scene.effect.DropShadow; 10 | import javafx.scene.effect.Glow; 11 | import javafx.util.Duration; 12 | import com.jfoenix.controls.*; 13 | import javafx.application.Application; 14 | import javafx.geometry.Insets; 15 | import javafx.geometry.Pos; 16 | import javafx.scene.Scene; 17 | import javafx.scene.control.*; 18 | import javafx.scene.layout.*; 19 | import javafx.scene.text.Text; 20 | import javafx.scene.paint.Color; 21 | import javafx.stage.DirectoryChooser; 22 | import javafx.stage.Stage; 23 | 24 | import java.io.File; 25 | import java.util.Arrays; 26 | import java.util.Collections; 27 | 28 | public class CyberScannerApp extends Application { 29 | private ScanTask currentTask; 30 | 31 | private JFXTextArea resultDisplay; 32 | private JFXButton scanButton; 33 | private JFXButton selectButton; 34 | private Label pathLabel; 35 | private TreeView fileTree; 36 | private JFXRadioButton auditRadio; 37 | private JFXRadioButton webshellRadio; 38 | private JFXCheckBox auditJsCheckbox; 39 | private StatusBar statusBar; 40 | private JFXProgressBar progressBar; 41 | 42 | @Override 43 | public void start(Stage primaryStage) { 44 | primaryStage.setOnCloseRequest(event -> { 45 | if (currentTask != null) { 46 | currentTask.stop(); 47 | } 48 | }); 49 | primaryStage.setTitle("CyberMatrix - 量子安全分析引擎"); 50 | primaryStage.setMinWidth(1280); 51 | primaryStage.setMinHeight(720); 52 | 53 | // 创建主布局 54 | BorderPane mainLayout = new BorderPane(); 55 | mainLayout.getStyleClass().add("main-layout"); 56 | mainLayout.setPrefSize(1280, 720); 57 | 58 | // 左侧面板 59 | VBox leftPanel = createLeftPanel(); 60 | leftPanel.setMinWidth(300); 61 | leftPanel.setPrefWidth(300); 62 | leftPanel.setMaxWidth(400); 63 | VBox.setVgrow(fileTree, Priority.ALWAYS); 64 | mainLayout.setLeft(leftPanel); 65 | 66 | // 右侧结果显示区 67 | resultDisplay = new JFXTextArea(); 68 | resultDisplay.getStyleClass().add("result-display"); 69 | resultDisplay.setEditable(false); 70 | resultDisplay.setWrapText(true); 71 | BorderPane.setMargin(resultDisplay, new Insets(10)); 72 | VBox.setVgrow(resultDisplay, Priority.ALWAYS); 73 | mainLayout.setCenter(resultDisplay); 74 | 75 | // 进度条 76 | progressBar = new JFXProgressBar(); 77 | progressBar.setProgress(0); 78 | progressBar.getStyleClass().add("progress-bar"); 79 | progressBar.setPrefWidth(Double.MAX_VALUE); 80 | 81 | // 状态栏 82 | statusBar = new StatusBar(); 83 | statusBar.getStyleClass().add("status-bar"); 84 | 85 | // 将进度条和状态栏放在底部 86 | VBox bottomBox = new VBox(5); 87 | bottomBox.setPadding(new Insets(5)); 88 | bottomBox.getChildren().addAll(progressBar, statusBar); 89 | mainLayout.setBottom(bottomBox); 90 | 91 | // 场景和样式 92 | Scene scene = new Scene(mainLayout); 93 | scene.getStylesheets().add(getClass().getResource("/styles/cyber-theme.css").toExternalForm()); 94 | 95 | primaryStage.setScene(scene); 96 | primaryStage.show(); 97 | 98 | // 添加启动动画序列 99 | new FadeIn(mainLayout).play(); 100 | new Pulse(selectButton).play(); 101 | 102 | // 为pathLabel添加发光效果动画 103 | Glow glow = new Glow(0); 104 | pathLabel.setEffect(glow); 105 | Timeline glowTimeline = new Timeline( 106 | new KeyFrame(Duration.ZERO, new KeyValue(glow.levelProperty(), 0)), 107 | new KeyFrame(Duration.seconds(1), new KeyValue(glow.levelProperty(), 0.8)), 108 | new KeyFrame(Duration.seconds(2), new KeyValue(glow.levelProperty(), 0)) 109 | ); 110 | glowTimeline.setCycleCount(Timeline.INDEFINITE); 111 | glowTimeline.play(); 112 | 113 | // 添加周期性动画效果 114 | Timeline pulseTimeline = new Timeline( 115 | new KeyFrame(Duration.seconds(2), 116 | new KeyValue(leftPanel.effectProperty(), 117 | new DropShadow(10, Color.valueOf("#4d4dff")))) 118 | ); 119 | pulseTimeline.setAutoReverse(true); 120 | pulseTimeline.setCycleCount(Timeline.INDEFINITE); 121 | pulseTimeline.play(); 122 | } 123 | 124 | private VBox createLeftPanel() { 125 | VBox leftPanel = new VBox(10); 126 | leftPanel.getStyleClass().add("left-panel"); 127 | leftPanel.setPadding(new Insets(10)); 128 | 129 | // 目录选择按钮 130 | selectButton = new JFXButton("🌐 初始化量子扫描目标"); 131 | selectButton.getStyleClass().add("cyber-button"); 132 | selectButton.setOnAction(e -> selectDirectory()); 133 | 134 | // 路径显示标签 135 | pathLabel = new Label("等待目标初始化..."); 136 | pathLabel.getStyleClass().add("path-label"); 137 | 138 | // 模式选择组 139 | VBox modeBox = createModeSelectionBox(); 140 | 141 | // 文件树 142 | fileTree = new TreeView<>(); 143 | fileTree.getStyleClass().add("file-tree"); 144 | VBox.setVgrow(fileTree, Priority.ALWAYS); 145 | 146 | // 扫描按钮 147 | scanButton = new JFXButton("⚡ 启动量子安全分析"); 148 | scanButton.getStyleClass().add("scan-button"); 149 | scanButton.setDisable(true); 150 | scanButton.setOnAction(e -> startScan()); 151 | 152 | leftPanel.getChildren().addAll( 153 | selectButton, 154 | pathLabel, 155 | modeBox, 156 | auditJsCheckbox, 157 | fileTree, 158 | scanButton 159 | ); 160 | 161 | return leftPanel; 162 | } 163 | 164 | private VBox createModeSelectionBox() { 165 | VBox modeBox = new VBox(5); 166 | modeBox.getStyleClass().add("mode-box"); 167 | 168 | Label modeLabel = new Label("🔧 分析模式"); 169 | modeLabel.getStyleClass().add("mode-label"); 170 | 171 | ToggleGroup modeGroup = new ToggleGroup(); 172 | auditRadio = new JFXRadioButton("深度安全分析"); 173 | webshellRadio = new JFXRadioButton("Webshell检测"); 174 | 175 | auditRadio.setToggleGroup(modeGroup); 176 | webshellRadio.setToggleGroup(modeGroup); 177 | auditRadio.setSelected(true); 178 | 179 | auditJsCheckbox = new JFXCheckBox("包含静态资源分析"); 180 | auditJsCheckbox.setSelected(true); 181 | 182 | modeBox.getChildren().addAll(modeLabel, auditRadio, webshellRadio); 183 | return modeBox; 184 | } 185 | 186 | private void selectDirectory() { 187 | DirectoryChooser directoryChooser = new DirectoryChooser(); 188 | directoryChooser.setTitle("选择代码矩阵接入点"); 189 | File selectedDirectory = directoryChooser.showDialog(null); 190 | 191 | if (selectedDirectory != null) { 192 | pathLabel.setText("📂 目标:" + selectedDirectory.getName()); 193 | updateFileTree(selectedDirectory); 194 | scanButton.setDisable(false); 195 | statusBar.setStatus("✅ 目标初始化完成"); 196 | 197 | // 添加动画效果 198 | new Pulse(scanButton).play(); 199 | } 200 | } 201 | 202 | private int totalFileCount = 0; 203 | 204 | private void updateFileTree(File root) { 205 | TreeItem rootItem = new TreeItem<>(root); 206 | rootItem.setExpanded(true); 207 | fileTree.setRoot(rootItem); 208 | totalFileCount = 0; 209 | populateFileTree(rootItem); 210 | 211 | // 设置单元格工厂来自定义显示 212 | fileTree.setCellFactory(tv -> new TreeCell() { 213 | @Override 214 | protected void updateItem(File item, boolean empty) { 215 | super.updateItem(item, empty); 216 | if (empty || item == null) { 217 | setText(null); 218 | } else { 219 | // 只显示文件或目录名,而不是完整路径 220 | setText(item.getName()); 221 | } 222 | } 223 | }); 224 | } 225 | 226 | private void populateFileTree(TreeItem item) { 227 | File[] files = item.getValue().listFiles(); 228 | if (files != null) { 229 | Arrays.sort(files, (f1, f2) -> { 230 | // 目录优先,然后按名称排序 231 | if (f1.isDirectory() && !f2.isDirectory()) { 232 | return -1; 233 | } else if (!f1.isDirectory() && f2.isDirectory()) { 234 | return 1; 235 | } else { 236 | return f1.getName().compareToIgnoreCase(f2.getName()); 237 | } 238 | }); 239 | 240 | for (File file : files) { 241 | TreeItem fileItem = new TreeItem<>(file); 242 | item.getChildren().add(fileItem); 243 | if (!file.isDirectory()) { 244 | totalFileCount++; 245 | } 246 | if (file.isDirectory()) { 247 | populateFileTree(fileItem); 248 | } 249 | } 250 | } 251 | } 252 | 253 | private void startScan() { 254 | if (currentTask != null) { 255 | currentTask.stop(); 256 | } 257 | if (fileTree.getRoot() == null) { 258 | showAlert("警告", "请先选择代码目录!"); 259 | return; 260 | } 261 | 262 | scanButton.setDisable(true); 263 | String initMsg = auditRadio.isSelected() ? 264 | "🚀 正在启动深度安全分析引擎..." : 265 | "🕵️ 正在启动Webshell检测引擎..."; 266 | 267 | resultDisplay.setText(initMsg + "\n" + String.join("", Collections.nCopies(50, "▮")) + "\n"); 268 | 269 | // 创建并启动扫描任务 270 | currentTask = new ScanTask( 271 | fileTree.getRoot().getValue(), 272 | auditRadio.isSelected(), 273 | auditJsCheckbox.isSelected(), 274 | this::updateProgress, 275 | this::updateStatus, 276 | this::showResults 277 | ); 278 | new Thread(currentTask).start(); 279 | } 280 | 281 | private void updateProgress(double progress) { 282 | javafx.application.Platform.runLater(() -> { 283 | progressBar.setProgress(progress); 284 | }); 285 | } 286 | 287 | private void updateStatus(String message, javafx.scene.paint.Color color) { 288 | javafx.application.Platform.runLater(() -> { 289 | statusBar.setStatus(message); 290 | String style = String.format("-fx-text-fill: %s;", color.toString().replace("0x", "#")); 291 | Text text = new Text("⚡ " + message + "\n"); 292 | text.setStyle(style); 293 | resultDisplay.appendText(text.getText()); 294 | resultDisplay.setStyle(style); 295 | }); 296 | } 297 | 298 | private void showResults(String report) { 299 | javafx.application.Platform.runLater(() -> { 300 | scanButton.setDisable(false); 301 | 302 | if (auditRadio.isSelected()) { 303 | String header = "\n📊 深度安全分析完成!统计信息:\n"; 304 | 305 | // 解析报告并生成统计信息 306 | String[] lines = report.split("\n"); 307 | int highRiskIssues = 0; 308 | int mediumRiskIssues = 0; 309 | 310 | for (String line : lines) { 311 | if (line.contains("[高危]")) highRiskIssues++; 312 | if (line.contains("[中危]")) mediumRiskIssues++; 313 | } 314 | 315 | int totalIssues = highRiskIssues + mediumRiskIssues; 316 | StringBuilder statisticsBuilder = new StringBuilder(); 317 | statisticsBuilder.append(String.format( 318 | "总扫描文件数:%d\n" + 319 | "高危问题数:%d\n" + 320 | "中危问题数:%d\n" + 321 | "问题总计:%d\n", 322 | totalFileCount, highRiskIssues, mediumRiskIssues, totalIssues 323 | )); 324 | 325 | resultDisplay.appendText(header + statisticsBuilder.toString()); 326 | } else { 327 | resultDisplay.appendText("\n🎯 Webshell检测完成!"); 328 | } 329 | 330 | statusBar.setStatus("✅ 扫描完成"); 331 | 332 | // 添加完成动画 333 | new Bounce(resultDisplay).play(); 334 | }); 335 | } 336 | 337 | private void showAlert(String title, String content) { 338 | Alert alert = new Alert(Alert.AlertType.WARNING); 339 | alert.setTitle(title); 340 | alert.setHeaderText(null); 341 | alert.setContentText(content); 342 | alert.showAndWait(); 343 | } 344 | 345 | public static void main(String[] args) { 346 | launch(args); 347 | } 348 | } -------------------------------------------------------------------------------- /src/main/java/com/cyberscanner/ProgressCallback.java: -------------------------------------------------------------------------------- 1 | package com.cyberscanner; 2 | 3 | @FunctionalInterface 4 | public interface ProgressCallback { 5 | void updateProgress(double progress); 6 | } 7 | 8 | @FunctionalInterface 9 | interface MessageCallback { 10 | void updateMessage(String message); 11 | } -------------------------------------------------------------------------------- /src/main/java/com/cyberscanner/ScanTask.java: -------------------------------------------------------------------------------- 1 | package com.cyberscanner; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import okhttp3.*; 6 | import org.yaml.snakeyaml.Yaml; 7 | 8 | import java.io.*; 9 | import java.nio.charset.StandardCharsets; 10 | import java.util.*; 11 | import java.util.function.Consumer; 12 | 13 | public class ScanTask implements Runnable { 14 | private volatile boolean isRunning = true; 15 | private final File rootDirectory; 16 | private final boolean isAuditMode; 17 | private final boolean includeJsFiles; 18 | private final ProgressCallback progressCallback; 19 | private final ColoredMessageCallback messageCallback; 20 | private final Consumer resultCallback; 21 | private final OkHttpClient httpClient; 22 | private final ObjectMapper objectMapper; 23 | private String ollamaHost; 24 | private String ollamaModel; 25 | 26 | public ScanTask(File rootDirectory, boolean isAuditMode, boolean includeJsFiles, 27 | ProgressCallback progressCallback, ColoredMessageCallback messageCallback, Consumer resultCallback) { 28 | this.rootDirectory = rootDirectory; 29 | this.isAuditMode = isAuditMode; 30 | this.includeJsFiles = includeJsFiles; 31 | this.progressCallback = progressCallback; 32 | this.messageCallback = messageCallback; 33 | this.resultCallback = resultCallback; 34 | this.httpClient = new OkHttpClient.Builder() 35 | .connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS) 36 | .writeTimeout(30, java.util.concurrent.TimeUnit.SECONDS) 37 | .readTimeout(60, java.util.concurrent.TimeUnit.SECONDS) 38 | .retryOnConnectionFailure(true) 39 | .build(); 40 | this.objectMapper = new ObjectMapper(); 41 | loadConfig(); 42 | } 43 | 44 | private void loadConfig() { 45 | try { 46 | // 首先尝试从jar包同级目录读取config.yaml 47 | File externalConfig = new File(new File(getClass().getProtectionDomain() 48 | .getCodeSource().getLocation().toURI()).getParent(), "config.yaml"); 49 | 50 | InputStream inputStream; 51 | if (externalConfig.exists()) { 52 | inputStream = new FileInputStream(externalConfig); 53 | } else { 54 | // 如果外部配置不存在,尝试从jar包内部读取 55 | inputStream = getClass().getResourceAsStream("/config.yaml"); 56 | if (inputStream == null) { 57 | // 如果两个位置都没有配置文件,显示错误对话框 58 | javafx.application.Platform.runLater(() -> { 59 | javafx.scene.control.Alert alert = new javafx.scene.control.Alert(javafx.scene.control.Alert.AlertType.ERROR); 60 | alert.setTitle("配置错误"); 61 | alert.setHeaderText("找不到配置文件"); 62 | alert.setContentText(String.format("请确保在程序目录 %s 下存在 config.yaml 配置文件。\n\n" + 63 | "配置文件示例内容:\n" + 64 | "api:\n" + 65 | " ollama:\n" + 66 | " url: http://localhost:11434/api\n" + 67 | " model: deepseek-coder", 68 | externalConfig.getParent())); 69 | alert.showAndWait(); 70 | System.exit(1); 71 | }); 72 | return; 73 | } 74 | } 75 | 76 | Yaml yaml = new Yaml(); 77 | Map config = yaml.load(inputStream); 78 | 79 | Map api = (Map) config.get("api"); 80 | Map ollama = (Map) api.get("ollama"); 81 | String ollamaUrl = (String) ollama.get("url"); 82 | this.ollamaHost = ollamaUrl.split("/api")[0]; 83 | this.ollamaModel = (String) ollama.get("model"); 84 | 85 | inputStream.close(); 86 | } catch (Exception e) { 87 | e.printStackTrace(); 88 | // 显示错误对话框 89 | javafx.application.Platform.runLater(() -> { 90 | javafx.scene.control.Alert alert = new javafx.scene.control.Alert(javafx.scene.control.Alert.AlertType.ERROR); 91 | alert.setTitle("配置错误"); 92 | alert.setHeaderText("配置文件读取失败"); 93 | alert.setContentText("请检查config.yaml文件格式是否正确。\n\n错误信息:" + e.getMessage()); 94 | alert.showAndWait(); 95 | System.exit(1); 96 | }); 97 | } 98 | } 99 | 100 | public void stop() { 101 | isRunning = false; 102 | } 103 | 104 | @Override 105 | public void run() { 106 | Map filesContent = scanCodeFiles(rootDirectory); 107 | List results = new ArrayList<>(); 108 | int totalFiles = filesContent.size(); // 使用实际扫描的文件数量 109 | int processedFiles = 0; 110 | int detectedFiles = 0; // 新增:检测到问题的文件数量 111 | 112 | // 初始化进度条 113 | javafx.application.Platform.runLater(() -> { 114 | progressCallback.updateProgress(0.0); 115 | }); 116 | 117 | for (Map.Entry entry : filesContent.entrySet()) { 118 | if (!isRunning) { 119 | messageCallback.updateMessage("⚠️ 扫描任务已中断", javafx.scene.paint.Color.web("#FFB86C")); 120 | return; 121 | } 122 | String filepath = entry.getKey(); 123 | String content = entry.getValue(); 124 | 125 | try { 126 | String filename = new File(filepath).getName(); 127 | messageCallback.updateMessage( 128 | isAuditMode ? 129 | String.format("🔍 分析 %s... (%d/%d)", filename, processedFiles + 1, totalFiles) : 130 | String.format("🕵️ 扫描 %s... (%d/%d)", filename, processedFiles + 1, totalFiles), 131 | javafx.scene.paint.Color.DODGERBLUE); 132 | 133 | String prompt = createPrompt(content); 134 | String response = callOllamaAPI(prompt); 135 | String result = processResponse(response); 136 | 137 | // 如果检测到问题(包含[高危]标记),增加检测文件计数 138 | if (!isAuditMode && result.contains("[高危]") && !results.stream().anyMatch(r -> r.contains(filepath))) { 139 | detectedFiles++; 140 | } 141 | 142 | // 根据扫描结果中的风险等级设置不同的颜色 143 | String formattedResult = String.format("%s %s\n%s\n%s", 144 | isAuditMode ? "📄" : "📁", 145 | filepath, 146 | result, 147 | String.join("", Collections.nCopies(50, "━"))); 148 | 149 | // 根据结果内容设置不同的颜色 150 | javafx.scene.paint.Color messageColor; 151 | if (result.contains("[高危]")) { 152 | messageColor = javafx.scene.paint.Color.web("#FF4444"); 153 | } else if (result.contains("[中危]")) { 154 | messageColor = javafx.scene.paint.Color.web("#FFB86C"); 155 | } else if (result.contains("[低危]")) { 156 | messageColor = javafx.scene.paint.Color.web("#50FA7B"); 157 | } else { 158 | messageColor = javafx.scene.paint.Color.web("#8BE9FD"); 159 | } 160 | 161 | messageCallback.updateMessage(formattedResult, messageColor); 162 | results.add(formattedResult); 163 | 164 | // 更新进度 165 | processedFiles++; 166 | double progress = (double) processedFiles / totalFiles; 167 | javafx.application.Platform.runLater(() -> { 168 | progressCallback.updateProgress(progress); 169 | }); 170 | } catch (Exception e) { 171 | String errorMessage = String.format("❌ 错误:%s\n%s", filepath, e.getMessage()); 172 | messageCallback.updateMessage(errorMessage, javafx.scene.paint.Color.web("#6272A4")); 173 | results.add(errorMessage); 174 | } 175 | } 176 | 177 | // Webshell检测模式下不在这里添加统计信息,统计信息由UI层处理 178 | resultCallback.accept(String.join("\n", results)); 179 | } 180 | 181 | private Map scanCodeFiles(File directory) { 182 | Map codeFiles = new HashMap<>(); 183 | List allowedExt = new ArrayList<>(Arrays.asList( 184 | ".php", ".jsp", ".jspx", ".asp", ".aspx", ".js", ".html", ".py", ".java" 185 | )); 186 | 187 | if (!includeJsFiles) { 188 | allowedExt.remove(".js"); 189 | allowedExt.remove(".html"); 190 | } 191 | 192 | scanDirectory(directory, codeFiles, allowedExt); 193 | return codeFiles; 194 | } 195 | 196 | private void scanDirectory(File directory, Map codeFiles, List allowedExt) { 197 | File[] files = directory.listFiles(); 198 | if (files != null) { 199 | for (File file : files) { 200 | if (file.isDirectory()) { 201 | scanDirectory(file, codeFiles, allowedExt); 202 | } else { 203 | String extension = getFileExtension(file); 204 | if (allowedExt.contains(extension.toLowerCase())) { 205 | try { 206 | String content = readFile(file); 207 | // 使用相对路径存储文件 208 | String relativePath = rootDirectory.toPath().relativize(file.toPath()).toString(); 209 | codeFiles.put(relativePath, content); 210 | } catch (IOException e) { 211 | String relativePath = rootDirectory.toPath().relativize(file.toPath()).toString(); 212 | codeFiles.put(relativePath, "无法读取文件内容"); 213 | } 214 | } 215 | } 216 | } 217 | } 218 | } 219 | 220 | private String getFileExtension(File file) { 221 | String name = file.getName(); 222 | int lastIndexOf = name.lastIndexOf("."); 223 | return lastIndexOf == -1 ? "" : name.substring(lastIndexOf); 224 | } 225 | 226 | private String readFile(File file) throws IOException { 227 | StringBuilder content = new StringBuilder(); 228 | try (BufferedReader reader = new BufferedReader( 229 | new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) { 230 | String line; 231 | while ((line = reader.readLine()) != null) { 232 | content.append(line).append("\n"); 233 | } 234 | } 235 | return content.toString(); 236 | } 237 | 238 | private String createPrompt(String content) { 239 | if (isAuditMode) { 240 | return String.format("【强制指令】你是一个专业的安全审计AI,请按以下要求分析代码:\n\n" + 241 | "1. 漏洞分析流程:\n" + 242 | " 1.1 识别潜在风险点(SQL操作、文件操作、用户输入点、文件上传漏洞、CSRF、SSRF、XSS、RCE、OWASP top10等漏洞)\n" + 243 | " 1.2 验证漏洞可利用性\n" + 244 | " 1.3 按CVSS评分标准评估风险等级\n\n" + 245 | "2. 输出规则:\n" + 246 | " - 仅输出确认存在的高危/中危漏洞\n" + 247 | " - 使用严格格式:[风险等级] 类型 - 位置:行号 - 50字内描述\n" + 248 | " - 禁止解释漏洞原理\n" + 249 | " - 禁止给出修复建议\n" + 250 | " - 如果有可能,给出POC(HTTP请求数据包)\n\n" + 251 | "3. 输出示例(除此外不要有任何输出):\n" + 252 | " [高危] SQL注入 - user_login.php:32 - 未过滤的$_GET参数直接拼接SQL查询\n" + 253 | " [POC]\nPOST /login.php HTTP/1.1\n" + 254 | " Host: example.com\n" + 255 | " Content-Type: application/x-www-form-urlencoded\n" + 256 | " [中危] XSS - comment.jsp:15 - 未转义的userInput输出到HTML\n" + 257 | " [POC]\nPOST /login.php HTTP/1.1\n" + 258 | " Host: example.com\n" + 259 | " Content-Type: application/x-www-form-urlencoded\n\n" + 260 | "4. 当前代码(仅限分析):\n%s", content.substring(0, Math.min(content.length(), 3000))); 261 | } else { 262 | return String.format("【Webshell检测指令】请严格按以下步骤分析代码:\n\n" + 263 | "1. 检测要求: \n" + 264 | " 请分析以下文件内容是否为WebShell或内存马。要求:\n" + 265 | " 1. 检查PHP/JSP/ASP等WebShell特征(如加密函数、执行系统命令、文件操作)\n" + 266 | " 2. 识别内存马特征(如无文件落地、进程注入、异常网络连接)\n" + 267 | " 3. 分析代码中的可疑功能(如命令执行、文件上传、信息收集)\n" + 268 | " 4. 检查混淆编码、加密手段等规避技术\n\n" + 269 | "2. 判断规则:\n" + 270 | " - 仅当确认恶意性时报告\n" + 271 | " - 输出格式:🔴 [高危] Webshell - 文件名:行号 - 检测到[特征1+特征2+...]\n\n" + 272 | "3. 输出示例(严格按照此格式输出,不要有任何的补充,如果未检测到危险,则不输出,除此之外,不要有任何输出):\n" + 273 | " 🔴 [高危] Webshell - malicious.php:8 - 检测到[system执行+base64解码+错误抑制]\n\n" + 274 | "4. 待分析代码:\n%s", content.substring(0, Math.min(content.length(), 3000))); 275 | } 276 | } 277 | 278 | private String callOllamaAPI(String prompt) throws IOException { 279 | MediaType JSON = MediaType.get("application/json; charset=utf-8"); 280 | Map requestMap = new HashMap<>(); 281 | requestMap.put("model", ollamaModel); 282 | requestMap.put("prompt", prompt); 283 | requestMap.put("stream", false); 284 | RequestBody body = RequestBody.create(JSON, objectMapper.writeValueAsString(requestMap)); 285 | 286 | Request request = new Request.Builder() 287 | .url(ollamaHost + "/api/generate") 288 | .post(body) 289 | .build(); 290 | 291 | try (Response response = httpClient.newCall(request).execute()) { 292 | if (!response.isSuccessful()) throw new IOException("请求失败: " + response); 293 | JsonNode jsonResponse = objectMapper.readTree(response.body().string()); 294 | return jsonResponse.get("response").asText(); 295 | } 296 | } 297 | 298 | private String processResponse(String response) { 299 | return response.replaceAll(".*?", ""); 300 | } 301 | } -------------------------------------------------------------------------------- /src/main/java/com/cyberscanner/StatusBar.java: -------------------------------------------------------------------------------- 1 | package com.cyberscanner; 2 | 3 | import javafx.animation.KeyFrame; 4 | import javafx.animation.KeyValue; 5 | import javafx.animation.Timeline; 6 | import javafx.geometry.Insets; 7 | import javafx.scene.control.Label; 8 | import javafx.scene.control.ProgressBar; 9 | import javafx.scene.effect.Glow; 10 | import javafx.scene.layout.HBox; 11 | import javafx.scene.layout.Priority; 12 | import javafx.scene.paint.Color; 13 | import javafx.scene.text.Font; 14 | import javafx.util.Duration; 15 | 16 | public class StatusBar extends HBox { 17 | private final Label statusLabel; 18 | private final Label progressLabel; 19 | private final ProgressBar progressBar; 20 | private Timeline progressTimeline; 21 | 22 | public StatusBar() { 23 | setSpacing(10); 24 | setPadding(new Insets(5)); 25 | getStyleClass().add("status-bar"); 26 | 27 | // 创建状态文本标签 28 | statusLabel = new Label("就绪"); 29 | statusLabel.setFont(Font.font("System", 14)); 30 | statusLabel.setTextFill(Color.valueOf("#4d4dff")); 31 | statusLabel.getStyleClass().add("status-label"); 32 | statusLabel.setWrapText(true); 33 | statusLabel.setMaxWidth(Double.MAX_VALUE); 34 | 35 | // 添加发光效果 36 | Glow glow = new Glow(0.6); 37 | statusLabel.setEffect(glow); 38 | 39 | // 创建进度条 40 | progressBar = new ProgressBar(); 41 | progressBar.setProgress(0); 42 | progressBar.setPrefWidth(200); 43 | progressBar.getStyleClass().add("status-progress"); 44 | progressBar.setVisible(false); 45 | 46 | // 创建进度百分比标签 47 | progressLabel = new Label("0%"); 48 | progressLabel.setFont(Font.font("System", 12)); 49 | progressLabel.setTextFill(Color.valueOf("#4d4dff")); 50 | progressLabel.setVisible(false); 51 | 52 | // 设置布局 53 | getChildren().addAll(statusLabel, progressBar, progressLabel); 54 | HBox.setHgrow(statusLabel, Priority.ALWAYS); 55 | } 56 | 57 | public void setStatus(String text) { 58 | statusLabel.setText(text); 59 | } 60 | 61 | public void setProgress(double progress) { 62 | // 确保进度条和百分比标签可见 63 | if (!progressBar.isVisible()) { 64 | progressBar.setVisible(true); 65 | progressLabel.setVisible(true); 66 | } 67 | 68 | // 取消之前的动画(如果存在) 69 | if (progressTimeline != null) { 70 | progressTimeline.stop(); 71 | } 72 | 73 | // 创建平滑动画 74 | progressTimeline = new Timeline( 75 | new KeyFrame(Duration.ZERO, 76 | new KeyValue(progressBar.progressProperty(), progressBar.getProgress())), 77 | new KeyFrame(Duration.millis(500), 78 | new KeyValue(progressBar.progressProperty(), progress)) 79 | ); 80 | 81 | progressTimeline.setOnFinished(event -> { 82 | // 更新百分比标签 83 | int percentage = (int) (progress * 100); 84 | progressLabel.setText(percentage + "%"); 85 | }); 86 | 87 | progressTimeline.play(); 88 | } 89 | 90 | public void reset() { 91 | statusLabel.setText("就绪"); 92 | progressBar.setProgress(0); 93 | progressLabel.setText("0%"); 94 | progressBar.setVisible(false); 95 | progressLabel.setVisible(false); 96 | } 97 | } -------------------------------------------------------------------------------- /src/main/resources/config.yaml: -------------------------------------------------------------------------------- 1 | # API配置 2 | api: 3 | type: ollama # 可选值: "deepseek" 或 "ollama" 4 | 5 | # DeepSeek API配置 6 | deepseek: 7 | # 官方默认API地址: "https://api.deepseek.com/v1/chat/completions" 8 | # 硅基流动:https://api.siliconflow.cn/v1/chat/completions 9 | url: "" 10 | api_key: "" 11 | # DeepSeek模型名称,官方默认模型: "deepseek-chat" 12 | # 硅基流动:deepseek-ai/DeepSeek-V3 13 | model: "" 14 | 15 | # Ollama API配置 16 | ollama: 17 | url: "http://x.x.x.x/api/chat" # Ollama API地址 18 | model: "qwen2.5:7b" # Ollama模型名称 19 | 20 | # 主题配色方案 21 | themes: 22 | dark: 23 | main_bg: "#1a1a2e" 24 | secondary_bg: "#16213e" 25 | text_color: "#e4e4e4" 26 | accent_color: "#4d4dff" 27 | border_color: "#7b2cbf" 28 | button_hover: "#00b4d8" 29 | button_pressed: "#0096c7" 30 | gradient_start: "#2b2b4b" 31 | gradient_end: "#1a1a2e" 32 | neon_glow: "#4d4dff" 33 | highlight: "#7b2cbf" 34 | 35 | light: 36 | main_bg: "#f5f5f5" 37 | secondary_bg: "#ffffff" 38 | text_color: "#333333" 39 | accent_color: "#2196f3" 40 | border_color: "#e0e0e0" 41 | button_hover: "#1976d2" 42 | button_pressed: "#1565c0" -------------------------------------------------------------------------------- /src/main/resources/styles/cyber-theme.css: -------------------------------------------------------------------------------- 1 | .main-layout { 2 | -fx-background-color: #282a36; 3 | -fx-text-fill: #f8f8f2; 4 | } 5 | 6 | .left-panel { 7 | -fx-background-color: #44475a; 8 | -fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.3), 10, 0, 0, 0); 9 | -fx-min-width: 300; 10 | -fx-pref-width: 300; 11 | } 12 | 13 | .cyber-button, .scan-button { 14 | -fx-background-color: #6272a4; 15 | -fx-text-fill: #f8f8f2; 16 | -fx-font-size: 14px; 17 | -fx-padding: 10px 20px; 18 | -fx-background-radius: 5px; 19 | } 20 | 21 | .cyber-button:hover, .scan-button:hover { 22 | -fx-background-color: #50fa7b; 23 | -fx-text-fill: #282a36; 24 | } 25 | 26 | .cyber-button:hover { 27 | -fx-background-color: #1a1a2e; 28 | -fx-border-color: #00ffff; 29 | -fx-effect: dropshadow(gaussian, #00ffff88, 20, 0, 0, 0); 30 | } 31 | 32 | .path-label { 33 | -fx-text-fill: #cc66ff; 34 | -fx-font-size: 10pt; 35 | -fx-font-family: 'Consolas'; 36 | -fx-padding: 5; 37 | } 38 | 39 | .mode-box { 40 | -fx-spacing: 5; 41 | -fx-padding: 10; 42 | -fx-border-color: #ff33ff; 43 | -fx-border-width: 1; 44 | -fx-border-radius: 5; 45 | -fx-effect: dropshadow(gaussian, #ff33ff44, 10, 0, 0, 0); 46 | } 47 | 48 | .mode-label { 49 | -fx-text-fill: #cc66ff; 50 | -fx-font-size: 12pt; 51 | -fx-font-family: 'Consolas'; 52 | -fx-font-weight: bold; 53 | } 54 | 55 | .jfx-radio-button { 56 | -fx-text-fill: #cc66ff; 57 | -fx-padding: 8; 58 | } 59 | 60 | .jfx-radio-button .radio { 61 | -fx-background-color: #2b0052; 62 | -fx-border-color: #ff33ff; 63 | -fx-border-width: 2; 64 | } 65 | 66 | .jfx-radio-button:selected .radio .dot { 67 | -fx-background-color: #ff33ff; 68 | } 69 | 70 | .jfx-check-box { 71 | -fx-text-fill: #cc66ff; 72 | -fx-padding: 8; 73 | } 74 | 75 | .jfx-check-box .box { 76 | -fx-background-color: #2b0052; 77 | -fx-border-color: #ff33ff; 78 | -fx-border-width: 2; 79 | } 80 | 81 | .jfx-check-box:selected .box .mark { 82 | -fx-background-color: #ff33ff; 83 | } 84 | 85 | .file-tree { 86 | -fx-background-color: #44475a; 87 | } 88 | 89 | .file-tree .tree-cell { 90 | -fx-background-color: #44475a; 91 | -fx-text-fill: #f8f8f2; 92 | } 93 | 94 | .file-tree .tree-cell:selected { 95 | -fx-background-color: #6272a4; 96 | } 97 | 98 | .file-tree .tree-cell { 99 | -fx-background-color: transparent; 100 | -fx-text-fill: #cc66ff; 101 | -fx-padding: 5; 102 | } 103 | 104 | .file-tree .tree-cell:hover { 105 | -fx-background-color: #2b0052; 106 | } 107 | 108 | .scan-button { 109 | -fx-background-color: #4d0099; 110 | -fx-text-fill: #ff33ff; 111 | -fx-border-color: #ff33ff; 112 | -fx-border-width: 2; 113 | -fx-padding: 15; 114 | -fx-font-size: 16pt; 115 | -fx-font-family: 'Consolas'; 116 | -fx-font-weight: bold; 117 | -fx-border-radius: 5; 118 | -fx-background-radius: 5; 119 | -fx-cursor: hand; 120 | -fx-effect: dropshadow(gaussian, #ff33ff44, 20, 0, 0, 0); 121 | } 122 | 123 | .scan-button:hover { 124 | -fx-background-color: #1a1a2e; 125 | -fx-effect: dropshadow(gaussian, #00ffff88, 25, 0, 0, 0); 126 | -fx-text-fill: #00ffff; 127 | } 128 | 129 | .scan-button:disabled { 130 | -fx-background-color: #2b0052; 131 | -fx-text-fill: #9933ff; 132 | -fx-border-color: #4d0099; 133 | -fx-effect: none; 134 | } 135 | 136 | .result-display { 137 | -fx-font-family: "JetBrains Mono", "Consolas", monospace; 138 | -fx-font-size: 14px; 139 | -fx-background-color: #282a36; 140 | -fx-text-fill: #f8f8f2; 141 | -fx-padding: 10px; 142 | } 143 | 144 | .result-display .content { 145 | -fx-background-color: #282a36; 146 | } 147 | 148 | .status-bar { 149 | -fx-padding: 5px; 150 | -fx-background-color: #44475a; 151 | -fx-text-fill: #f8f8f2; 152 | } 153 | 154 | .progress-bar { 155 | -fx-accent: #50fa7b; 156 | } 157 | 158 | .progress-bar .track { 159 | -fx-background-color: #44475a; 160 | } 161 | 162 | .progress-bar .track { 163 | -fx-background-color: #2b0052; 164 | } 165 | 166 | .progress-bar .bar { 167 | -fx-background-insets: 0; 168 | -fx-background-radius: 0; 169 | -fx-effect: dropshadow(gaussian, #ff33ff44, 10, 0, 0, 0); 170 | } 171 | 172 | /* 滚动条样式 */ 173 | .scroll-bar:vertical, 174 | .scroll-bar:horizontal { 175 | -fx-background-color: #1a0033; 176 | } 177 | 178 | .scroll-bar:vertical .thumb, 179 | .scroll-bar:horizontal .thumb { 180 | -fx-background-color: #4d0099; 181 | -fx-background-radius: 0; 182 | } 183 | 184 | .scroll-bar:vertical .thumb:hover, 185 | .scroll-bar:horizontal .thumb:hover { 186 | -fx-background-color: #6600cc; 187 | } 188 | 189 | .scroll-bar .increment-button, 190 | .scroll-bar .decrement-button { 191 | -fx-background-color: #2b0052; 192 | -fx-border-color: #4d0099; 193 | } 194 | 195 | .scroll-bar .increment-button:hover, 196 | .scroll-bar .decrement-button:hover { 197 | -fx-background-color: #4d0099; 198 | } 199 | 200 | /* 动画效果 */ 201 | .cyber-glow { 202 | -fx-effect: dropshadow(gaussian, #ff33ff44, 20, 0, 0, 0); 203 | -fx-transition: all 0.3s ease; 204 | } 205 | 206 | .cyber-glow:hover { 207 | -fx-effect: dropshadow(gaussian, #ff33ff88, 25, 0, 0, 0); 208 | } --------------------------------------------------------------------------------