简介:一套基于Java的漏洞扫描系统完整项目,面向Java工程师与网络安全入门者,解决从零搭建漏洞扫描工具的核心需求,覆盖端口扫描、服务识别、漏洞指纹匹配、扫描策略配置与报告生成等关键环节。资源包共927个文件,大小33.07MB,主体为604个.nse脚本与146个Lua文件,主要承担漏洞探测与指纹匹配;同时提供Java源码、class字节码、jar依赖包及XML/TXT配置文档,用于扫描引擎构建、规则加载和结果输出。目前已有182人学习下载。借助这套资源,读者可深入理解Java Socket网络通信、多线程并发扫描、漏洞数据库集成、SSL/TLS指纹识别等实现思路;项目还包含Nmap服务探测数据库、SSL指纹库和端口服务列表等数据文件,便于直接运行,也可用于课程设计、毕业设计或安全测试工具,是研究漏洞扫描系统内部机制的高质量素材。
1. 基于Java的漏洞扫描系统.zip:比“能跑”更值钱的是扫描引擎怎么拆
当你解压一个基于Java的漏洞扫描系统.zip,第一反应可能是 mvn spring-boot:run,看它能不能起来。可一旦跑通,你会发现问题不在启动,而在于扫描引擎怎么拆:任务调度、网络探测、漏洞判定、结果沉淀。我见过太多把端口扫描和漏洞匹配写进一个大 for 循环的毕设,扫 100 个 IP 就内存吃满、误报不断。这篇文章不替某个 zip 做源码讲解,而是把这个标题背后最常见的工程方案拆给你看:从 Java 线程池、Socket 探测、指纹匹配到报告输出,每一层可复用的代码和参数都写出来,并标出容易翻车的地方。适合两类人:用 Java 做安全工具开发的工程师,以及想拿这个方向做面试项目的 Java 学习者。
2. 先立骨架:把漏洞扫描拆成调度、探测、判定三层
2.1 为什么不用脚本而用 Java 重写调度层
做漏洞扫描,最快的原型是 Shell 或 Python 脚本:nc 探测端口、curl 拉 Banner、grep 匹配漏洞规则。为什么还要用 Java 重写?两个核心原因:并发和状态管理。Python 脚本的并发往往靠多进程,子进程通信、资源回收都是麻烦;Java 从语言层面给你线程池、Future、CompletableFuture,可以精确控制扫描的速率和并发度。第二个原因是集成——企业里已有的资产管理系统、工单系统大多跑在 JVM 上,用 Java 写的扫描器可以作为一个 Maven 模块直接嵌进去,而不是在 Jenkins 上额外维护一套 Python 环境。
另外,Java 的类型系统在构造扫描任务时很有用。你有一个目标 IP、一个端口列表、一组超时策略,如果全用 Map<String,Object> 塞来塞去,写起来快,但跑起来很容易出现 ClassCastException。定义成带字段的类,后续加规则也好、加报告字段也好,编译器能帮你拦掉一批低级错误。这也是为什么很多一线团队在重写安全工具时,默认选 Java 而不是脚本。
常被忽略的一点是定时触发。漏洞扫描不能只跑一次,通常需要固定节奏,比如每周对全量资产扫一遍。Java 这边有成熟的定时任务框架:Spring Task 的 @Scheduled、Quartz 的 CronTrigger,都可以让扫描任务自动排队。注意如果你的扫描器是独立进程而不是 Spring Boot 应用,用 ScheduledExecutorService 也够,但要处理 JVM 退出时的任务持久化,这业务上并不简单,很容易丢任务。所以我的习惯是:调度器只负责触发,扫描任务本身写进数据库,靠状态字段驱动,而不是靠内存里的 Map 保存待扫队列。
这里展开说下什么叫“状态字段驱动”。最常见的一张表是 scan_job,字段包括 job_id、target_range、scan_type、status(pending/running/finished/failed)、create_time、finish_time。调度器到点后只做一件事:把新的 job 插入数据库,状态置为 pending。然后一组 worker 线程轮询 pending 的 job,取出来拆分成 ScanTask 丢进线程池。这样 JVM 重启,未完成任务仍然在数据库里,不会凭空消失。
2.2 任务模型与线程池参数:从 ScanTask 到 ExecutorService 的最小代码
先定义扫描任务。一个任务代表“对某个 IP 的某个端口做一次探测”,包含重试次数和超时参数。不过实际中更常见的粒度是“一个目标主机的全端口扫描”,但为了并发控制精细,我一般拆到单端口,这样某几个慢端口不会阻塞同一主机的其他端口。
public class ScanTask implements Runnable { private final String host; private final int port; private final int timeoutMs; private final int retries; private final String scanId; public ScanTask(String host, int port, int timeoutMs, int retries, String scanId) { this.host = host; this.port = port; this.timeoutMs = timeoutMs; this.retries = retries; this.scanId = scanId; } @Override public void run() { // 具体探测逻辑在第三章实现,这里先做超时包裹 try { boolean open = PortProber.probe(host, port, timeoutMs); if (open) { ScanResultHolder.record(scanId, host, port, "open"); } } catch (Exception e) { ScanResultHolder.record(scanId, host, port, "error:" + e.getMessage()); } } }这段代码把“要做什么”和“怎么做”分离了。ScanTask 只负责携带参数和结果落点,真正的 Socket 逻辑放在 PortProber 里。ScanResultHolder 是一个内部缓冲,可以用 ConcurrentHashMap 按 scanId 存结果,避免在线程里直接操作共享的 ArrayList。
线程池不能乱写。我见过直接用 Executors.newFixedThreadPool(1000) 的,瞬间把系统文件描述符打满。正确姿势是手动 new ThreadPoolExecutor,显式指定队列和拒绝策略:
int cores = Runtime.getRuntime().availableProcessors(); ThreadPoolExecutor executor = new ThreadPoolExecutor( cores * 2, cores * 4, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(200), new ThreadFactoryBuilder().setNameFormat("scan-pool-%d").build(), new ThreadPoolExecutor.CallerRunsPolicy() );参数含义:核心线程数给 CPU 数的两倍,是因为扫描任务大部分时间阻塞在网络上,不需要占满 CPU,核心线程多一些能让更多连接并行。最大线程数给四倍,防止极端情况下把内存打爆。队列用有界队列,容量 200,一旦挤满就触发拒绝策略。拒绝策略我选 CallerRunsPolicy,即把多余任务退回提交者线程执行,这样不会丢任务,同时天然给系统一个背压信号。
如果你用 Java 8,ThreadFactoryBuilder 来自 Guava;不想引依赖就实现 ThreadFactory,写个带前缀的线程工厂也很简单。这里多啰嗦一句:线程池里线程名一定要起好,不然线程 dump 时你看到的全是 pool-3-thread-1,排查问题想哭。另一个容易踩的坑是线程池用了无界队列,任务量一大,队列里堆几万个 ScanTask,每个都持有 IP 和端口字符串,内存几十兆起步。用有界队列加上拒绝策略,是最稳妥的做法。
2.3 扫描速率控制:令牌桶在调度层的实现
不管线程池参数调得多好,真正推到生产环境时,最容易被安全设备盯上的就是“扫描速率失控”。我见过一个并发 500 的任务,把目标机房的千兆带宽打满,最后对方运维直接拔线。要控制速率,最实用的工具是 Guava 的 RateLimiter,或者你自己写一个简单的令牌桶。
public class ScanRateLimiter { private final Semaphore semaphore; private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); private final int permitsPerSecond; public ScanRateLimiter(int permitsPerSecond) { this.permitsPerSecond = permitsPerSecond; this.semaphore = new Semaphore(permitsPerSecond); scheduler.scheduleAtFixedRate(() -> semaphore.release(permitsPerSecond - semaphore.availablePermits()), 1, 1, TimeUnit.SECONDS); } public void acquire() { semaphore.acquireUninterruptibly(); } }这段代码每秒补充固定数量的许可,ScanTask 在真正发起 Socket 连接前先 acquire()。速率设置要根据目标资产规模调整:扫描内网 50 台机器,每 IP 每秒 10 个包足够;扫描公网域名,建议降到每 IP 每秒 2-3 个包。别小看这个参数,它决定你的扫描器是被当成正常审计流量,还是被 IDS 重点标记。令牌桶的另一个好处是天然支持突发,秒级突发几个包不会触发告警,长期匀速才是最安全的。
3. 探测层落地:端口扫描与 Banner 抓取的两个核心模块
3.1 TCP Connect 端口扫描:超时、并发、结果过滤的调参细节
端口扫描是漏洞扫描的地基,端口不对,后面所有指纹和规则都白搭。Java 实现一个 TCP Connect 扫描非常直接,就是新建 Socket 去 connect,在规定时间内连上说明端口开放。这里的核心变量是超时时间:太短,网络抖动一下就会漏报;太长,几百个端口扫下来整体耗时会拖垮调度。
public class PortProber { public static boolean probe(String host, int port, int timeoutMs) { try (Socket socket = new Socket()) { socket.connect(new InetSocketAddress(host, port), timeoutMs); socket.setSoTimeout(timeoutMs); return true; } catch (IOException e) { return false; } } public static List<Integer> scanRange(String host, int startPort, int endPort, int timeoutMs) { List<Integer> openPorts = Collections.synchronizedList(new ArrayList<>()); CountDownLatch latch = new CountDownLatch(endPort - startPort + 1); // 配合线程池逐个提交,或使用 parallelStream for (int p = startPort; p <= endPort; p++) { executor.submit(() -> { try { if (probe(host, p, timeoutMs)) { openPorts.add(p); } } finally { latch.countDown(); } }); } try { latch.await(2, TimeUnit.MINUTES); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return openPorts; } }注意两点:一是 try-with-resources 确保 Socket 一定关闭,否则连接多了会耗尽文件描述符。二是 connect 的 timeoutMs 和 SO_TIMEOUT 是两回事,前者管建立连接,后者管后续读取。这里 setSoTimeout 是为了后面 Banner 读取不被卡死。
超时参数的江湖经验是:同机房内网,800ms 够用;跨公网扫描,建议 1500ms 到 3000ms。如果你想扫描的端口不是常见的 1-65535,而是针对特定服务比如 3306、6379、9200,可以把超时降到 300ms,毕竟这些内部服务暴露在公网本来就值得怀疑。扫描结果里经常会混入一些“半开”端口——防火墙接受 TCP 握手但不给数据。区分方法是连接建立后立刻写一个空数据包,然后读一次流,读不到数据并且连接被 reset,多半是防火墙。这个技巧在结果过滤里很有用。
并发度怎么定?前面线程池最大线程数给了 cores*4,但实际单机扫描 1000 个端口时,系统默认的 socket 连接数上限(Linux 下 /proc/sys/net/core/somaxconn 和 ulimit -n)会先打满。经验值:单进程同时保持 200 个 TCP 连接比较安全,再多容易把目标服务打挂,也容易被 IDS 特征匹配到。控制手段很简单,信号量或者线程池容量。
3.2 服务指纹识别:NIO 与正则匹配的取舍
端口开了之后,下一步是知道背后跑的是什么服务。最常见的探测是发一个协议触发包,比如 HTTP 服务就发 "GET / HTTP/1.0\r\n\r\n",SSH 服务等它主动弹 Banner。Java 里做这个,传统 IO 就能干,但你要处理“服务不响应”和“响应半截”的情况。NIO 能批量管理成百上千个非阻塞连接,可代价是代码复杂度和调试难度直线上升。我的看法是:如果扫描规模在几千个 IP 以内,用普通 Socket + 线程池就够了;只有到了资产数十万,才值得引入 Netty 或 NIO 重写探测层。
public class BannerGrabber { public static String grab(String host, int port, int timeoutMs) { try (Socket socket = new Socket()) { socket.connect(new InetSocketAddress(host, port), timeoutMs); socket.setSoTimeout(timeoutMs); OutputStream out = socket.getOutputStream(); out.write("GET / HTTP/1.0\r\nHost: " + host + "\r\n\r\n".getBytes(StandardCharsets.UTF_8)); out.flush(); BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); StringBuilder banner = new StringBuilder(); char[] buffer = new char[1024]; int len; long deadline = System.currentTimeMillis() + timeoutMs; while ((len = reader.read(buffer)) != -1 && System.currentTimeMillis() < deadline) { banner.append(buffer, 0, len); if (banner.length() > 8192) break; // 防止被大响应包拖死 } return banner.toString(); } catch (IOException e) { return ""; } } }这里有个坑:很多服务要等先发数据才回 Banner,比如 MySQL、Redis,它们不一定理解 HTTP 请求。所以完整的指纹探测要对常见端口准备不同的触发包,比如 MySQL 用它的握手包,Redis 直接发 "PING\r\n"。这个触发包与端口的映射表,是扫描器里最占维护精力的部分。别想着用一套万能请求通吃,不现实。
正则匹配 Banner 时,建议不要用贪婪匹配。比如你想提取 SSH 版本号,用 "(?i)SSH-([\d.]+)" 而不是 "SSH-(.*)"。前者能准确抓版本,后者会把整个 Banner 都吞进去,影响后续漏洞版本匹配。另外,Banner 里经常带 ANSI 颜色码、乱码控制字符,匹配前先清理一下非可见字符,能显著降低误报。清理可以用 banner.replaceAll("[\x00-\x1f\x7f]", ""),但这会去掉换行,如果需要按行匹配就先 split 之后再处理。
3.3 UDP 扫描:为什么大多数 Java 扫描器不做
端口扫描还有一个 UDP 维度,但绝大多数基于 Java 的漏洞扫描系统并不实现 UDP 深度探测。原因是 UDP 无连接,你发一个包过去,对端可能不理你,也可能返回一个 ICMP 不可达,Java 标准库拿不到 ICMP 信息,所以判断“端口是否开放”很难。常见的做法是用 UDP Socket 发送探测包后等待响应,等不到就算关闭。这个逻辑简单,但误报率很高。如果你的项目标题里明确要支持 UDP 服务(比如 DNS、SNMP、TFTP),我建议不要自己造轮子,直接调外部工具如 nmap 的结果导入扫描器,Java 端只负责解析输出。这不是偷懒,而是把专业的事交给专业工具,避免你花两周调一个永远不准的 UDP 扫描器。
4. 判定层与结果沉淀:规则匹配与报告生成
4.1 用 Java 规则引擎做漏洞匹配:Map、正则、优先级
拿到服务 Banner 和端口信息后,进入判定层。很多初学者一上来就引入 Drools、Easy Rules,实际上你手头只有几百条漏洞规则时,用 Java 的 Pattern 和 HashMap 就是最高效、最容易调试的方案。规则引擎只有在规则数量上万、需要复杂的条件组合和冲突消解时才有优势。
规则定义一个不可变类:
public class VulnRule { private final String vulnId; // 例如 CVE-2021-41773 private final String serviceName; private final String versionPattern; // 正则,从 Banner 提取版本 private final String matchPattern; // 正则,直接匹配 Banner private final String[] affectedVersions; // 受影响版本列表 private final int severity; // 1-5 public boolean matches(String banner) { if (matchPattern != null && !Pattern.compile(matchPattern, Pattern.CASE_INSENSITIVE) .matcher(banner).find()) { return false; } if (versionPattern != null) { Matcher matcher = Pattern.compile(versionPattern).matcher(banner); if (!matcher.find()) return false; String version = matcher.group(1); for (String affected : affectedVersions) { if (versionMatches(version, affected)) { return true; } } return false; } return true; } }这里的关键是 versionMatches 方法。直接 equals 比较版本号会漏,比如 Banner 写 "Apache 2.4.49" 而规则写 "2.4.49",中间带空格。我一般会写一个简单的比较器:先把版本拆成数字段,再按段比较,支持通配符和区间。不要用什么语义化版本库,漏洞版本范围往往包含 "<= 2.4.49" 这类标记,自己维护一个 30 行的小工具类更可控。
规则匹配的顺序要讲究优先级。端口 80/443 上的漏洞规则很可能有几十条,如果每来一个 Banner 就把所有规则全跑一遍,性能没问题,但会有“重复命中”和“子规则覆盖父规则”的问题。我的做法是:按 serviceName 建 Map<String, List >,先过滤服务,再按 severity 降序排列,命中的第一条加入结果,同时记录一个“已覆盖”的标记,后面更低优先级的规则如果命中同一个 CVE 组,自动跳过。这样报告里不会出现同一漏洞被报三次的尴尬。
规则从哪来?如果你是个人项目,可以手工从 NVD、CNVD 维护一小部分高危漏洞规则;如果是生产系统,建议接入商业或开源的漏洞库,然后转换成内部规则格式。手工维护规则时,一定要留一个规则来源字段,方便到时候回溯。我见过有人把网上乱抄的规则塞进生产库,误报率直接飙到 80%,最后只能一条条翻日志删规则。
4.2 扫描结果落库与 JSON 报告输出
扫描结果不能只放在内存。生产环境通常要落库,MySQL 或 PostgreSQL 建一张 scan_result 表,字段大概有:scan_id、host、port、service_name、vuln_id、severity、matched_banner、scan_time。写库可以用 Spring Data JPA,但如果你不想引整套框架,用 JDBC + 一个线程安全的 Batch 插入也能凑合。注意批量插入时控制 batch size,200 条一批比较稳,太多容易撑爆数据库连接的 prepared statement。
public class ScanResult { private String scanId; private String host; private int port; private String serviceName; private String vulnId; private int severity; private String matchedBanner; private long scanTime; public String toJson() { // 使用 Jackson ObjectMapper,这里省略依赖导入 ObjectMapper mapper = new ObjectMapper(); try { return mapper.writeValueAsString(this); } catch (JsonProcessingException e) { return "{\"error\":\"serialize failed\"}"; } } }如果你不想引 Jackson,手动拼 JSON 也可以,但一定要处理转义,Banner 里出现双引号和反斜杠是常事。我建议至少用 Jackson 或 Gson,手动拼字符串的翻车率实在太高。报告输出上,我一般同时生成 JSON 和 CSV 两种格式:JSON 给下游系统对接,CSV 给人看,用 Excel 打开后可以透视排序。CSV 的坑是逗号和换行,Banner 里都有,输出时必须用引号包裹并按 RFC 4180 转义双引号。
报告里最好带上落败的规则匹配记录,也就是“哪些规则候选但没匹配上”。这看起来浪费空间,但对后续规则调优是救命的数据。你在线上看到某个服务版本特别多,却一直没有命中漏洞,有了这个候选记录就能快速发现正则写窄了。
4.3 结果去重与聚合:别让报告淹没运维
漏洞扫描的原始记录是“主机-端口-CVE”三件套,但运维同学看报告时,真正想知道的是“我们一共有多少个主机受影响、涉及哪些漏洞”。所以判定层之后一定要有一层聚合。我习惯的做法是扫描结果落库后,跑一个 SQL 按 vuln_id 和 service_name 做分组,统计受影响主机数量,生成一张 risk_summary 表。前端展示时,直接查这张表,比一次查几千条原始记录快得多。聚合时要注意,同一个 CVE 在同一主机多个端口命中,应该合并成一条,否则报告里一个漏洞出现三四次,排名虚高。
5. 避坑指南:Java 漏洞扫描系统开发中常见的 6 个问题
5.1 线程一多就卡死:文件描述符耗尽与连接池没有上限
现象:扫描线程从 50 加到 200,程序突然全部阻塞,日志出现 SocketException: Too many open files。 原因:Linux 默认单进程文件描述符限制是 1024,每个 Socket 连接至少占一个 fd,加上线程栈和日志文件,200 个并发连接很容易超限。 解决:先改系统 ulimit -n 到 65535,更关键的是在 Java 线程池里加一个 Semaphore 限制同时打开的 Socket 数不超过 300。另外,所有 Socket 创建都用 try-with-resources,确保异常路径也关闭连接。这里有个细节:Semaphore 要在 try 块里 acquire,但 release 必须在 finally 中执行,否则探测异常时许可不会归还,线程池很快全部阻塞。
5.2 超时设太短导致漏报错报:Connect 超时和 Read 超时是两回事
现象:扫描结果里大量端口显示 closed,但手工 nc 连一下是通的。 原因:很多人只设了 connectTimeout 1000ms,然后读取 Banner 时沿用同一个超时,某些服务响应慢,超过 1 秒就被判定失败。 解决:把连接超时和读取超时分开设置。连接超时控制在 1000ms 左右,读取超时至少 3000ms。同时建议加一个重试机制,对首次无响应的端口做第二次探测,避免网络抖动影响结果。重试次数不是越多越好,我一般设 1 次,再多会显著拉长整体扫描时间。
5.3 正则贪婪匹配导致版本识别失败
现象:Apache 2.4.49 的 Banner 匹配不到 CVE-2021-41773 规则,或误报成 2.4.49-2.4.50。 原因:用 "Server: Apache/(.*?)" 时捕获组里包含后续的 OS 信息,版本比较时遇到非数字字符。 解决:版本提取正则改为 "Server: Apache/([\d.]+)",只捕获数字和点,并且用非贪婪模式。另外,匹配前先做数据清洗,去掉换行、转义序列和多余空格。这里可以加一个小测试:把典型的 Banner 存成单元测试样本,每次改正则后先跑测试,避免把原本正确的匹配改坏。
5.4 内存溢出:结果列表无界增长
现象:扫描 2 万个端口后,内存占用持续上升,最终 OOM。 原因:把全部 ScanResult 放在内存 ArrayList 里,没有及时落库,GC 回收不掉。 解决:ScanResultHolder 里用一个有界队列,比如 ArrayBlockingQueue(5000),当队列满时由消费者线程批量写入数据库。写库失败时要丢弃还是重试?我建议丢弃并记录错误计数,避免队列被卡死。另一个容易忽视的点是 matchedBanner 字段,Banner 最长可能 8KB,2 万条就是 160MB,所以在存储前要把 Banner 截断到 1KB,既保留关键指纹,又不爆内存。
5.5 JDK 版本差异:TLS 握手报 “算法被禁用”
现象:在 JDK 8 上跑得好好的扫描器,升级到 JDK 17 后,连很多 HTTPS 站点都报 SSLHandshakeException。 原因:新版 JDK 默认禁用了 TLSv1、TLSv1.1 和一部分弱加密套件,而很多老旧服务只支持这些协议。 解决:如果扫描目标确实包含老设备,可以在 JVM 参数里临时启用:-Dhttps.protocols=TLSv1,TLSv1.1,TLSv1.2。但要注意,启用旧协议本身也有合规风险,只应在授权的内部资产扫描时使用。更稳妥的做法是扫描器里针对 HTTPS 资产单独维护一个 SSLContext,只对老设备名单启用弱协议,而不是全局放开。
5.6 扫描目标未授权:被安全设备封 IP 甚至产生法律问题
现象:扫描器跑得正欢,突然所有请求超时,然后运维邮件过来说是扫描器触发了 IDS,把对端设备搞宕机了。 原因:没有确认目标授权,扫描速率过高,或没有在扫描前加目标白名单校验。 解决:扫描入口加一个授权状态检查,数据库维护一张“已授权资产表”,不在表里的 IP 一律拒绝扫描。同时把并发度降低,扫描速率限制在每个 IP 每秒不超过 20 个包。这条不是技术问题,但比所有技术问题都重要。我还建议在扫描计划里加一个“紧急停止开关”,一旦收到目标方投诉,能立刻暂停整个扫描集群的任务分发。
6. 把扫描系统当产品打磨:用自建靶机集做误报率回归
前一章说了这么多坑,最后分享一个我长期保留的习惯:给扫描器建一个最小化的“靶机集”,用来做规则回归。我本地用 Docker 起一组容器,固定版本:Apache 2.4.49、OpenSSH 7.4、Redis 5.0 等,每个容器只暴露对应端口,然后写一个 JUnit 参数化测试,把 Banner 样例和期望命中的漏洞 ID 直接放进测试数据。
@ParameterizedTest @CsvSource({ "Apache/2.4.49, CVE-2021-41773, true", "Apache/2.4.50, CVE-2021-41773, false", "SSH-2.0-OpenSSH_7.4, CVE-2017-15906, true" }) void testVulnMatch(String banner, String vulnId, boolean expected) { VulnRule rule = ruleRegistry.find(vulnId); Assertions.assertEquals(expected, rule.matches(banner)); }这个技巧的价值在于,每当你改了一条正则或者新增一个漏洞规则,跑一遍测试就能知道有没有影响其他规则。以前我写扫描器总是先写探测后写匹配,结果线上误报率很高,被同事吐槽是“漏洞复读机”。后来改成先积累测试样例,再写规则,先把已确认的 Banner 样本变成测试集,规则只会在测试集上越改越准。这个习惯比任何调优技巧都管用。你可以把测试集放到 src/test/resources 下,每次发布扫描器前跑一次 mvn test,CI 里直接挡住回归。
除了单元测试,我还会每个月做一次真机验证:从已授权资产里抽 5 台,手工跑一遍扫描,把结果和商业扫描器做对比。漏报的补规则,误报的调正则。这套流程跑下来,扫描器的准确率会稳定在一个可信水位。希望这个方向能帮到你,哪怕是刚开始写第一个 Socket 探测,也先把这个骨架立住,后面加规则只是时间问题。希望帮到你。
本文还有配套的精品资源,点击获取