diff --git a/README.md b/README.md index 6e045c4..380d16a 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,9 @@ | 模块 | 说明 | |------|------| -| **被动指纹识别** | 代理流量自动匹配 YAML 规则库,识别 Spring、Swagger、Nacos、Jenkins 等 | +| **被动指纹识别** | 代理流量自动匹配 YAML 规则库,识别 Spring、Swagger、Nacos、Jenkins 等;支持路径精确匹配 | | **Favicon Hash** | 自动采集网站图标,计算 MurmurHash3 / MD5,匹配已知应用指纹(兼容 Quake / FOFA) | +| **全局响应过滤** | 自定义过滤正则,响应体命中时自动跳过指纹识别(拦截伪 200 响应、重放攻击告警等) | | **递归目录扫描** | 基于 URL 路径层级 x 规则路径列表组合扫描 | | **Payload 处理** | 对请求进行自定义变换(前缀/后缀/正则替换/条件断言)后重放 | | **路径收集** | 自动提取代理流量中的一级路径,统计命中主机数,可导出为字典 | @@ -63,10 +64,11 @@ ### 指纹管理 -管理 YAML 格式的指纹规则,包含两个子面板: +管理 YAML 格式的指纹规则,包含三个子面板: -- **正则规则** — 基于 URL + 正则表达式匹配响应体,支持增删改查、导入导出、批量启用/禁用 +- **正则规则** — 基于 URL + 正则表达式匹配响应体,支持增删改查、导入导出、批量启用/禁用。被动匹配时要求请求路径与规则 `url` 字段一致(`url` 为 `/` 时仅匹配响应内容) - **Icon Hash 规则** — 基于 MurmurHash3 / MD5 匹配 Favicon +- **过滤规则** — 全局响应过滤器,正则命中响应体时跳过所有指纹识别(用于拦截"伪 200"响应、重放攻击告警等),支持增删改查 image ### 图标数据 @@ -196,6 +198,20 @@ Icon_Hash_List: `murmur_hash` 和 `md5` 至少填写一个,Hash 值与 Quake / FOFA 格式兼容。 +### 过滤规则 + +```yaml +Filter_List: + - name: Response Body 404 + re: '"status":404|"code":404' + loaded: true + - name: Replay Attack Detection + re: '"msg":"检测到重放攻击!"' + loaded: true +``` + +当响应体匹配任意已启用过滤规则的正则时,跳过该响应的所有指纹识别(正则匹配 + Icon Hash 匹配均跳过)。适用于某些系统始终返回 HTTP 200 但响应体内嵌真实错误码的场景。 + ## 编译构建 **环境要求:** JDK 17+ @@ -244,7 +260,7 @@ BurpExtender (Montoya API) ScanOrchestrator ├── FilterChain (Method → Host → Suffix) └── ScanStrategy[] - ├── PassiveFingerprintStrategy 被动指纹匹配 + ├── PassiveFingerprintStrategy 被动指纹匹配(支持路径精确匹配) ├── IconHashStrategy Favicon 采集 + Hash ├── RecursiveDirectoryScanStrategy 递归目录扫描 └── PayloadProcessingStrategy Payload 变换 @@ -254,7 +270,12 @@ RequestPipeline ├── AnalysisPool (10 threads) 指纹匹配 + Hash 计算 ├── DeduplicateFilter URL 去重 ├── QpsLimiter 限速 + ├── ResponseFilter 全局响应过滤器(Filter_List) └── ResultDispatcher 结果分发 + +YamlRuleEngine + ├── 正则规则匹配(re + state + url 路径比对) + └── 全局过滤拦截(isResponseFiltered → 命中跳过所有规则) ``` ## 致谢 diff --git a/extender/src/main/java/burp/BurpExtender.java b/extender/src/main/java/burp/BurpExtender.java index 3ff89a7..f013544 100644 --- a/extender/src/main/java/burp/BurpExtender.java +++ b/extender/src/main/java/burp/BurpExtender.java @@ -69,7 +69,9 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.regex.Matcher; @@ -143,6 +145,7 @@ private void initNewArchitecture() { // 1. 配置加载器(带缓存 + SafeConstructor) String yamlPath = Config.get("yaml_config_path"); mYamlConfigLoader = new YamlConfigLoader(yamlPath); + initDefaultFilterRules(); // 2. 规则引擎(仅 YAML) YamlRuleEngine yamlEngine = new YamlRuleEngine(mYamlConfigLoader); @@ -173,7 +176,8 @@ private void initNewArchitecture() { mApi, ruleEngine, TASK_THREAD_COUNT, ANALYSIS_THREAD_COUNT, qpsLimit, qpsDelay, - iconHashMatcher, mIconHashStore); + iconHashMatcher, mIconHashStore, + mYamlConfigLoader); // 4. 过滤器链 FaviconRegistry faviconRegistry = new FaviconRegistry(); @@ -351,6 +355,33 @@ public List provideMenuItems(ContextMenuEvent event) { mStatusRefresh.start(); } + /** + * 初始化默认全局过滤规则(仅当 Filter_List 为空时) + */ + private void initDefaultFilterRules() { + if (mYamlConfigLoader == null) return; + List> existing = mYamlConfigLoader.getFilterRules(); + if (!existing.isEmpty()) return; + + List> defaults = new ArrayList<>(); + Map rule1 = new HashMap<>(); + rule1.put("name", "Response Body 404"); + rule1.put("re", "\"status\":404|\"code\":404"); + rule1.put("loaded", true); + defaults.add(rule1); + + Map rule2 = new HashMap<>(); + rule2.put("name", "Replay Attack Detection"); + rule2.put("re", "\"msg\":\"检测到重放攻击!\""); + rule2.put("loaded", true); + defaults.add(rule2); + + Map config = mYamlConfigLoader.readConfig(); + config.put("Filter_List", defaults); + mYamlConfigLoader.writeConfig(config); + Logger.debug("Initialized default filter rules: %d rules", defaults.size()); + } + /** * 获取工作目录路径 */ diff --git a/extender/src/main/java/burp/tdou/fingerscan/config/YamlConfigLoader.java b/extender/src/main/java/burp/tdou/fingerscan/config/YamlConfigLoader.java index 83d4805..3ee44ab 100644 --- a/extender/src/main/java/burp/tdou/fingerscan/config/YamlConfigLoader.java +++ b/extender/src/main/java/burp/tdou/fingerscan/config/YamlConfigLoader.java @@ -15,6 +15,9 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * YAML 配置加载器(带缓存 + SafeConstructor) @@ -38,6 +41,11 @@ public class YamlConfigLoader { private volatile List cachedScanPaths; private volatile long cachedRulesLastModified = -1; + // 过滤规则缓存 + private volatile List> cachedEnabledFilters; + private volatile long cachedFiltersLastModified = -1; + private final ConcurrentHashMap filterPatternCache = new ConcurrentHashMap<>(); + public YamlConfigLoader(String configFilePath) { this.configFilePath = configFilePath != null ? configFilePath : getDefaultConfigPath(); } @@ -313,6 +321,129 @@ public void removeRule(String ruleId) { writeConfig(config); } + // ============================================================ + // 全局过滤规则(Response Body Filter) + // ============================================================ + + /** + * 获取所有过滤规则 + */ + @SuppressWarnings("unchecked") + public List> getFilterRules() { + Map config = readConfig(); + List> list = (List>) config.get("Filter_List"); + return list != null ? list : new ArrayList<>(); + } + + /** + * 获取已启用的过滤规则(带缓存) + */ + public List> getEnabledFilterRules() { + File file = new File(configFilePath); + long lastModified = file.exists() ? file.lastModified() : 0; + + if (cachedEnabledFilters != null && lastModified == cachedFiltersLastModified) { + return cachedEnabledFilters; + } + + List> rules = getFilterRules(); + List> enabled = new ArrayList<>(); + for (Map rule : rules) { + Object loaded = rule.get("loaded"); + if (loaded == null || Boolean.TRUE.equals(loaded)) { + enabled.add(rule); + } + } + + cachedEnabledFilters = enabled; + cachedFiltersLastModified = lastModified; + return enabled; + } + + /** + * 检查响应内容是否匹配任意已启用的过滤规则 + * + * @param responseBytes 响应原始字节 + * @return true 表示应跳过指纹识别 + */ + public boolean isResponseFiltered(byte[] responseBytes) { + if (responseBytes == null || responseBytes.length == 0) { + return false; + } + + List> filters = getEnabledFilterRules(); + if (filters.isEmpty()) { + return false; + } + + String responseStr = new String(responseBytes); + for (Map filter : filters) { + Object regexObj = filter.get("re"); + if (regexObj == null) continue; + String regex = regexObj.toString(); + if (regex.isEmpty()) continue; + try { + Pattern pattern = filterPatternCache.computeIfAbsent(regex, r -> { + try { + return Pattern.compile(r, Pattern.CASE_INSENSITIVE | Pattern.DOTALL); + } catch (Exception e) { + Logger.debug("Invalid filter regex: %s", r); + return null; + } + }); + if (pattern != null && pattern.matcher(responseStr).find()) { + return true; + } + } catch (Exception e) { + Logger.debug("Filter regex match error: %s", e.getMessage()); + } + } + return false; + } + + /** + * 添加过滤规则 + */ + public void addFilterRule(Map rule) { + Map config = readConfig(); + List> list = getFilterRules(); + list.add(rule); + config.put("Filter_List", list); + writeConfig(config); + cachedEnabledFilters = null; + filterPatternCache.clear(); + } + + /** + * 更新过滤规则(按索引) + */ + public void updateFilterRule(int index, Map updatedRule) { + Map config = readConfig(); + List> list = getFilterRules(); + if (index >= 0 && index < list.size()) { + list.set(index, updatedRule); + config.put("Filter_List", list); + writeConfig(config); + cachedEnabledFilters = null; + filterPatternCache.clear(); + } + } + + /** + * 删除过滤规则(按索引) + */ + public void removeFilterRule(int index) { + Map config = readConfig(); + List> list = getFilterRules(); + if (index >= 0 && index < list.size()) { + list.remove(index); + config.put("Filter_List", list); + writeConfig(config); + cachedEnabledFilters = null; + filterPatternCache.clear(); + } + } + /** * 强制清除缓存 */ @@ -322,6 +453,9 @@ public void invalidateCache() { cachedEnabledRules = null; cachedScanPaths = null; cachedRulesLastModified = -1; + cachedEnabledFilters = null; + cachedFiltersLastModified = -1; + filterPatternCache.clear(); } public String getConfigFilePath() { diff --git a/extender/src/main/java/burp/tdou/fingerscan/core/YamlConfigManager.java b/extender/src/main/java/burp/tdou/fingerscan/core/YamlConfigManager.java index 71f4ba2..c5ab8b8 100644 --- a/extender/src/main/java/burp/tdou/fingerscan/core/YamlConfigManager.java +++ b/extender/src/main/java/burp/tdou/fingerscan/core/YamlConfigManager.java @@ -167,6 +167,45 @@ public void removeIconHashRule(int index) { } } + // ============================================================ + // 全局过滤规则 CRUD + // ============================================================ + + @SuppressWarnings("unchecked") + public List> getFilterRules() { + Map yamlData = readYamlConfig(); + List> list = (List>) yamlData.get("Filter_List"); + return list != null ? list : new ArrayList<>(); + } + + public void addFilterRule(Map rule) { + Map yamlData = readYamlConfig(); + List> list = getFilterRules(); + list.add(rule); + yamlData.put("Filter_List", list); + writeYamlConfig(yamlData); + } + + public void updateFilterRule(int index, Map updatedRule) { + Map yamlData = readYamlConfig(); + List> list = getFilterRules(); + if (index >= 0 && index < list.size()) { + list.set(index, updatedRule); + yamlData.put("Filter_List", list); + writeYamlConfig(yamlData); + } + } + + public void removeFilterRule(int index) { + Map yamlData = readYamlConfig(); + List> list = getFilterRules(); + if (index >= 0 && index < list.size()) { + list.remove(index); + yamlData.put("Filter_List", list); + writeYamlConfig(yamlData); + } + } + /** * 从字符串解析YAML * @param yamlString YAML字符串 diff --git a/extender/src/main/java/burp/tdou/fingerscan/core/pipeline/RequestPipeline.java b/extender/src/main/java/burp/tdou/fingerscan/core/pipeline/RequestPipeline.java index 3e35811..a7bfbaa 100644 --- a/extender/src/main/java/burp/tdou/fingerscan/core/pipeline/RequestPipeline.java +++ b/extender/src/main/java/burp/tdou/fingerscan/core/pipeline/RequestPipeline.java @@ -14,6 +14,7 @@ import burp.tdou.fingerscan.core.ScanTask; import burp.tdou.fingerscan.core.rule.MatchResult; import burp.tdou.fingerscan.core.rule.RuleEngine; +import burp.tdou.fingerscan.config.YamlConfigLoader; import burp.tdou.fingerscan.core.iconhash.IconHashMatcher; import burp.tdou.fingerscan.core.iconhash.IconHashStore; @@ -45,6 +46,7 @@ public class RequestPipeline { private final MontoyaApi api; private final IconHashMatcher iconHashMatcher; private final IconHashStore iconHashStore; + private final YamlConfigLoader configLoader; private volatile ExecutorService requestPool; private volatile ExecutorService analysisPool; @@ -59,13 +61,15 @@ public class RequestPipeline { public RequestPipeline(MontoyaApi api, RuleEngine ruleEngine, int requestThreadCount, int analysisThreadCount, int qpsLimit, int qpsDelay, - IconHashMatcher iconHashMatcher, IconHashStore iconHashStore) { + IconHashMatcher iconHashMatcher, IconHashStore iconHashStore, + YamlConfigLoader configLoader) { this.api = api; this.ruleEngine = ruleEngine; this.requestThreadCount = requestThreadCount; this.analysisThreadCount = analysisThreadCount; this.iconHashMatcher = iconHashMatcher; this.iconHashStore = iconHashStore; + this.configLoader = configLoader; this.dedup = new DeduplicateFilter(); this.qpsLimiter = new QpsLimiter(qpsLimit, qpsDelay); @@ -107,8 +111,9 @@ private void submitAnalysis(ScanTask task) { pool.execute(() -> { try { + String requestPath = extractRequestPath(task.getReqResp()); List matches = ruleEngine.match( - task.getExistingRequest(), task.getExistingResponse()); + task.getExistingRequest(), task.getExistingResponse(), requestPath); // 无论是否匹配到指纹,都构建完整结果用于扫描记录 ScanResult.Builder builder = new ScanResult.Builder() @@ -142,6 +147,11 @@ private void submitIconHashAnalysis(ScanTask task) { return; } + // 全局响应过滤器:匹配到过滤规则则跳过 icon hash 识别 + if (configLoader != null && configLoader.isResponseFiltered(respBytes)) { + return; + } + // Parse response to extract body - use raw byte parsing instead of helpers String respStr = new String(respBytes); @@ -300,7 +310,7 @@ private ScanResult buildHttpResult(ScanTask task, HttpRequestResponse reqResp, H } byte[] reqBytes = request != null ? request.toByteArray().getBytes() : null; - List matches = ruleEngine.match(reqBytes, respBytes); + List matches = ruleEngine.match(reqBytes, respBytes, reqUrl); return new ScanResult.Builder() .task(task) @@ -352,18 +362,28 @@ private void fillResultFromReqResp(ScanResult.Builder builder, ScanTask task) { */ private String extractRequestPath(HttpRequest request) { if (request == null) return ""; - // Parse from raw request header line + // 优先使用 Montoya API 的 path(),返回纯路径(如 /swagger-ui.html) + String path = request.path(); + if (path != null && !path.isEmpty()) { + return path; + } + // 降级:从原始请求行解析 String reqStr = request.toString(); int lineEnd = reqStr.indexOf("\r\n"); if (lineEnd < 0) lineEnd = reqStr.indexOf("\n"); - if (lineEnd < 0) return request.path(); + if (lineEnd < 0) return ""; String reqLine = reqStr.substring(0, lineEnd); int start = reqLine.indexOf(' '); int end = reqLine.lastIndexOf(" HTTP/"); if (start >= 0 && end > start) { return reqLine.substring(start + 1, end); } - return request.path(); + return ""; + } + + private String extractRequestPath(HttpRequestResponse reqResp) { + if (reqResp == null || reqResp.request() == null) return ""; + return extractRequestPath(reqResp.request()); } private static int parseStatusCode(String statusLine) { diff --git a/extender/src/main/java/burp/tdou/fingerscan/core/rule/CompositeRuleEngine.java b/extender/src/main/java/burp/tdou/fingerscan/core/rule/CompositeRuleEngine.java index 95a4089..ffbe9d4 100644 --- a/extender/src/main/java/burp/tdou/fingerscan/core/rule/CompositeRuleEngine.java +++ b/extender/src/main/java/burp/tdou/fingerscan/core/rule/CompositeRuleEngine.java @@ -30,11 +30,11 @@ public void unregister(RuleEngine engine) { } @Override - public List match(byte[] request, byte[] response) { + public List match(byte[] request, byte[] response, String requestPath) { List allResults = new ArrayList<>(); for (RuleEngine engine : engines) { try { - List results = engine.match(request, response); + List results = engine.match(request, response, requestPath); if (results != null) { allResults.addAll(results); } diff --git a/extender/src/main/java/burp/tdou/fingerscan/core/rule/RuleEngine.java b/extender/src/main/java/burp/tdou/fingerscan/core/rule/RuleEngine.java index df44d32..708a732 100644 --- a/extender/src/main/java/burp/tdou/fingerscan/core/rule/RuleEngine.java +++ b/extender/src/main/java/burp/tdou/fingerscan/core/rule/RuleEngine.java @@ -11,11 +11,12 @@ public interface RuleEngine { /** * 对请求/响应数据进行指纹匹配 * - * @param request 请求原始字节(可为 null) - * @param response 响应原始字节(可为 null) + * @param request 请求原始字节(可为 null) + * @param response 响应原始字节(可为 null) + * @param requestPath 请求路径(可为 null),用于与规则 url 字段做路径比对 * @return 匹配结果列表(无匹配返回空列表,永不返回 null) */ - List match(byte[] request, byte[] response); + List match(byte[] request, byte[] response, String requestPath); /** * 获取本引擎需要扫描的路径列表(用于递归扫描策略) diff --git a/extender/src/main/java/burp/tdou/fingerscan/core/rule/YamlRuleEngine.java b/extender/src/main/java/burp/tdou/fingerscan/core/rule/YamlRuleEngine.java index 9bfdd62..8e1b5d3 100644 --- a/extender/src/main/java/burp/tdou/fingerscan/core/rule/YamlRuleEngine.java +++ b/extender/src/main/java/burp/tdou/fingerscan/core/rule/YamlRuleEngine.java @@ -3,6 +3,7 @@ import burp.tdou.common.log.Logger; import burp.tdou.fingerscan.config.YamlConfigLoader; +import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -30,7 +31,7 @@ public YamlRuleEngine(YamlConfigLoader configLoader) { } @Override - public List match(byte[] request, byte[] response) { + public List match(byte[] request, byte[] response, String requestPath) { if (response == null || response.length == 0) { return Collections.emptyList(); } @@ -40,6 +41,11 @@ public List match(byte[] request, byte[] response) { return Collections.emptyList(); } + // 全局响应过滤器:匹配到过滤规则则跳过指纹识别 + if (configLoader.isResponseFiltered(response)) { + return Collections.emptyList(); + } + String responseStr = new String(response); int statusCode = parseStatusCode(responseStr); List results = new ArrayList<>(); @@ -57,6 +63,11 @@ public List match(byte[] request, byte[] response) { continue; } + // 路径比对:url 为 null、空或 "/" 时不限制路径,否则要求请求路径匹配 + if (!matchPath(requestPath, getStringField(rule, "url"))) { + continue; + } + if (matchesRegex(responseStr, regex)) { results.add(MatchResult.fromYamlRule(name, regex)); } @@ -124,6 +135,34 @@ private int parseStatusCode(String responseStr) { return -1; } + /** + * 校验请求路径是否匹配规则的 url 字段 + * url 为 null、空或 "/" 时忽略路径条件(始终匹配,纯内容正则) + */ + private boolean matchPath(String requestPath, String ruleUrl) { + if (ruleUrl == null || ruleUrl.isEmpty() || "/".equals(ruleUrl)) { + return true; + } + if (requestPath == null || requestPath.isEmpty()) { + return false; + } + // 清理请求路径中的查询参数和锚点 + String cleanPath = requestPath.split("\\?")[0].split("#")[0]; + // 兼容完整 URL 格式(如 http://host/path),提取纯路径部分 + if (cleanPath.startsWith("http://") || cleanPath.startsWith("https://")) { + try { + cleanPath = new URL(cleanPath).getPath(); + } catch (Exception e) { + return false; + } + } + // 确保路径以 "/" 开头 + if (!cleanPath.startsWith("/")) { + cleanPath = "/" + cleanPath; + } + return cleanPath.equals(ruleUrl); + } + /** * 校验响应状态码是否匹配规则的 state 字段 * state 为 null、空、"0" 时忽略状态码条件(始终匹配) diff --git a/extender/src/main/java/burp/tdou/fingerscan/ui/tab/FingerprintPanel.java b/extender/src/main/java/burp/tdou/fingerscan/ui/tab/FingerprintPanel.java index 0a45806..7d6f03c 100644 --- a/extender/src/main/java/burp/tdou/fingerscan/ui/tab/FingerprintPanel.java +++ b/extender/src/main/java/burp/tdou/fingerscan/ui/tab/FingerprintPanel.java @@ -10,6 +10,7 @@ import burp.tdou.fingerscan.common.Config; import burp.tdou.fingerscan.core.YamlConfigManager; +import burp.tdou.fingerscan.ui.widget.FilterRuleDialog; import burp.tdou.fingerscan.ui.widget.FingerprintRuleDialog; import burp.tdou.fingerscan.ui.widget.IconHashRuleDialog; import burp.tdou.fingerscan.ui.widget.TestRuleDialog; @@ -54,6 +55,12 @@ public class FingerprintPanel extends JPanel implements ActionListener, KeyListe private TableRowSorter iconHashTableSorter; private JTextField iconHashSearchField; private JLabel iconHashCountLabel; + + // 过滤规则 UI组件 + private JTable filterTable; + private DefaultTableModel filterTableModel; + private TableRowSorter filterTableSorter; + private JLabel filterCountLabel; // 表格列名 private static final String[] COLUMN_NAMES = { @@ -63,6 +70,10 @@ public class FingerprintPanel extends JPanel implements ActionListener, KeyListe private static final String[] ICON_HASH_COLUMN_NAMES = { "名称", "MurmurHash3", "MD5", "类型", "描述" }; + + private static final String[] FILTER_COLUMN_NAMES = { + "名称", "正则表达式", "启用" + }; /** * 构造函数 @@ -74,6 +85,7 @@ public FingerprintPanel(YamlConfigManager configManager) { initializeUI(); loadFingerprintRules(); loadIconHashRules(); + loadFilterRules(); } public void setOnReloadCallback(Runnable callback) { @@ -108,6 +120,9 @@ private void initializeUI() { // Tab 2: Icon Hash 规则 tabbedPane.addTab("Icon Hash 规则", createIconHashPanel()); + // Tab 3: 过滤规则 + tabbedPane.addTab("过滤规则", createFilterPanel()); + add(tabbedPane, BorderLayout.CENTER); } @@ -497,6 +512,15 @@ public void actionPerformed(ActionEvent e) { case "icon-hash-clear-search": clearIconHashSearch(); break; + case "filter-add": + addFilterRule(); + break; + case "filter-edit": + editFilterRule(); + break; + case "filter-delete": + deleteFilterRule(); + break; } } @@ -522,6 +546,7 @@ private void browseConfigFile() { private void reloadConfig() { loadFingerprintRules(); loadIconHashRules(); + loadFilterRules(); if (onReloadCallback != null) { onReloadCallback.run(); } @@ -914,6 +939,145 @@ private void clearIconHashSearch() { iconHashTableSorter.setRowFilter(null); } + // ============================================================ + // 全局过滤规则管理 + // ============================================================ + + private JPanel createFilterPanel() { + JPanel panel = new JPanel(new BorderLayout()); + + filterTableModel = new DefaultTableModel(FILTER_COLUMN_NAMES, 0) { + @Override + public boolean isCellEditable(int row, int column) { + return column == 2; + } + + @Override + public Class getColumnClass(int columnIndex) { + return columnIndex == 2 ? Boolean.class : String.class; + } + }; + + filterTable = new JTable(filterTableModel); + filterTableSorter = new TableRowSorter<>(filterTableModel); + filterTable.setRowSorter(filterTableSorter); + filterTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + + filterTable.getModel().addTableModelListener(e -> { + if (e.getColumn() == 2) { + int row = e.getFirstRow(); + updateFilterRuleEnabledStatus(row); + } + }); + + int[] widths = {200, 350, 60}; + for (int i = 0; i < widths.length && i < filterTable.getColumnCount(); i++) { + filterTable.getColumnModel().getColumn(i).setPreferredWidth(widths[i]); + } + + JScrollPane scrollPane = new JScrollPane(filterTable); + + // 按钮面板 + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + buttonPanel.setBorder(new EmptyBorder(5, 0, 0, 0)); + JButton addBtn = new JButton("添加"); + addBtn.setActionCommand("filter-add"); + addBtn.addActionListener(this); + buttonPanel.add(addBtn); + JButton editBtn = new JButton("编辑"); + editBtn.setActionCommand("filter-edit"); + editBtn.addActionListener(this); + buttonPanel.add(editBtn); + JButton deleteBtn = new JButton("删除"); + deleteBtn.setActionCommand("filter-delete"); + deleteBtn.addActionListener(this); + buttonPanel.add(deleteBtn); + + buttonPanel.add(new JSeparator(SwingConstants.VERTICAL)); + + filterCountLabel = new JLabel("0"); + buttonPanel.add(new JLabel("规则数量: ")); + buttonPanel.add(filterCountLabel); + + JPanel bottomPanel = new JPanel(new BorderLayout()); + bottomPanel.add(buttonPanel, BorderLayout.NORTH); + + panel.add(scrollPane, BorderLayout.CENTER); + panel.add(bottomPanel, BorderLayout.SOUTH); + return panel; + } + + public void loadFilterRules() { + filterTableModel.setRowCount(0); + List> rules = configManager.getFilterRules(); + for (Map rule : rules) { + Object[] rowData = { + str(rule.get("name")), + str(rule.get("re")), + rule.get("loaded") == null || Boolean.TRUE.equals(rule.get("loaded")) + }; + filterTableModel.addRow(rowData); + } + filterCountLabel.setText(String.valueOf(rules.size())); + } + + private void updateFilterRuleEnabledStatus(int row) { + if (row < 0 || row >= filterTableModel.getRowCount()) return; + List> rules = configManager.getFilterRules(); + if (row < rules.size()) { + Map rule = rules.get(row); + rule.put("loaded", filterTableModel.getValueAt(row, 2)); + configManager.updateFilterRule(row, rule); + } + } + + private void addFilterRule() { + FilterRuleDialog dialog = new FilterRuleDialog( + (JFrame) SwingUtilities.getWindowAncestor(this), "添加过滤规则", null); + Map newRule = dialog.showDialog(); + if (newRule != null) { + configManager.addFilterRule(newRule); + loadFilterRules(); + onReloadCallback.run(); // 刷新运行时缓存 + } + } + + private void editFilterRule() { + int selectedRow = filterTable.getSelectedRow(); + if (selectedRow < 0) { + JOptionPane.showMessageDialog(this, "请先选择要编辑的规则", "提示", JOptionPane.WARNING_MESSAGE); + return; + } + int modelRow = filterTable.convertRowIndexToModel(selectedRow); + List> rules = configManager.getFilterRules(); + if (modelRow >= rules.size()) return; + + FilterRuleDialog dialog = new FilterRuleDialog( + (JFrame) SwingUtilities.getWindowAncestor(this), "编辑过滤规则", rules.get(modelRow)); + Map edited = dialog.showDialog(); + if (edited != null) { + configManager.updateFilterRule(modelRow, edited); + loadFilterRules(); + onReloadCallback.run(); + } + } + + private void deleteFilterRule() { + int selectedRow = filterTable.getSelectedRow(); + if (selectedRow < 0) { + JOptionPane.showMessageDialog(this, "请先选择要删除的规则", "提示", JOptionPane.WARNING_MESSAGE); + return; + } + int result = JOptionPane.showConfirmDialog(this, "确定要删除选中的过滤规则吗?", "确认删除", + JOptionPane.YES_NO_OPTION); + if (result == JOptionPane.YES_OPTION) { + int modelRow = filterTable.convertRowIndexToModel(selectedRow); + configManager.removeFilterRule(modelRow); + loadFilterRules(); + onReloadCallback.run(); + } + } + @Override public void keyTyped(KeyEvent e) { // 不需要实现 diff --git a/extender/src/main/java/burp/tdou/fingerscan/ui/widget/FilterRuleDialog.java b/extender/src/main/java/burp/tdou/fingerscan/ui/widget/FilterRuleDialog.java new file mode 100644 index 0000000..c6b456e --- /dev/null +++ b/extender/src/main/java/burp/tdou/fingerscan/ui/widget/FilterRuleDialog.java @@ -0,0 +1,142 @@ +package burp.tdou.fingerscan.ui.widget; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Pattern; + +/** + * 全局过滤规则编辑对话框 + * 用于添加/编辑响应内容过滤器规则(匹配到则跳过指纹识别) + */ +public class FilterRuleDialog extends JDialog implements ActionListener { + + private Map originalRule; + private Map resultRule; + + private JTextField nameField; + private JCheckBox enabledCheckBox; + private JTextArea regexArea; + + public FilterRuleDialog(JFrame parent, String title, Map rule) { + super(parent, title, true); + this.originalRule = rule; + this.resultRule = null; + initializeUI(); + if (rule != null) { + populateFields(rule); + } + pack(); + setLocationRelativeTo(parent); + setMinimumSize(new Dimension(450, 250)); + } + + private void initializeUI() { + JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); + mainPanel.setBorder(new EmptyBorder(10, 10, 10, 10)); + + JPanel formPanel = new JPanel(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.insets = new Insets(5, 5, 5, 5); + gbc.anchor = GridBagConstraints.WEST; + + // 名称 + gbc.gridx = 0; gbc.gridy = 0; + formPanel.add(new JLabel("名称:*"), gbc); + gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL; gbc.weightx = 1.0; + nameField = new JTextField(25); + formPanel.add(nameField, gbc); + + // 启用 + gbc.gridx = 0; gbc.gridy = 1; + formPanel.add(new JLabel("启用:"), gbc); + gbc.gridx = 1; + enabledCheckBox = new JCheckBox("启用此过滤规则"); + enabledCheckBox.setSelected(true); + formPanel.add(enabledCheckBox, gbc); + + // 正则表达式 + gbc.gridx = 0; gbc.gridy = 2; gbc.fill = GridBagConstraints.NONE; gbc.anchor = GridBagConstraints.NORTHWEST; + formPanel.add(new JLabel("正则表达式:*"), gbc); + gbc.gridx = 1; gbc.fill = GridBagConstraints.BOTH; gbc.weighty = 1.0; + regexArea = new JTextArea(4, 25); + regexArea.setLineWrap(true); + regexArea.setWrapStyleWord(true); + JScrollPane scrollPane = new JScrollPane(regexArea); + scrollPane.setBorder(BorderFactory.createTitledBorder("匹配响应内容的正则表达式")); + formPanel.add(scrollPane, gbc); + + mainPanel.add(formPanel, BorderLayout.CENTER); + + // 按钮 + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + JButton okButton = new JButton("确定"); + okButton.setActionCommand("ok"); + okButton.addActionListener(this); + buttonPanel.add(okButton); + JButton cancelButton = new JButton("取消"); + cancelButton.setActionCommand("cancel"); + cancelButton.addActionListener(this); + buttonPanel.add(cancelButton); + mainPanel.add(buttonPanel, BorderLayout.SOUTH); + + add(mainPanel); + setDefaultCloseOperation(DISPOSE_ON_CLOSE); + setResizable(true); + } + + private void populateFields(Map rule) { + nameField.setText(rule.get("name") != null ? rule.get("name").toString() : ""); + Object loaded = rule.get("loaded"); + enabledCheckBox.setSelected(loaded == null || Boolean.TRUE.equals(loaded)); + regexArea.setText(rule.get("re") != null ? rule.get("re").toString() : ""); + } + + private boolean validateInput() { + if (nameField.getText().trim().isEmpty()) { + JOptionPane.showMessageDialog(this, "请输入规则名称!", "输入错误", JOptionPane.ERROR_MESSAGE); + return false; + } + if (regexArea.getText().trim().isEmpty()) { + JOptionPane.showMessageDialog(this, "请输入正则表达式!", "输入错误", JOptionPane.ERROR_MESSAGE); + return false; + } + try { + Pattern.compile(regexArea.getText().trim()); + } catch (Exception e) { + JOptionPane.showMessageDialog(this, "正则表达式格式错误:" + e.getMessage(), "输入错误", JOptionPane.ERROR_MESSAGE); + return false; + } + return true; + } + + private Map createRule() { + Map rule = new HashMap<>(); + rule.put("name", nameField.getText().trim()); + rule.put("re", regexArea.getText().trim()); + rule.put("loaded", enabledCheckBox.isSelected()); + return rule; + } + + public Map showDialog() { + setVisible(true); + return resultRule; + } + + @Override + public void actionPerformed(ActionEvent e) { + if ("ok".equals(e.getActionCommand())) { + if (validateInput()) { + resultRule = createRule(); + dispose(); + } + } else if ("cancel".equals(e.getActionCommand())) { + resultRule = null; + dispose(); + } + } +}