资讯动态

A盾5.0终端安全管控系统源码解析与二次开发实战

发布时间:2026/10/25 17:49:59 来源:尧图企业网站定制
简介A盾 v5.0 版本源代码面向具备 Windows 底层开发基础的安全研究人员与驱动开发者给出了一套完整的网络数据包拦截与进程管控实现参考可用于理解内核态与用户态协同工作的机制。压缩包共 319 个文件体积约 5.1MB其中 133 个 .h 头文件、54 个 .c 与 52 个 .cpp 源文件构成代码主体另配 .rc 资源脚本及 .ico、.bmp 图标位图等界面素材还包含 vcproj/vcxproj 工程、sln 解决方案、makefile 与 sources 等构建配置以及 .sys 驱动和 .dll 模块产物可直接用 Visual Studio 或 WDK 编译调试。目前已有 423 人学习下载。读者可从中了解 NDIS 包过滤驱动的分层代码结构、控制设备与用户态通信接口、指令长度反汇编等具体实现并借助配套批处理脚本理解资源嵌入流程适合用作驱动开发入门与二次开发的对照样本。1. 一台重装完系统的办公机三天后又变得混乱一台重装完系统的办公机三天后桌面上多出四个来路不明的“加速器”、两个浏览器插件还有一个开机自启的托盘程序没人说得清是谁装的——类似的事在内网里几乎每周都发生。A盾 5.0 源代码指向的就是这个场景它是一套终端安全防护系统的完整工程客户端常驻在被管机器上负责进程、启动项、外设和关键目录的采集与拦截服务端负责接收上报、下发策略、生成告警再加一个 Web 控制台做配置和看板。它不是给个人用户装的杀毒软件而是给几十到几千台机器做统一管控的那一类东西重点在“可见”和“可控”不在病毒库。适合两类人一类是想照着跑通一套终端管控 Demo 的客户端或后端开发另一类是要在这套骨架上加自己检查项的二次开发者。前者关心能不能编译、能不能连上、能不能在控制台看见自己那台机器后者关心规则字段怎么扩、采集点怎么加、告警怎么收敛。这两条路我都走过一遍坑不算少但骨架是完整的。2. 技术栈与目录结构客户端、服务端、控制台的三段式骨架2.1 技术选型为什么是 C 客户端 Java 服务端 Vue 控制台A盾 5.0 的分工很干脆贴近操作系统的那一层用 C 写业务和界面那两层用 Java 和 Vue 写。这种组合在国内的终端管控类项目里非常常见原因也不玄学——客户端要枚举进程、要读注册表启动项、要 open 设备句柄做外设拦截、要注册成开机自启的系统服务这几件事用托管语言做要么做不到要么做起来很别扭而服务端和控制台全是标准的增删改查加展示用 Spring Boot 加 Vue 能把开发速度拉满。层次主要技术职责落地注意客户端 AgentC11、Win32 API、CMake采集、拦截、上报、执行策略编译产物是 Windows 服务不是普通 exe服务端Java 8、Spring Boot、MyBatis、MySQL 8、Redis注册、心跳、策略、告警、审计日志单体应用按模块分包不拆微服务控制台Vue 3、Vite、Element Plus终端列表、策略配置、告警看板纯静态资源打包后丢给 Nginx通信HTTP(S) JSON心跳 30 秒上报与下发共用一套接口断网要能本地缓存补传存储MySQL 存业务Redis 存在线状态终端在线判定、策略版本号Redis 只做缓存丢了不影响正确性服务端这一层常见做法是单体应用按agent、policy、alert、audit四个业务包分不拆微服务。终端管控这类系统的写入量其实不大几千台机器、30 秒一次心跳QPS 也就百来级别拆微服务只会让自己多运维几套进程。真正吃资源的是告警写入和审计日志这两块后面单独讲收敛。客户端的构建用 CMake 管理好处是同一份CMakeLists.txt既能出 Debug 版给开发调试也能出 Release 版给装机用不用维护两套 VS 工程文件。依赖的第三方库不多主要是 HTTP 客户端和 JSON 解析剩下的全是系统 API所以编译环境相对干净这也是它适合拿来练手的原因。2.2 目录结构先认清哪几个包是必须动的拿到源码先别急着编译花十分钟把目录过一遍后面改代码能省一半时间。下面是这套工程的主干结构不同分支可能略有出入但层次基本一致adun/ ├── agent/ # C 客户端 │ ├── src/ │ │ ├── main.cpp # 入口判断是服务模式还是控制台模式 │ │ ├── service/ # 服务安装、启动、守护 │ │ ├── collect/ # 进程、启动项、外设、文件采集 │ │ ├── policy/ # 本地规则执行与拦截动作 │ │ ├── net/ # HTTP 上报、心跳、重试 │ │ └── common/ # 日志、配置、字符串与句柄工具 │ └── CMakeLists.txt ├── server/ # Java 服务端 │ └── src/main/java/com/adun/ │ ├── agent/ # 注册、心跳、指令下发 │ ├── policy/ # 策略 CRUD 与版本管理 │ ├── alert/ # 告警接收、收敛、查询 │ ├── audit/ # 操作审计 │ └── common/ # 统一响应、拦截器、工具 ├── console/ # Vue 控制台 │ └── src/views/ # terminal / policy / alert / audit ├── sql/ # 建库脚本与初始化数据 └── docs/ # 接口说明与部署说明真正高频改动的只有四块agent/src/collect决定你采什么agent/src/policy决定命中之后做什么server/.../policy决定规则怎么存怎么下发console/src/views/policy决定运维怎么配。其余部分跑通之后基本不用碰改坏了反而容易把心跳链路搞挂。docs/里的接口说明值得先读一遍它定义的是客户端和服务端之间的约定包括字段名、时间格式、错误码。这块一旦对不上表现就是客户端一直离线、控制台一直空白排查起来非常费时间。2.3 进程模型三个进程各干什么客户端装完之后不是只有一个进程常见做法是三个主服务进程负责采集和执行策略守护进程负责在主语程被杀掉之后把它拉起来托盘进程负责给使用者一个可见的界面和“我要退出”的入口。三个进程之间用命名管道或者本地 socket 通信不用共享内存简单可靠。主服务进程以 Windows 服务的方式注册StartService之后由系统托管不依赖任何用户登录。这一点很关键——如果做成开机自启的普通 exe那么机器锁屏、切换用户、远程桌面断开的时候都可能出问题而服务模式下这些场景都是稳定的。守护进程同样是服务但优先级更高它在启动时先起主语程然后每 10 秒检查一次主语程的 PID 是否还在不在就重新拉起。启动顺序上有个细节服务端连接必须放在采集线程启动之前完成注册否则第一批采集数据没有agent_id可挂只能丢进本地队列等下一次白白多一轮落盘。我一般会在main.cpp里把初始化拆成“读配置 → 注册 → 起心跳线程 → 起采集线程 → 起策略线程”这五步串行执行任何一步失败就写日志并退出让守护进程去重试而不是在代码里写一堆 sleep 硬等。3. 核心模块拆解从进程采集到策略下发的完整链路3.1 采集模块一次进程快照要花多少代价进程采集是整条链路的起点也是最容易写出性能问题的地方。核心代码不长但每个参数都有讲究// agent/src/collect/process_collector.cpp // 一次全量进程快照句柄用完必须 CloseHandle否则长时间运行会句柄泄漏 std::vectorProcInfo CollectProcesses(bool withHash) { std::vectorProcInfo result; // TH32CS_SNAPPROCESS 只取进程列表比带上模块信息轻很多 HANDLE snap CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (snap INVALID_HANDLE_VALUE) { LogError(snapshot failed, err%lu, GetLastError()); return result; } PROCESSENTRY32W pe{}; pe.dwSize sizeof(pe); // 这个字段不赋值Process32FirstW 会直接失败 if (Process32FirstW(snap, pe)) { do { ProcInfo info; info.pid pe.th32ProcessID; info.ppid pe.th32ParentProcessID; info.name WideToUtf8(pe.szExeFile); info.path QueryImagePath(pe.th32ProcessID); // 可能为空权限不够时拿不到 if (withHash !info.path.empty()) { info.md5 FileMd5(info.path); // 计算全盘文件哈希只对白名单外的文件开 } result.push_back(std::move(info)); } while (Process32NextW(snap, pe)); } CloseHandle(snap); return result; }CreateToolhelp32Snapshot的第一个参数决定快照内容这里只用TH32CS_SNAPPROCESS拿到的就是 PID、父 PID、进程名这些基础字段代价很低几百个进程通常几十毫秒内就能返回。PROCESSENTRY32W的dwSize字段必须手动赋值这是 Win32 里老生常谈的一个坑忘了就是返回 false日志里还看不出原因。QueryImagePath内部走的是OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, ...)加QueryFullProcessImageNameW。这里不要用PROCESS_QUERY_INFORMATION那个权限在普通用户下经常打不开系统进程结果就是路径一栏全是空字符串规则也就匹配不上了。哈希计算是可选开关因为读文件是 IO全量算下来在机械盘机器上能把 CPU 顶到 20% 以上实践中只对规则关心的高风险目录开。采集频率是个需要权衡的参数。进程快照适合 30 秒到 60 秒一次太密了没意义还费电太疏了会漏掉短命进程。启动项和外设可以更慢五分钟一次足够因为这两类变化本身就低频。文件完整性检查最贵通常是按目录配置、错峰执行别和进程采集跑在同一个线程里。3.2 规则引擎一条策略怎么匹配到一条记录采集上来的是一条条记录规则引擎负责判断这条记录该不该拦。这套实现刻意做得简单——顺序遍历规则、命中即返回没有搞复杂的表达式树。原因很实在规则数量通常只有几十条遍历的代价远小于维护一棵树的复杂度而且运维看不懂表达式出问题没法解释。// server/src/main/java/com/adun/policy/RuleEngine.java // 规则按 priority 升序排列命中即返回action 决定是 BLOCK / ALERT / LOG public MatchResult match(ProcessRecord rec, ListPolicyRule rules) { for (PolicyRule rule : rules) { if (!rule.getEnabled()) continue; // 停用的规则直接跳过 if (!rule.getOsType().equalsIgnoreCase(rec.getOsType())) continue; // 平台过滤 String actual rec.field(rule.getField()); // name / path / md5 / publisher if (actual null) continue; if (compare(actual, rule.getOperator(), rule.getValue())) { return MatchResult.hit(rule); } } return MatchResult.pass(); } private boolean compare(String actual, String op, String expect) { switch (op) { case EQ: return actual.equalsIgnoreCase(expect); case CONTAINS: return actual.toLowerCase().contains(expect.toLowerCase()); case PREFIX: return actual.toLowerCase().startsWith(expect.toLowerCase()); default: return false; // 未知操作符一律不命中宁可放过不可错杀 } }field是一个字符串到取值的映射不是反射。反射写起来爽但线上排查时一个字段名拼错表现就是规则永远不命中日志里什么都没有。用显式 map 的话字段名不认识就能立刻抛出来控制台保存策略时也能做校验。compare里默认返回 false 这一行值得单独说。规则引擎的失败方向决定了它是“误杀”还是“漏杀”终端管控场景下误杀一个正常业务程序的代价远大于漏掉一个可疑程序所以未知操作符、空值、解析异常统统走不命中。真正需要严格拦截的场景用白名单兜底而不是靠规则引擎变严格。规则的优先级设计也要注意。常见做法是“白名单优先级最高、黑名单次之、普通监控最低”也就是同一台机器上先跑白名单命中的直接放行不再往下比。这样运维加一条白名单就能临时救火不用去翻是哪条黑名单规则误伤了。3.3 通信层注册、心跳、策略拉取的重试与幂等客户端和服务端之间就三件事注册、心跳、取策略。全部走 HTTP JSON接口数量很少但每个都要考虑失败。// agent/src/net/reporter.cpp // 三次退避重试全部失败就落本地队列由下一次心跳补传 bool Report(const std::string url, const std::string body) { for (int i 0; i 3; i) { if (HttpPost(url, body, 3000)) return true; // 3000ms 连接读取超时 Sleep(1000 * (1 i)); // 1s / 2s / 4s } LocalQueue::Push(url, body); // 落盘队列上限 10000 条超了丢最老的 return false; }退避间隔用1 i而不是固定值是因为服务端重启或者网络抖动往往是几秒级的固定间隔容易在同一个时间点集中重试把刚起来的服务端又打一遍。落盘队列的上限必须设否则一台机器断网三天本地文件能把磁盘塞满这个坑我在别的项目里踩过后来所有本地缓存都强制带上限和淘汰策略。服务端这边的注册接口要做幂等。客户端的agent_id一般由机器指纹生成主机名 主板序列号 首个物理网卡 MAC 做哈希重装客户端之后指纹不变注册接口就应该更新记录而不是插入新记录// server/src/main/java/com/adun/agent/AgentController.java PostMapping(/api/agent/register) public RRegisterResp register(RequestBody Valid RegisterReq req) { AgentDO exist agentMapper.selectByAgentId(req.getAgentId()); if (exist null) { agentMapper.insert(buildAgent(req)); // 首次注册 } else { agentMapper.updateHeartbeat(req.getAgentId(), now()); // 重装/重启只刷新状态 } redis.opsForValue().set(agent:online: req.getAgentId(), 1, Duration.ofSeconds(90)); return R.ok(new RegisterResp(policyVersionService.current())); }RegisterResp里带上当前策略版本号客户端一比就知道要不要拉新策略省掉一次无意义的全量拉取。Redis 的在线标记 TTL 设成心跳间隔的三倍30 秒心跳对应 90 秒 TTL这样偶发丢一两个心跳不会误判离线连续丢三个才认为掉线。这个三倍关系不是玄学是留给网络抖动和 GC 停顿的余量。策略下发用“版本号 全量拉取”不做增量。策略体量小全量一次也就几十 KB增量的复杂度完全不划算。客户端本地缓存一份策略文件启动时先加载本地版本保证有策略可执行再去服务端比对版本这样即使服务端挂了已有策略依然生效。3.4 服务端落库与告警收敛别让一条规则生成大量重复告警告警表是整套系统里写入最猛的一张表。一条规则命中一次写一条如果不做收敛一台机器上某个高频轮询的程序能在一小时内刷出几千条一模一样的记录控制台直接卡死运维也不会再看这个页面。CREATE TABLE t_alert ( id BIGINT NOT NULL AUTO_INCREMENT, agent_id VARCHAR(64) NOT NULL COMMENT 终端唯一标识, rule_id BIGINT NOT NULL COMMENT 命中的规则, rule_name VARCHAR(128) NOT NULL, target VARCHAR(512) NOT NULL COMMENT 命中的进程路径或外设名, action VARCHAR(16) NOT NULL COMMENT BLOCK/ALERT/LOG, hit_count INT NOT NULL DEFAULT 1 COMMENT 收敛窗口内的命中次数, first_time DATETIME NOT NULL, last_time DATETIME NOT NULL, PRIMARY KEY (id), KEY idx_agent_rule_time (agent_id, rule_id, last_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;收敛逻辑放在写入前同一个agent_id rule_id target在 5 分钟窗口内只保留一行重复命中就hit_count 1并更新last_time不新增行。实现上先用 Redis 做一个短期的去重键命中就直接累加没命中才走数据库的 upsert。这样高频告警的写入压力基本落在 Redis 上MySQL 只承担真正的状态变更。idx_agent_rule_time这个联合索引是为控制台的“某台机器最近告警”查询准备的字段顺序不能换。反过来如果先按rule_id查“某条规则命中了哪些机器”就得再建一个(rule_id, last_time)的索引。索引这块别偷懒告警表上百万行很快的。审计日志和告警要分表。告警是“出事了”审计是“谁改了什么”两者的查询模式、保留周期、合规要求都不一样。混在一张表里最常见的结果是运维想查最近告警被一堆“修改策略”的记录淹没。4. 把项目跑起来编译、建库、联调客户端4.1 环境与依赖编译前先把这几样装齐这套工程的依赖不算多但版本卡得比较死尤其是 JDK 和 MySQL 驱动。下面是我在一台干净的开发机上验证过的组合组件版本要求用途备注JDK8 或 11服务端编译运行别上 17部分老依赖会报模块化错误Maven3.6服务端构建配好国内镜像否则首次拉依赖很慢MySQL8.0业务数据字符集必须是 utf8mb4Redis5.0在线状态、告警去重不设密码也行本地开发够用Node.js16 或 18控制台构建版本太高 Vite 老配置会报错VS Build Tools2019客户端编译装“使用 C 的桌面开发”工作负载CMake3.20客户端构建低于 3.15 有些语法不认客户端编译这块有个容易被忽略的点必须装 Windows SDK而且要和 VS 的版本匹配。只装 Build Tools 不勾 SDKCMake 配置阶段能过一编译就报找不到windows.h报错信息还挺绕看着像是代码问题。4.2 服务端建库、改配置、起服务先建库跑脚本sql/目录下一般会有schema.sql和init_data.sql两个文件前者建表后者塞一个默认管理员账号和几条示例规则。执行顺序不能反否则初始化数据插不进表。# 1. 建库字符集一定要显式指定不然中文规则名会变问号 mysql -uroot -p -e CREATE DATABASE adun DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci; # 2. 导表结构和初始数据 mysql -uroot -p adun sql/schema.sql mysql -uroot -p adun sql/init_data.sql # 3. 改配置后打包启动 cd server mvn -DskipTests clean package java -jar target/adun-server-5.0.jar --spring.profiles.activedev配置文件里要改的就三处数据库连接、Redis 地址、客户端上报的服务端地址。第三项经常被漏掉agent.report.base-url默认写的是127.0.0.1本地联调没问题一放到虚拟机上客户端就永远连不上。# server/src/main/resources/application-dev.yml spring: datasource: url: jdbc:mysql://127.0.0.1:3306/adun?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你的密码 redis: host: 127.0.0.1 port: 6379 agent: report: base-url: http://192.168.1.20:8080 # 客户端实际能访问到的地址别写 localhost heartbeat: timeout-seconds: 90serverTimezone这个参数必须显式写。MySQL 8 的驱动默认按 UTC 解析时间不配的话控制台上所有时间都会比实际早 8 小时看起来像是采集延迟其实是时区问题这个坑每年都有人踩。启动成功的标志是控制台打出Started AdunServerApplication并且 8080 端口处于监听状态。如果卡在HikariPool初始化八成是数据库连不上或者密码错了先去看sql/schema.sql是不是漏跑了。4.3 客户端CMake 编译与注册成服务客户端编译分两步先 CMake 生成工程再用 VS 的构建工具编译。命令行一把梭cd agent # -A x64 必须写默认可能生成 32 位装到 64 位系统上跑不起来 cmake -S . -B build -G Visual Studio 16 2019 -A x64 -DCMAKE_BUILD_TYPERelease cmake --build build --config Release # 产物一般在 build/Release/agent.exe先以前台模式跑一次看日志 ./build/Release/agent.exe -console-console这个参数是给调试用的让程序不注册服务、直接在当前终端里跑日志打到标准输出。第一次跑一定要用这个模式确认注册接口通了、心跳正常了再去装服务。直接装服务的话出问题只能去事件查看器里翻效率低很多。确认无误后注册成服务# 注意 binPath 后面必须有一个空格这是 sc 命令的老规矩少了就报参数错误 sc create AdunAgent binPath C:\adun\agent.exe -service start auto DisplayName A盾终端防护 sc description AdunAgent 终端安全防护客户端 sc start AdunAgentstart auto表示开机自启binPath里的路径要是绝对路径并且带引号路径里有空格的时候尤其要注意。装完之后用sc query AdunAgent看状态STATE显示RUNNING才算成功。如果显示STOPPED并且启动时报 1053一般是服务入口函数没有在规定时间内调用StartServiceCtrlDispatcher回头检查main.cpp里服务模式的分支有没有走到。4.4 联调从客户端注册到控制台出现第一条告警联调顺序建议是“先手动调接口再跑客户端”。手动能通说明服务端没问题剩下就是客户端的事排查范围直接砍一半。# 手动模拟一次注册确认服务端接口和数据库写入都正常 curl -X POST http://127.0.0.1:8080/api/agent/register \ -H Content-Type: application/json \ -d {agentId:test-0001,hostname:vm-test,osType:windows,agentVersion:5.0} # 再查一次终端列表应该能看到刚注册的这台 curl http://127.0.0.1:8080/api/agent/list?page1size10接口通了之后启动客户端在控制台的终端列表里应该几十秒内出现这台机器状态从“离线”变成“在线”。然后去策略页面加一条最简单的规则比如“进程名包含minesweeper时记录日志”再到虚拟机上手动跑一下对应程序等一个采集周期告警页面就会出现记录。验证链路上有三个观察点客户端的本地日志有没有“register success”和“heartbeat ok”服务端日志有没有收到/api/agent/heartbeat控制台列表的状态列是不是绿色。三个点里哪一个断了问题就定位在哪一段不用瞎猜。5. 避坑与排查五个让我加班到凌晨的问题5.1 服务装上了但起不来事件日志报 1053现象是sc start AdunAgent返回 1053服务管理器里状态一闪就变回“已停止”客户端日志一个字都没有。原因基本固定在服务入口。Windows 服务的ServiceMain必须在StartServiceCtrlDispatcher调用后 30 秒内上报SERVICE_RUNNING而很多示例代码在ServiceMain里做了耗时的初始化读配置、连服务端、等注册返回一旦超时就判定启动失败。另外还有一个更隐蔽的原因main函数里判断命令行参数时用了宽字符比较但argv是窄字符-service这个分支根本没进去程序直接进了控制台模式然后退出。解决方式是把耗时初始化挪到独立线程里ServiceMain里只做注册状态、起线程、立即上报 RUNNING 三件事。参数判断统一转成宽字符再比或者干脆用CommandLineToArgvW取参数。改完之后启动时间从 3 秒降到 200 毫秒问题消失。5.2 控制台显示离线但机器能 ping 通服务端现象是客户端日志里一直刷“register failed, retry”服务端日志干干净净一条请求都没收到。原因通常是地址不对不是网络不通。客户端配置里写的是127.0.0.1:8080装在虚拟机里跑的时候这个地址指向虚拟机自己当然连不上另一种情况是服务端只监听了localhost没有监听0.0.0.0外部请求直接被拒。还有一种是公司网络里的防火墙只放行了 80 和 4438080 被拦了telnet一下就看得出来。解决方式是三步先在客户端机器上telnet 服务端IP 8080确认端口通再检查服务端server.address配置最后确认客户端配置里的地址是客户端视角能访问到的地址不是开发机的视角。这个顺序别乱先查配置能省下抓包的时间。5.3 策略改了不生效重启客户端才行现象是控制台改了规则、点了保存被管机器上的行为一点变化都没有把客户端服务重启一次就立刻生效了。原因是策略拉取做成了“只在注册时拉一次”心跳接口返回的版本号客户端压根没解析。这种 bug 在联调阶段特别容易漏因为开发时每次都是改完代码重启客户端相当于每次都走了注册流程看起来一切正常。解决方式是在心跳响应里带上policyVersion客户端拿到之后和本地缓存比对不一致就异步触发一次全量拉取拉完再比对一次 MD5 防止半包。要注意拉取动作要加锁否则心跳和手动刷新同时触发会拉两次虽然不出错但浪费带宽。5.4 采集到的进程列表重复CPU 占用一路飙升现象是控制台的进程列表里同一个进程出现好几条机器风扇开始狂转任务管理器里客户端 CPU 稳定在 30% 以上。原因有两个往往同时出现。一是采集线程被重复创建了服务模式和守护进程模式各起了一份两个线程往同一个队列里塞数据二是哈希计算没有做缓存每一轮采集都把全盘所有 exe 重新算一遍 MD5机械盘上这一项就能吃掉大半 CPU。解决方式是在采集模块加一个全局的单例保护用std::call_once或者原子标志位都行保证采集线程全程只有一个哈希加一层按“路径 文件大小 修改时间”做 key 的内存缓存命中就不重算缓存上限设个几千条超了按 LRU 淘汰。改完之后 CPU 降到 3% 以内。5.5 告警时间比实际早八小时怎么改都改不对现象是控制台里所有告警的时间戳都比真实时间早 8 小时客户端和服务器的系统时间都是对的。原因是 JDBC 连接串里没写serverTimezoneMySQL 8 的驱动默认按 UTC 处理DATETIME写入时按 UTC 解释读出来再按本地时区展示一来一回就差了一个时区。另一种情况是 Java 侧用了java.util.Date而数据库字段是DATETIME中间转换环节又做了一次时区换算叠加之后偏差更大。解决方式是连接串显式加serverTimezoneAsia/Shanghai实体类里的时间字段统一用LocalDateTime服务端和数据库都按同一个时区存。改完之后清一次历史数据否则新旧数据混在一起看板上的趋势图会有一道明显的断层。6. 二次开发把通用盾改成你自己的盾拿到这套源码之后绝大多数人的第一件事都是加检查项。我拿“禁止运行指定目录下的可执行文件”举例走一遍完整链路顺便说说哪些地方容易改出问题。扩展点一共五处改之前先对着表确认影响范围层次改动位置改动内容风险数据库t_policy_rule表新增操作符枚举值无需加字段低服务端RuleEngine.compare增加REGEX分支中正则写错会拖慢匹配客户端policy/rule_executor.cpp增加对应动作分支中动作写错会误拦控制台views/policy/RuleForm.vue下拉框加一个选项低文档docs/api.md补充操作符说明低服务端这一层最好改加个枚举分支就行// server/src/main/java/com/adun/policy/Operator.java public enum Operator { EQ, CONTAINS, PREFIX, REGEX; // 5.0 原生只有前三个这个分支是二次开发加的 public static Operator of(String v) { for (Operator op : values()) { if (op.name().equalsIgnoreCase(v)) return op; } return null; // 返回 null 而不是抛异常交给上层决定是否忽略该规则 } }正则匹配一定要做两件事编译结果缓存、超时保护。Pattern.compile是有成本的每条记录都编译一次几十条规则乘以几百个进程一秒内能编译上万次超时保护则是防灾难性回溯用户在控制台填一个(a)$这种表达式能把采集线程卡死。我的做法是用 Caffeine 或者简单的ConcurrentHashMap缓存Pattern再加一层字符串长度前置校验超过 256 字符的目标直接不匹配。客户端那边的动作执行有个必须注意的点拦截动作要在服务进程里做不要在托盘进程里做。托盘进程是用户态的用户一关就没了拦截随之失效而且托盘进程通常以普通用户权限运行TerminateProcess一个系统服务根本打不进去。所有拦截统一走服务进程托盘只负责展示和转发指令。验证方法我是这么走的先在服务端写一个规则匹配的单元测试用固定的一组ProcessRecord跑断言命中结果和动作然后在虚拟机上装客户端配一条只会 LOG 不会 BLOCK 的规则跑一天确认不会误伤最后才把动作改成 BLOCK在隔离环境里试。跳过第二步直接上 BLOCK把业务程序拦了运维找上门的时候解释起来很麻烦。从那以后我每次动规则引擎都会先把t_alert里最近一天的记录导出来用新规则离线重跑一遍对比命中数量的变化。这一步花不了十分钟但能提前发现“新加的正则把所有系统进程都匹配上了”这种灾难。终端管控这类系统出错的代价从来不是报错而是悄悄拦掉了不该拦的东西。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑