Compare commits
No commits in common. "main" and "feature/info-monitor" have entirely different histories.
main
...
feature/in
|
|
@ -34,13 +34,3 @@ config.local.yaml
|
|||
|
||||
# 系统文件
|
||||
.DS_Store
|
||||
|
||||
# 需求文件
|
||||
todo.txt
|
||||
|
||||
# 测试文件
|
||||
test/
|
||||
|
||||
# 编译文件
|
||||
sysmonitord
|
||||
fixroad.md
|
||||
|
|
|
|||
132
README.md
132
README.md
|
|
@ -1,131 +1,19 @@
|
|||
# SysMonitord - 轻量级 Linux 安全终端探针
|
||||
# sysmonitord
|
||||
|
||||
`SysMonitord` 是一个基于 Go 语言开发的轻量级主机安全监控代理(HIDS/EDR Agent)。它采用**白名单机制**来监控文件系统的完整性,结合实时 SSH 审计与系统状态采集,通过 WebSocket 与中心服务器保持实时通讯。
|
||||
重写VigilGoGuard,更符合工程规范
|
||||
|
||||
设计目标:**低资源占用、高灵敏度、配置动态下发。**
|
||||
多线程支持、更加模块化,以及...
|
||||
|
||||
## ✨ 核心特性
|
||||
## ToDo
|
||||
|
||||
- **🛡️ 文件完整性防护 (FIM)**
|
||||
- **白名单引擎**:基于文件 Hash (SHA256) 的白名单校验,拒绝未授权的二进制文件。
|
||||
- **双模感知**:
|
||||
- **实时监听 (Watcher)**:基于 `inotify`,毫秒级感知文件创建、修改、删除。
|
||||
- **主动巡逻 (Scanner)**:周期性全盘扫描,具备 **CPU 负载避让** 机制(CPU > 50% 自动休眠),防止影响业务。
|
||||
1. 用户登录监控 (ssh, su, sudo)
|
||||
|
||||
- **🔌 动态配置与通讯**
|
||||
- **远程配置**:启动时通过 HTTP 拉取最新的官方策略(白名单)和用户配置。
|
||||
- **双通道长连接**:
|
||||
- **控制通道**:与中心服务器保持 WebSocket 连接,上报状态和接收指令。
|
||||
- **审计通道**:独立的 WebSocket 连接用于传输敏感日志(如 SSH 登录记录)。
|
||||
- **断线重连**:内置指数退避算法,网络波动自动恢复。
|
||||
2. 进程监控 (异常进程、资源占用)
|
||||
|
||||
- **👁️ 行为审计**
|
||||
- **SSH 监控**:实时分析 `/var/log/secure` 或 `journald`,捕获异常登录尝试。
|
||||
- **资源监控**:实时采集 CPU、内存、磁盘 IO 等系统指标。
|
||||
3. 文件监控 (关键文件变更)
|
||||
|
||||
## 🛠️ 架构概览
|
||||
4. 网络连接监控
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[启动 Start] --> B[下载配置 HTTP]
|
||||
B --> C[初始化白名单引擎 Whitelist Manager]
|
||||
C --> D{启动子系统}
|
||||
D --> E[网络客户端 Network Client]
|
||||
D --> F[感知系统 Sensor]
|
||||
D --> G[监控系统 Monitor]
|
||||
5. 系统资源监控 (CPU、内存、磁盘)
|
||||
|
||||
E --> |WebSocket| H[中心服务器]
|
||||
E --> |WebSocket| I[审计服务器]
|
||||
|
||||
F --> |inotify| J[实时监听 Watcher]
|
||||
F --> |Walk| K[周期扫描 Scanner]
|
||||
|
||||
G --> L[SSH 分析器]
|
||||
G --> M[性能采集器]
|
||||
|
||||
J & K & L & M --> |Alert/Metrics| E
|
||||
```
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 环境要求
|
||||
- **操作系统**: Linux (依赖 `inotify` 和 `/proc` 文件系统)
|
||||
- **权限**: `root` (用于读取系统日志及遍历关键目录)
|
||||
- **构建环境**: Go 1.18+
|
||||
|
||||
### 1. 编译项目
|
||||
|
||||
```bash
|
||||
# 下载依赖
|
||||
go mod tidy
|
||||
|
||||
# 编译二进制文件
|
||||
go build -o sysmonitord cmd/sysmonitord/main.go
|
||||
```
|
||||
|
||||
### 2. 运行
|
||||
|
||||
由于涉及系统底层文件监控,建议使用 `sudo` 运行:
|
||||
|
||||
```bash
|
||||
sudo ./sysmonitord
|
||||
```
|
||||
|
||||
## ⚙️ 配置说明
|
||||
|
||||
程序启动时会请求两个 JSON 配置文件。你需要开发一个简单的 HTTP Server 来提供这些 JSON。
|
||||
|
||||
### 1. 官方配置 (Official Config)
|
||||
用于定义“什么是合法的”。
|
||||
|
||||
```json
|
||||
{
|
||||
"hash_whitelist": {
|
||||
"/usr/bin/ls": "sha256_hash_value_here",
|
||||
"/usr/sbin/nginx": "sha256_hash_value_here"
|
||||
},
|
||||
"ignored_paths": [
|
||||
"/tmp/logs",
|
||||
"/var/cache"
|
||||
],
|
||||
"update_interval": 3600
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 用户配置 (User Config)
|
||||
用于定义运行时行为。
|
||||
|
||||
```json
|
||||
{
|
||||
"audit_server_url": "ws://audit.example.com/ws",
|
||||
"reporting_interval": 60,
|
||||
"alert_threshold": 5
|
||||
}
|
||||
```
|
||||
|
||||
## 📂 项目结构
|
||||
|
||||
```
|
||||
sysmonitord/
|
||||
├── cmd/
|
||||
│ └── sysmonitord/ # 程序入口
|
||||
│ └── main.go
|
||||
├── internal/
|
||||
│ ├── config/ # 配置结构体定义
|
||||
│ ├── monitor/ # SSH 日志分析与系统资源采集
|
||||
│ ├── network/ # HTTP 加载器与 WebSocket 客户端
|
||||
│ ├── scanner/ # 文件扫描 (Scanner) 与实时监听 (Watcher)
|
||||
│ └── whitelist/ # 核心白名单判定逻辑
|
||||
├── go.mod
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 🗓️ 开发计划 (Roadmap)
|
||||
|
||||
- [x] 基础框架搭建
|
||||
- [x] 核心白名单逻辑实现
|
||||
- [x] 网络通信层 (HTTP/WebSocket)
|
||||
- [x] 文件扫描与实时监控集成
|
||||
- [ ] **后端控制台开发** (接收上报数据并可视化)
|
||||
- [ ] **主动防御能力** (发现恶意文件自动隔离/删除)
|
||||
- [ ] **一键安装脚本** (Systemd 集成)
|
||||
6. 安全事件报警
|
||||
|
|
|
|||
|
|
@ -4,155 +4,153 @@ import (
|
|||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/wuko233/sysmonitord/internal/config"
|
||||
"github.com/wuko233/sysmonitord/internal/monitor"
|
||||
"github.com/wuko233/sysmonitord/internal/network"
|
||||
"github.com/wuko233/sysmonitord/internal/scanner"
|
||||
"github.com/wuko233/sysmonitord/internal/whitelist"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
)
|
||||
|
||||
const (
|
||||
OfficialConfigURL = "http://localhost:8090/api/v1/configs/official.json"
|
||||
UserConfigURL = "http://localhost:8090/api/v1/configs/user.json"
|
||||
CenterServerURL = "ws://localhost:8090/api/v1/ws"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
initLogger()
|
||||
log.Println("==========================================")
|
||||
log.Print(`
|
||||
_ _ _
|
||||
(_) | | |
|
||||
___ _ _ ___ _ __ ___ ___ _ __ _| |_ ___ _ __ __| |
|
||||
/ __| | | / __| '_ ` + "`" + ` _ \ / _ \| '_ \| | __/ _ \| '__/ _` + "`" + ` |
|
||||
\__ \ |_| \__ \ | | | | | (_) | | | | | || (_) | | | (_| |
|
||||
|___/\__, |___/_| |_| |_|\___/|_| |_|_|\__\___/|_| \__,_|
|
||||
__/ |
|
||||
|___/
|
||||
`)
|
||||
log.Println("==========================================")
|
||||
|
||||
// 加载配置
|
||||
log.Println("[启动流程] 1/6: 下载远程安全策略配置...")
|
||||
cfgLoader := network.NewConfigLoader()
|
||||
|
||||
officialCfg, userCfg, err := cfgLoader.LoadConfigs(network.ConfigUrls{
|
||||
OfficialConfigUrl: OfficialConfigURL,
|
||||
UserConfigUrl: UserConfigURL,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("[启动错误]下载配置失败: %v", err)
|
||||
log.Println("[启动降级] 继续使用默认空配置...")
|
||||
// os.Exit(1)
|
||||
}
|
||||
|
||||
log.Println("[启动流程] 2/6: 初始化白名单判定引擎...")
|
||||
wlManager := whitelist.NewManager()
|
||||
wlManager.UpdateConfig(officialCfg, userCfg)
|
||||
|
||||
log.Println("[启动流程] 3/6: 启动中心服务器连接...")
|
||||
centerClient := network.NewWSClient(network.ClientConfig{
|
||||
ServerURL: CenterServerURL,
|
||||
SendInterval: 1 * time.Second,
|
||||
BufferSize: 1000,
|
||||
})
|
||||
centerClient.Start()
|
||||
|
||||
auditUrl := wlManager.GetAuditServerUrl()
|
||||
if auditUrl == "" {
|
||||
auditUrl = CenterServerURL
|
||||
}
|
||||
auditClient := network.NewWSClient(network.ClientConfig{
|
||||
ServerURL: auditUrl,
|
||||
SendInterval: 1 * time.Second,
|
||||
BufferSize: 1000,
|
||||
})
|
||||
auditClient.Start()
|
||||
|
||||
log.Println("[启动流程] 4/6: 启动文件完整性防护...")
|
||||
|
||||
// 扫盘器
|
||||
sysScanner := scanner.NewScanner(wlManager, centerClient)
|
||||
sysScanner.Start()
|
||||
|
||||
// 监控器
|
||||
sysWatcher, err := scanner.NewWatcher(wlManager, centerClient)
|
||||
if err != nil {
|
||||
log.Fatalf("[启动错误] 初始化监控器失败: %v", err)
|
||||
} else {
|
||||
sysWatcher.Start()
|
||||
}
|
||||
|
||||
log.Println("[启动流程] 5/6: 启动系统行为监控...")
|
||||
|
||||
// SSH监控
|
||||
sshAlertChan := make(chan monitor.Alert, 100)
|
||||
sshMon := monitor.NewSSHMonitor(&config.SSHMonitor{
|
||||
Enabled: true,
|
||||
AlertOnRootLogin: true,
|
||||
DisplayOnShell: true,
|
||||
}, sshAlertChan)
|
||||
|
||||
go func() {
|
||||
for alert := range sshAlertChan {
|
||||
packet := network.NewPacket("SSH_ALERT", alert)
|
||||
auditClient.SendQueue(packet)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
if err := sshMon.Start(); err != nil {
|
||||
log.Printf("[监控错误] SSH监控遇到错误: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// 状态监控
|
||||
metricsChan := make(chan monitor.ServerMetrics, 100)
|
||||
infoMon := monitor.NewInfoMonitor(nil, metricsChan)
|
||||
|
||||
go func() {
|
||||
for metrics := range metricsChan {
|
||||
packet := network.NewPacket("STATUS_UPDATE", metrics)
|
||||
centerClient.SendQueue(packet)
|
||||
}
|
||||
}()
|
||||
|
||||
go infoMon.Start()
|
||||
|
||||
log.Println("[启动流程] 6/6: 系统监控守护进程启动完成!")
|
||||
|
||||
stopChan := make(chan os.Signal, 1)
|
||||
signal.Notify(stopChan, os.Interrupt, syscall.SIGTERM)
|
||||
<-stopChan
|
||||
log.Println("[守护进程] 接收到停止信号,正在关闭...")
|
||||
|
||||
if sysWatcher != nil {
|
||||
sysWatcher.Stop()
|
||||
}
|
||||
sysScanner.Stop()
|
||||
sshMon.Stop()
|
||||
infoMon.Stop()
|
||||
centerClient.Stop()
|
||||
auditClient.Stop()
|
||||
log.Println("[守护进程] 已成功停止,安全退出程序。")
|
||||
}
|
||||
|
||||
func initLogger() {
|
||||
// 设置日志输出到文件和控制台
|
||||
log.SetOutput(os.Stdout)
|
||||
fileLogger := &lumberjack.Logger{
|
||||
Filename: "/var/log/sysmonitord/sysmonitord.log",
|
||||
MaxSize: 100, // MB
|
||||
MaxBackups: 7,
|
||||
MaxAge: 30, // 天
|
||||
MaxSize: 10, // MB
|
||||
MaxBackups: 5,
|
||||
MaxAge: 28, // days
|
||||
Compress: true,
|
||||
}
|
||||
|
||||
log.SetOutput(io.MultiWriter(os.Stdout, fileLogger))
|
||||
|
||||
log.Println("启动sysmonitord...")
|
||||
|
||||
// SSH监控配置
|
||||
sshCfg := &config.SSHMonitor{
|
||||
Enabled: true,
|
||||
DisplayOnShell: true,
|
||||
AlertOnRootLogin: true,
|
||||
}
|
||||
|
||||
// 信息监控配置
|
||||
infoCfg := &monitor.InfoMonitorConfig{
|
||||
Enabled: true,
|
||||
Interval: 30 * time.Second, // 每30秒采集一次
|
||||
LogFilePath: "/var/log/sysmonitord/info_monitor.log",
|
||||
MaxLogSize: 100 * 1024 * 1024, // 100MB
|
||||
LogRetention: 30, // 保留30天
|
||||
ProcessLimit: 10, // 显示10个进程
|
||||
CollectNetwork: true,
|
||||
CollectProcess: true,
|
||||
}
|
||||
|
||||
alertChan := make(chan monitor.Alert, 100)
|
||||
metricsChan := make(chan monitor.ServerMetrics, 100)
|
||||
|
||||
log.Println("初始化监控器...")
|
||||
|
||||
// 创建SSH监控器
|
||||
sshMonitor := monitor.NewSSHMonitor(sshCfg, alertChan)
|
||||
|
||||
// 创建信息监控器
|
||||
infoMonitor := monitor.NewInfoMonitor(infoCfg, metricsChan)
|
||||
|
||||
// 启动告警处理
|
||||
log.Println("启动告警处理...")
|
||||
go handleAlerts(alertChan)
|
||||
|
||||
// 启动指标处理
|
||||
log.Println("启动指标处理...")
|
||||
go handleMetrics(metricsChan)
|
||||
|
||||
// 启动SSH监控器
|
||||
go func() {
|
||||
log.Println("启动SSH监控器...")
|
||||
if err := sshMonitor.Start(); err != nil {
|
||||
log.Fatalf("启动SSH监控器失败: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// 启动信息监控器
|
||||
go func() {
|
||||
log.Println("启动信息监控器...")
|
||||
if err := infoMonitor.Start(); err != nil {
|
||||
log.Fatalf("启动信息监控器失败: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
log.Println("sysmonitord监控系统已启动.")
|
||||
log.Println("按Ctrl+C退出...")
|
||||
|
||||
stopChan := make(chan os.Signal, 1)
|
||||
|
||||
<-stopChan
|
||||
|
||||
log.Println("正在停止监控器...")
|
||||
|
||||
// 停止信息监控器
|
||||
if err := infoMonitor.Stop(); err != nil {
|
||||
log.Printf("停止信息监控器失败: %v", err)
|
||||
}
|
||||
|
||||
// 停止SSH监控器
|
||||
if err := sshMonitor.Stop(); err != nil {
|
||||
log.Printf("停止SSH监控器失败: %v", err)
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
log.Println("sysmonitord已退出.")
|
||||
}
|
||||
|
||||
func handleAlerts(alertChan <-chan monitor.Alert) {
|
||||
for alert := range alertChan {
|
||||
log.Printf("[告警] 类型: %s | 级别: %s | 时间: %s | 消息: %s | 数据: %+v\n",
|
||||
alert.Type, alert.Level, alert.Timestamp.Format(time.RFC3339), alert.Message, alert.Data)
|
||||
|
||||
switch alert.Type {
|
||||
case "SSH_ROOT_LOGIN":
|
||||
log.Println("ROOT用户登入警告!")
|
||||
// Todo: 接入发信接口
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleMetrics(metricsChan <-chan monitor.ServerMetrics) {
|
||||
for metrics := range metricsChan {
|
||||
// 这里可以处理指标数据,比如:
|
||||
// 1. 存储到数据库
|
||||
// 2. 发送到监控系统
|
||||
// 3. 生成告警
|
||||
|
||||
// 示例:检查指标并生成告警
|
||||
checkMetrics(&metrics)
|
||||
}
|
||||
}
|
||||
|
||||
func checkMetrics(metrics *monitor.ServerMetrics) {
|
||||
// 检查CPU使用率
|
||||
if metrics.CPU.UsagePercent > 90 {
|
||||
log.Printf("[警告] CPU使用率过高: %.2f%%\n", metrics.CPU.UsagePercent)
|
||||
}
|
||||
|
||||
// 检查内存使用率
|
||||
if metrics.Memory.UsedPercent > 90 {
|
||||
log.Printf("[警告] 内存使用率过高: %.2f%%\n", metrics.Memory.UsedPercent)
|
||||
}
|
||||
|
||||
// 检查磁盘使用率
|
||||
for _, disk := range metrics.Disk {
|
||||
if disk.UsedPercent > 90 {
|
||||
log.Printf("[警告] 磁盘%s使用率过高: %.2f%%\n",
|
||||
disk.Mountpoint, disk.UsedPercent)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查负载
|
||||
if metrics.Load.RelativeLoad1 > 3.0 {
|
||||
log.Printf("[警告] 系统负载过高: %.2f\n", metrics.Load.Load1)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
5
go.mod
5
go.mod
|
|
@ -4,11 +4,6 @@ go 1.24.3
|
|||
|
||||
require github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
||||
|
||||
require (
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/ebitengine/purego v0.9.1 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -4,13 +4,9 @@ github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5z
|
|||
github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU=
|
||||
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
|
||||
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
|
|
|
|||
|
|
@ -1,44 +1,5 @@
|
|||
package config
|
||||
|
||||
import "time"
|
||||
|
||||
type Configuration struct {
|
||||
Local Localconfig // 本地配置
|
||||
Offical OfficialConfig // 官方配置
|
||||
User UserConfig // 用户自定义配置
|
||||
}
|
||||
|
||||
type Localconfig struct {
|
||||
LogPath string `yaml:"log_path"`
|
||||
CheckInterval time.Duration `yaml:"check_interval"`
|
||||
ServerUrl string `yaml:"server_url"`
|
||||
}
|
||||
|
||||
type OfficialConfig struct {
|
||||
WhitelistFiles map[string][]string `yaml:"whitelist_files"`
|
||||
WhitelistProcesses []string `yaml:"whitelist_processes"`
|
||||
IgnoredPaths []string `yaml:"ignored_paths"`
|
||||
}
|
||||
|
||||
type UserConfig struct {
|
||||
AuditServerUrl string `json:"audit_server_url"` // 审计服务器地址
|
||||
// 用户补充的白名单文件
|
||||
SupplementFiles map[string][]string `json:"supplement_files"`
|
||||
// 用户补充的进程列表
|
||||
// Key: 进程名, Value: 启动指令(如果为空则仅作为白名单,如果不为空则需保活)
|
||||
SupplementProcesses map[string]string `json:"supplement_processes"`
|
||||
IgnoredPaths []string `json:"ignored_paths"`
|
||||
CheckPermPaths []string `json:"check_perm_paths"` // 检查权限的目录
|
||||
|
||||
// 邮件配置
|
||||
EmailConfig EmailConfig `json:"email_config"`
|
||||
}
|
||||
|
||||
type EmailConfig struct {
|
||||
ImapServer string `json:"imap_server"`
|
||||
EmergencyMail []string `json:"emergency_mail"`
|
||||
}
|
||||
|
||||
type SSHMonitor struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
DisplayOnShell bool `yaml:"display_on_shell"`
|
||||
|
|
|
|||
|
|
@ -1,141 +0,0 @@
|
|||
package network
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type ClientConfig struct {
|
||||
ServerURL string
|
||||
SendInterval time.Duration
|
||||
BufferSize int
|
||||
}
|
||||
|
||||
type WSClient struct {
|
||||
config ClientConfig
|
||||
conn *websocket.Conn
|
||||
sendChan chan Packet
|
||||
mu sync.Mutex
|
||||
isConnected bool
|
||||
stopChan chan struct{}
|
||||
}
|
||||
|
||||
func NewWSClient(cfg ClientConfig) *WSClient {
|
||||
if cfg.BufferSize == 0 {
|
||||
cfg.BufferSize = 100
|
||||
}
|
||||
return &WSClient{
|
||||
config: cfg,
|
||||
sendChan: make(chan Packet, cfg.BufferSize),
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *WSClient) Start() {
|
||||
go c.connectionLoop()
|
||||
go c.sendLoop()
|
||||
}
|
||||
|
||||
func (c *WSClient) SendQueue(packet Packet) {
|
||||
select {
|
||||
case c.sendChan <- packet:
|
||||
default:
|
||||
log.Printf("[网络] 发送队列已满,丢弃消息: %s", packet.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *WSClient) Stop() {
|
||||
close(c.stopChan)
|
||||
if c.conn != nil {
|
||||
c.conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *WSClient) sendLoop() {
|
||||
for {
|
||||
select {
|
||||
case <-c.stopChan:
|
||||
return
|
||||
case packet := <-c.sendChan:
|
||||
c.sendRaw(packet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *WSClient) sendRaw(packet Packet) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if !c.isConnected || c.conn == nil {
|
||||
log.Printf("[网络] 无连接,无法发送消息: %s", packet.Type)
|
||||
return
|
||||
}
|
||||
|
||||
c.conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
|
||||
|
||||
payload, _ := json.Marshal(packet)
|
||||
if err := c.conn.WriteMessage(websocket.TextMessage, payload); err != nil {
|
||||
log.Printf("[网络] 发送消息失败: %v", err)
|
||||
c.isConnected = false
|
||||
}
|
||||
}
|
||||
|
||||
func (c *WSClient) connectionLoop() {
|
||||
for {
|
||||
select {
|
||||
case <-c.stopChan:
|
||||
return
|
||||
default:
|
||||
if !c.isConnected {
|
||||
if err := c.connect(); err != nil {
|
||||
log.Printf("[网络] 连接 %s 失败: %v. 5秒后重试...", c.config.ServerURL, err)
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
_, _, err := c.conn.ReadMessage()
|
||||
if err != nil {
|
||||
log.Printf("[网络] 连接断开: %v", err)
|
||||
c.closeConn()
|
||||
}
|
||||
|
||||
// TODO: 处理服务器消息
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *WSClient) closeConn() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.conn != nil {
|
||||
c.conn.Close()
|
||||
c.conn = nil
|
||||
}
|
||||
c.isConnected = false
|
||||
}
|
||||
|
||||
func (c *WSClient) connect() error {
|
||||
conn, _, err := websocket.DefaultDialer.Dial(c.config.ServerURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
select {
|
||||
case <-c.stopChan:
|
||||
conn.Close()
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
|
||||
c.conn = conn
|
||||
c.isConnected = true
|
||||
log.Printf("[网络] 成功连接到服务器: %s", c.config.ServerURL)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
package network
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/wuko233/sysmonitord/internal/config"
|
||||
)
|
||||
|
||||
type ConfigLoader struct {
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewConfigLoader() *ConfigLoader {
|
||||
return &ConfigLoader{
|
||||
client: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (l *ConfigLoader) LoadConfigs(urls ConfigUrls) (config.OfficialConfig, config.UserConfig, error) {
|
||||
var official config.OfficialConfig
|
||||
var user config.UserConfig
|
||||
// 1. 下载官方配置
|
||||
if err := l.fetchJSON(urls.OfficialConfigUrl, &official); err != nil {
|
||||
return official, user, fmt.Errorf("下载官方配置失败: %v", err)
|
||||
}
|
||||
// 2. 下载用户配置
|
||||
if err := l.fetchJSON(urls.UserConfigUrl, &user); err != nil {
|
||||
return official, user, fmt.Errorf("下载用户配置失败: %v", err)
|
||||
}
|
||||
return official, user, nil
|
||||
}
|
||||
|
||||
func (l *ConfigLoader) fetchJSON(url string, target interface{}) error {
|
||||
resp, err := l.client.Get(url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("请求失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("非200响应: %d", resp.StatusCode)
|
||||
}
|
||||
return json.NewDecoder(resp.Body).Decode(target)
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
package network
|
||||
|
||||
import "time"
|
||||
|
||||
type Packet struct {
|
||||
Type string `json:"type"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Payload interface{} `json:"payload"`
|
||||
}
|
||||
|
||||
func NewPacket(msgType string, payload interface{}) Packet {
|
||||
return Packet{
|
||||
Type: msgType,
|
||||
Timestamp: time.Now().Unix(),
|
||||
Payload: payload,
|
||||
}
|
||||
}
|
||||
|
||||
type ConfigUrls struct {
|
||||
OfficialConfigUrl string
|
||||
UserConfigUrl string
|
||||
}
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
package scanner
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/cpu"
|
||||
"github.com/wuko233/sysmonitord/internal/network"
|
||||
"github.com/wuko233/sysmonitord/internal/whitelist"
|
||||
)
|
||||
|
||||
type Scanner struct {
|
||||
wlManager *whitelist.Manager
|
||||
client *network.WSClient
|
||||
cpuLimit float64
|
||||
scanPaths []string
|
||||
stopChan chan struct{}
|
||||
}
|
||||
|
||||
func NewScanner(wl *whitelist.Manager, client *network.WSClient) *Scanner {
|
||||
return &Scanner{
|
||||
wlManager: wl,
|
||||
client: client,
|
||||
cpuLimit: 50.0,
|
||||
scanPaths: []string{"/bin", "/sbin", "/usr/bin", "/usr/sbin", "/etc", "/tmp", "/home"},
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scanner) Start() {
|
||||
log.Println("[扫描器] 启动文件完整性扫描...")
|
||||
go s.scanLoop()
|
||||
}
|
||||
|
||||
func (s *Scanner) scanLoop() {
|
||||
ticker := time.NewTicker(10 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-s.stopChan:
|
||||
return
|
||||
case <-ticker.C:
|
||||
s.performScan()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scanner) performScan() {
|
||||
log.Println("[扫描器] 开始新一轮全盘扫描")
|
||||
fileCount := 0
|
||||
|
||||
for _, root := range s.scanPaths {
|
||||
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||
|
||||
select {
|
||||
case <-s.stopChan:
|
||||
return filepath.SkipDir
|
||||
default:
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Printf("[扫描器] 访问错误: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
fileCount++
|
||||
|
||||
if fileCount%100 == 0 {
|
||||
s.checkCPUAndSleep()
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
if s.wlManager.IsPathIgnored(path) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
isWhitelisted, isHashMatch, err := s.wlManager.CheckFileStatus(path)
|
||||
if err != nil {
|
||||
log.Printf("[扫描器] 检查文件状态失败: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if !isWhitelisted {
|
||||
log.Printf("[扫描器] 发现未在白名单文件: %s", path)
|
||||
s.reportFile(path, "NON_WHITELISTED_FILE")
|
||||
} else if !isHashMatch {
|
||||
log.Printf("[扫描器] 警告!文件Hash不匹配(可能被篡改): %s", path)
|
||||
s.reportFile(path, "FILE_HASH_MISMATCH")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("[扫描器] 扫描目录 %s 出错: %v", root, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scanner) checkCPUAndSleep() {
|
||||
percent, err := cpu.Percent(200*time.Millisecond, false)
|
||||
if err != nil || len(percent) == 0 {
|
||||
log.Printf("[扫描器] 获取CPU使用率失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if percent[0] > s.cpuLimit {
|
||||
log.Printf("[扫描器] CPU使用率过高 (%.2f%%),暂停扫描5秒", percent[0])
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
func (s *Scanner) reportFile(path string, alertType string) {
|
||||
payload := map[string]interface{}{
|
||||
"filepath": path,
|
||||
"status": "detected",
|
||||
}
|
||||
|
||||
packet := network.NewPacket(alertType, payload)
|
||||
|
||||
s.client.SendQueue(packet)
|
||||
}
|
||||
|
||||
func (s *Scanner) Stop() {
|
||||
log.Println("[扫描器] 停止文件完整性扫描...")
|
||||
close(s.stopChan)
|
||||
}
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
package scanner
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/wuko233/sysmonitord/internal/network"
|
||||
"github.com/wuko233/sysmonitord/internal/whitelist"
|
||||
)
|
||||
|
||||
type Watcher struct {
|
||||
wlManager *whitelist.Manager
|
||||
client *network.WSClient
|
||||
watcher *fsnotify.Watcher
|
||||
stopChan chan struct{}
|
||||
watchPaths []string
|
||||
}
|
||||
|
||||
func NewWatcher(wl *whitelist.Manager, client *network.WSClient) (*Watcher, error) {
|
||||
fsWatch, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Watcher{
|
||||
wlManager: wl,
|
||||
client: client,
|
||||
watcher: fsWatch,
|
||||
stopChan: make(chan struct{}),
|
||||
|
||||
// TODO: 当前仅实现对主目录的监控,后续实现递归监控子目录
|
||||
watchPaths: []string{
|
||||
"/bin", "/sbin", "/usr/bin", "/etc/init.d", "/tmp",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (w *Watcher) Start() {
|
||||
log.Println("[监听器] 启动实时文件监控...")
|
||||
|
||||
for _, path := range w.watchPaths {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
if err := w.watcher.Add(path); err != nil {
|
||||
log.Printf("[监听器] 无法监控路径 %s: %v", path, err)
|
||||
} else {
|
||||
log.Printf("[监听器] 开始监控路径: %s", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
go w.eventLoop()
|
||||
}
|
||||
|
||||
func (w *Watcher) eventLoop() {
|
||||
for {
|
||||
select {
|
||||
case <-w.stopChan:
|
||||
return
|
||||
case event, ok := <-w.watcher.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if event.Has(fsnotify.Create) || event.Has(fsnotify.Write) {
|
||||
if w.wlManager.IsPathIgnored(event.Name) {
|
||||
continue
|
||||
}
|
||||
|
||||
go w.handleFileChange(event.Name, event.Op.String())
|
||||
}
|
||||
case err, ok := <-w.watcher.Errors:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
log.Printf("[监听器] 错误: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Watcher) handleFileChange(path string, op string) {
|
||||
|
||||
time.Sleep(200 * time.Millisecond) // 等待文件写入完成
|
||||
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
|
||||
isWhitelisted, isHashMatch, err := w.wlManager.CheckFileStatus(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !isWhitelisted {
|
||||
log.Printf("[监听器] 实时拦截:检测到非白名单文件变动 (%s): %s", op, path)
|
||||
w.reportEvent(path, "REALTIME_FILE_ALERT", op)
|
||||
} else if !isHashMatch {
|
||||
log.Printf("[监听器] 实时拦截:检测到白名单文件被篡改 (%s): %s", op, path)
|
||||
w.reportEvent(path, "REALTIME_HASH_MISMATCH", op)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Watcher) reportEvent(path, alertType, op string) {
|
||||
payload := map[string]interface{}{
|
||||
"filepath": path,
|
||||
"operation": op,
|
||||
"time": time.Now(),
|
||||
}
|
||||
|
||||
packet := network.NewPacket(alertType, payload)
|
||||
w.client.SendQueue(packet)
|
||||
}
|
||||
|
||||
func (w *Watcher) Stop() {
|
||||
log.Println("[监听器] 停止实时文件监控...")
|
||||
close(w.stopChan)
|
||||
w.watcher.Close()
|
||||
}
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
package whitelist
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/wuko233/sysmonitord/internal/config"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
mu sync.RWMutex
|
||||
official config.OfficialConfig
|
||||
user config.UserConfig
|
||||
mergedIgnore []string
|
||||
}
|
||||
|
||||
func NewManager() *Manager {
|
||||
return &Manager{
|
||||
official: config.OfficialConfig{
|
||||
WhitelistFiles: make(map[string][]string),
|
||||
},
|
||||
user: config.UserConfig{
|
||||
SupplementFiles: make(map[string][]string),
|
||||
SupplementProcesses: make(map[string]string),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) UpdateConfig(official config.OfficialConfig, user config.UserConfig) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.official = official
|
||||
m.user = user
|
||||
|
||||
m.mergedIgnore = append([]string{}, m.official.IgnoredPaths...)
|
||||
m.mergedIgnore = append(m.mergedIgnore, m.user.IgnoredPaths...)
|
||||
}
|
||||
|
||||
func (m *Manager) IsPathIgnored(path string) bool {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
return m.IsPathIgnoredUnsafe(path)
|
||||
}
|
||||
|
||||
func (m *Manager) IsPathIgnoredUnsafe(path string) bool {
|
||||
path = filepath.Clean(path)
|
||||
for _, ignore := range m.mergedIgnore {
|
||||
if strings.HasPrefix(path, filepath.Clean(ignore)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// CheckFileStatus 检查文件状态
|
||||
// 返回: isWhitelisted(是否在白名单), isValid(Hash是否匹配), err
|
||||
func (m *Manager) CheckFileStatus(path string) (bool, bool, error) {
|
||||
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
if m.IsPathIgnoredUnsafe((path)) {
|
||||
return true, true, nil
|
||||
}
|
||||
|
||||
hashes, exists := m.official.WhitelistFiles[path]
|
||||
if !exists {
|
||||
hashes, exists = m.user.SupplementFiles[path]
|
||||
}
|
||||
if !exists {
|
||||
return false, false, nil
|
||||
}
|
||||
|
||||
fileHash, err := CalculateFileHash(path)
|
||||
if err != nil {
|
||||
return true, false, fmt.Errorf("计算文件哈希失败: %v", err)
|
||||
}
|
||||
|
||||
for _, h := range hashes {
|
||||
if strings.EqualFold(h, fmt.Sprintf("%v", fileHash)) {
|
||||
return true, true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return true, false, nil
|
||||
}
|
||||
|
||||
func CalculateFileHash(filePath string) (string, error) {
|
||||
file, err := os.Open(filePath)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
hash := sha256.New()
|
||||
if _, err := io.Copy(hash, file); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return hex.EncodeToString(hash.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func (m *Manager) IsProcessAllowed(procName string, cmdLine string) bool {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
for _, p := range m.official.WhitelistProcesses {
|
||||
if p == procName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := m.user.SupplementProcesses[procName]; ok {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Manager) GetAuditServerUrl() string {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
return m.user.AuditServerUrl
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user