探针中增加目录防篡改
This commit is contained in:
1 parent
b05c0beaae
commit
0cc12c2416
6 files changed
+1414
-3
No files matched your search
@@ -4,6 +4,7 @@ go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/coreos/go-oidc/v3 v3.16.0
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/go-errors/errors v1.5.1
|
||||
github.com/go-orz/orz v0.2.9
|
||||
github.com/go-orz/toolkit v0.1.0
|
||||
@@ -32,7 +33,6 @@ require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/ebitengine/purego v0.9.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
|
||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||
github.com/glebarez/sqlite v1.11.0 // indirect
|
||||
|
||||
@@ -50,6 +50,9 @@ const (
|
||||
// 指标消息
|
||||
MessageTypeMetrics MessageType = "metrics"
|
||||
MessageTypeMonitorConfig MessageType = "monitor_config"
|
||||
// 防篡改消息
|
||||
MessageTypeTamperProtect MessageType = "tamper_protect"
|
||||
MessageTypeTamperEvent MessageType = "tamper_event"
|
||||
)
|
||||
|
||||
type MetricType string
|
||||
@@ -262,3 +265,26 @@ type MonitorData struct {
|
||||
CertExpiryTime int64 `json:"certExpiryTime,omitempty"` // 证书过期时间(毫秒时间戳)
|
||||
CertDaysLeft int `json:"certDaysLeft,omitempty"` // 证书剩余天数
|
||||
}
|
||||
|
||||
// TamperProtectConfig 防篡改保护配置
|
||||
type TamperProtectConfig struct {
|
||||
Paths []string `json:"paths"` // 要保护的目录列表(完整列表)
|
||||
}
|
||||
|
||||
// TamperProtectResponse 防篡改保护响应
|
||||
type TamperProtectResponse struct {
|
||||
Success bool `json:"success"` // 是否成功
|
||||
Message string `json:"message"` // 响应消息
|
||||
Paths []string `json:"paths"` // 当前保护的目录列表
|
||||
Added []string `json:"added,omitempty"` // 新增的目录
|
||||
Removed []string `json:"removed,omitempty"` // 移除的目录
|
||||
Error string `json:"error,omitempty"` // 错误信息
|
||||
}
|
||||
|
||||
// TamperEventData 防篡改事件数据
|
||||
type TamperEventData struct {
|
||||
Path string `json:"path"` // 被修改的路径
|
||||
Operation string `json:"operation"` // 操作类型: write, remove, rename, chmod, create
|
||||
Timestamp int64 `json:"timestamp"` // 事件时间(毫秒)
|
||||
Details string `json:"details"` // 详细信息
|
||||
}
|
||||
+133
-2
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/dushixiang/pika/pkg/agent/collector"
|
||||
"github.com/dushixiang/pika/pkg/agent/config"
|
||||
"github.com/dushixiang/pika/pkg/agent/id"
|
||||
"github.com/dushixiang/pika/pkg/agent/tamper"
|
||||
"github.com/dushixiang/pika/pkg/version"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/jpillora/backoff"
|
||||
@@ -60,13 +61,15 @@ type Agent struct {
|
||||
activeConn *safeConn
|
||||
collectorMu sync.RWMutex
|
||||
collectorManager *collector.Manager
|
||||
tamperProtector *tamper.Protector
|
||||
}
|
||||
|
||||
// New 创建 Agent 实例
|
||||
func New(cfg *config.Config) *Agent {
|
||||
return &Agent{
|
||||
cfg: cfg,
|
||||
idMgr: id.NewManager(),
|
||||
cfg: cfg,
|
||||
idMgr: id.NewManager(),
|
||||
tamperProtector: tamper.NewProtector(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,6 +195,11 @@ func (a *Agent) runOnce(ctx context.Context) error {
|
||||
}
|
||||
}()
|
||||
|
||||
// 启动防篡改事件监控
|
||||
go func() {
|
||||
a.tamperEventLoop(ctx, conn, done)
|
||||
}()
|
||||
|
||||
// 等待错误或上下文取消
|
||||
select {
|
||||
case err := <-errChan:
|
||||
@@ -242,6 +250,8 @@ func (a *Agent) readLoop(conn *websocket.Conn, done chan struct{}) error {
|
||||
go a.handleCommand(msg.Data)
|
||||
case protocol.MessageTypeMonitorConfig:
|
||||
go a.handleMonitorConfig(msg.Data)
|
||||
case protocol.MessageTypeTamperProtect:
|
||||
go a.handleTamperProtect(msg.Data)
|
||||
default:
|
||||
// 忽略其他类型
|
||||
}
|
||||
@@ -576,3 +586,124 @@ func (a *Agent) sendCommandResponse(conn *safeConn, cmdID, cmdType, status, errM
|
||||
func GetVersion() string {
|
||||
return version.GetVersion()
|
||||
}
|
||||
|
||||
// handleTamperProtect 处理防篡改保护指令
|
||||
func (a *Agent) handleTamperProtect(data json.RawMessage) {
|
||||
var config protocol.TamperProtectConfig
|
||||
if err := json.Unmarshal(data, &config); err != nil {
|
||||
log.Printf("⚠️ 解析防篡改保护配置失败: %v", err)
|
||||
a.sendTamperProtectResponse(false, "解析配置失败", nil, nil, nil, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("📥 收到防篡改保护配置: Paths=%v", config.Paths)
|
||||
|
||||
conn := a.getActiveConn()
|
||||
if conn == nil {
|
||||
log.Println("⚠️ 当前连接未就绪,无法执行防篡改保护")
|
||||
return
|
||||
}
|
||||
|
||||
// 如果配置为空列表,停止所有保护
|
||||
if len(config.Paths) == 0 {
|
||||
if err := a.tamperProtector.StopAll(); err != nil {
|
||||
log.Printf("❌ 停止所有防篡改保护失败: %v", err)
|
||||
a.sendTamperProtectResponse(false, "停止所有保护失败", []string{}, nil, nil, err.Error())
|
||||
return
|
||||
}
|
||||
log.Println("✅ 已停止所有防篡改保护")
|
||||
a.sendTamperProtectResponse(true, "已停止所有防篡改保护", []string{}, []string{}, a.tamperProtector.GetProtectedPaths(), "")
|
||||
return
|
||||
}
|
||||
|
||||
// 更新保护目录列表
|
||||
ctx := context.Background()
|
||||
result, err := a.tamperProtector.UpdatePaths(ctx, config.Paths)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ 更新防篡改保护失败: %v", err)
|
||||
// 即使有错误也返回部分成功的结果
|
||||
if result != nil {
|
||||
a.sendTamperProtectResponse(false, "部分更新失败", result.Current, result.Added, result.Removed, err.Error())
|
||||
} else {
|
||||
a.sendTamperProtectResponse(false, "更新失败", nil, nil, nil, err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 成功更新
|
||||
message := fmt.Sprintf("防篡改保护已更新: 新增 %d 个, 移除 %d 个, 当前保护 %d 个目录",
|
||||
len(result.Added), len(result.Removed), len(result.Current))
|
||||
log.Printf("✅ %s", message)
|
||||
a.sendTamperProtectResponse(true, message, result.Current, result.Added, result.Removed, "")
|
||||
}
|
||||
|
||||
// sendTamperProtectResponse 发送防篡改保护响应
|
||||
func (a *Agent) sendTamperProtectResponse(success bool, message string, paths []string, added []string, removed []string, errMsg string) {
|
||||
conn := a.getActiveConn()
|
||||
if conn == nil {
|
||||
return
|
||||
}
|
||||
|
||||
resp := protocol.TamperProtectResponse{
|
||||
Success: success,
|
||||
Message: message,
|
||||
Paths: paths,
|
||||
Added: added,
|
||||
Removed: removed,
|
||||
Error: errMsg,
|
||||
}
|
||||
|
||||
respData, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ 序列化防篡改保护响应失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
msg := protocol.Message{
|
||||
Type: protocol.MessageTypeTamperProtect,
|
||||
Data: respData,
|
||||
}
|
||||
|
||||
if err := conn.WriteJSON(msg); err != nil {
|
||||
log.Printf("⚠️ 发送防篡改保护响应失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// tamperEventLoop 防篡改事件监控循环
|
||||
func (a *Agent) tamperEventLoop(ctx context.Context, conn *safeConn, done chan struct{}) {
|
||||
eventCh := a.tamperProtector.GetEvents()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case event := <-eventCh:
|
||||
// 发送防篡改事件到服务端
|
||||
eventData := protocol.TamperEventData{
|
||||
Path: event.Path,
|
||||
Operation: event.Operation,
|
||||
Timestamp: event.Timestamp.UnixMilli(),
|
||||
Details: event.Details,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(eventData)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ 序列化防篡改事件失败: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
msg := protocol.Message{
|
||||
Type: protocol.MessageTypeTamperEvent,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
if err := conn.WriteJSON(msg); err != nil {
|
||||
log.Printf("⚠️ 发送防篡改事件失败: %v", err)
|
||||
} else {
|
||||
log.Printf("📤 已上报防篡改事件: %s - %s", event.Path, event.Operation)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,518 @@
|
||||
# 防篡改保护模块
|
||||
|
||||
## 功能概述
|
||||
|
||||
该模块为 Pika Agent 提供 Linux 文件系统防篡改保护功能。通过以下机制实现:
|
||||
|
||||
1. **文件属性保护**: 使用 `chattr +i` 命令将指定目录设置为不可变(immutable)属性
|
||||
2. **实时监控**: 使用 `fsnotify` 库监控受保护目录的文件系统事件
|
||||
3. **事件上报**: 将检测到的文件变动事件实时上报到服务端
|
||||
4. **动态管理**: 支持动态增加、移除保护目录,自动计算差异并应用
|
||||
|
||||
## 系统要求
|
||||
|
||||
- **操作系统**: 仅支持 Linux 系统
|
||||
- **权限**: 需要 root 权限才能执行 `chattr` 命令
|
||||
- **依赖**: `github.com/fsnotify/fsnotify`
|
||||
|
||||
## 工作原理
|
||||
|
||||
### 动态目录管理
|
||||
|
||||
服务端每次发送的是**完整的目录列表**,Agent 会自动计算需要新增和移除的目录:
|
||||
|
||||
#### 场景示例
|
||||
|
||||
**第一次配置**: `/a /b /c`
|
||||
```json
|
||||
{
|
||||
"type": "tamper_protect",
|
||||
"data": {
|
||||
"paths": ["/a", "/b", "/c"]
|
||||
}
|
||||
}
|
||||
```
|
||||
- Agent 操作: 新增保护 `/a /b /c`
|
||||
- 响应: `added: ["/a", "/b", "/c"], removed: [], current: ["/a", "/b", "/c"]`
|
||||
|
||||
**第二次配置**: `/a /b` (移除 /c)
|
||||
```json
|
||||
{
|
||||
"type": "tamper_protect",
|
||||
"data": {
|
||||
"paths": ["/a", "/b"]
|
||||
}
|
||||
}
|
||||
```
|
||||
- Agent 操作: 移除 `/c` 的保护 (执行 `chattr -i /c`)
|
||||
- 响应: `added: [], removed: ["/c"], current: ["/a", "/b"]`
|
||||
|
||||
**第三次配置**: `/a /b /c /d` (新增 /c /d)
|
||||
```json
|
||||
{
|
||||
"type": "tamper_protect",
|
||||
"data": {
|
||||
"paths": ["/a", "/b", "/c", "/d"]
|
||||
}
|
||||
}
|
||||
```
|
||||
- Agent 操作: 新增保护 `/c /d` (执行 `chattr +i /c` 和 `chattr +i /d`)
|
||||
- 响应: `added: ["/c", "/d"], removed: [], current: ["/a", "/b", "/c", "/d"]`
|
||||
|
||||
**第四次配置**: `[]` (清空所有保护)
|
||||
```json
|
||||
{
|
||||
"type": "tamper_protect",
|
||||
"data": {
|
||||
"paths": []
|
||||
}
|
||||
}
|
||||
```
|
||||
- Agent 操作: 移除所有目录的保护
|
||||
- 响应: `added: [], removed: ["/a", "/b", "/c", "/d"], current: []`
|
||||
|
||||
### 核心流程
|
||||
|
||||
```
|
||||
服务端发送完整列表
|
||||
↓
|
||||
Agent 计算差异
|
||||
• 新列表 - 当前列表 = 需要新增
|
||||
• 当前列表 - 新列表 = 需要移除
|
||||
↓
|
||||
执行操作
|
||||
• 对移除的目录: chattr -i + watcher.Remove()
|
||||
• 对新增的目录: chattr +i + watcher.Add()
|
||||
↓
|
||||
返回结果
|
||||
• 成功/失败状态
|
||||
• 新增的目录列表
|
||||
• 移除的目录列表
|
||||
• 当前所有保护的目录
|
||||
```
|
||||
|
||||
## 核心 API
|
||||
|
||||
### Protector.UpdatePaths()
|
||||
|
||||
```go
|
||||
func (p *Protector) UpdatePaths(ctx context.Context, newPaths []string) (*UpdateResult, error)
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `newPaths`: 新的完整目录列表
|
||||
|
||||
**返回**:
|
||||
- `UpdateResult`: 更新结果
|
||||
- `Added`: 本次新增保护的目录
|
||||
- `Removed`: 本次移除保护的目录
|
||||
- `Current`: 当前所有受保护的目录
|
||||
- `error`: 错误信息(如果部分操作失败,仍会返回 UpdateResult)
|
||||
|
||||
**特性**:
|
||||
- **幂等性**: 多次使用相同参数调用,结果一致
|
||||
- **原子性**: 单个目录的添加/移除操作失败不影响其他目录
|
||||
- **智能差异**: 自动计算需要变更的目录,避免重复操作
|
||||
|
||||
### Protector.StopAll()
|
||||
|
||||
```go
|
||||
func (p *Protector) StopAll() error
|
||||
```
|
||||
|
||||
停止所有防篡改保护,移除所有目录的不可变属性,关闭文件监控器。
|
||||
|
||||
### Protector.GetProtectedPaths()
|
||||
|
||||
```go
|
||||
func (p *Protector) GetProtectedPaths() []string
|
||||
```
|
||||
|
||||
获取当前所有受保护的目录列表。
|
||||
|
||||
### Protector.IsProtected()
|
||||
|
||||
```go
|
||||
func (p *Protector) IsProtected(path string) bool
|
||||
```
|
||||
|
||||
检查指定路径是否受保护。
|
||||
|
||||
## Protocol 定义
|
||||
|
||||
### 消息类型
|
||||
|
||||
```go
|
||||
MessageTypeTamperProtect MessageType = "tamper_protect"
|
||||
MessageTypeTamperEvent MessageType = "tamper_event"
|
||||
```
|
||||
|
||||
### 数据结构
|
||||
|
||||
#### 配置请求
|
||||
```go
|
||||
type TamperProtectConfig struct {
|
||||
Paths []string // 要保护的目录列表(完整列表)
|
||||
}
|
||||
```
|
||||
|
||||
#### 配置响应
|
||||
```go
|
||||
type TamperProtectResponse struct {
|
||||
Success bool // 是否成功
|
||||
Message string // 响应消息
|
||||
Paths []string // 当前保护的目录列表
|
||||
Added []string // 本次新增的目录
|
||||
Removed []string // 本次移除的目录
|
||||
Error string // 错误信息
|
||||
}
|
||||
```
|
||||
|
||||
#### 事件数据
|
||||
```go
|
||||
type TamperEventData struct {
|
||||
Path string // 被修改的路径
|
||||
Operation string // 操作类型: write, remove, rename, chmod, create
|
||||
Timestamp int64 // 事件时间(毫秒)
|
||||
Details string // 详细信息
|
||||
}
|
||||
```
|
||||
|
||||
## Agent 集成
|
||||
|
||||
在 `pkg/agent/service/agent.go` 中的集成:
|
||||
|
||||
```go
|
||||
// 初始化
|
||||
agent.tamperProtector = tamper.NewProtector()
|
||||
|
||||
// 接收配置消息
|
||||
case protocol.MessageTypeTamperProtect:
|
||||
go a.handleTamperProtect(msg.Data)
|
||||
|
||||
// 处理配置
|
||||
func (a *Agent) handleTamperProtect(data json.RawMessage) {
|
||||
var config protocol.TamperProtectConfig
|
||||
// ...解析配置
|
||||
|
||||
if len(config.Paths) == 0 {
|
||||
// 清空所有保护
|
||||
a.tamperProtector.StopAll()
|
||||
} else {
|
||||
// 更新保护目录
|
||||
result, err := a.tamperProtector.UpdatePaths(ctx, config.Paths)
|
||||
// ...发送响应
|
||||
}
|
||||
}
|
||||
|
||||
// 事件监控循环
|
||||
func (a *Agent) tamperEventLoop(ctx context.Context, conn *safeConn, done chan struct{}) {
|
||||
eventCh := a.tamperProtector.GetEvents()
|
||||
for event := range eventCh {
|
||||
// 上报事件到服务端
|
||||
sendTamperEvent(event)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 服务端: 第一次配置
|
||||
|
||||
```go
|
||||
config := protocol.TamperProtectConfig{
|
||||
Paths: []string{"/etc/nginx", "/var/www"},
|
||||
}
|
||||
|
||||
// 发送到 Agent
|
||||
sendMessage(protocol.MessageTypeTamperProtect, config)
|
||||
|
||||
// 期望响应
|
||||
// {
|
||||
// "success": true,
|
||||
// "message": "防篡改保护已更新: 新增 2 个, 移除 0 个, 当前保护 2 个目录",
|
||||
// "paths": ["/etc/nginx", "/var/www"],
|
||||
// "added": ["/etc/nginx", "/var/www"],
|
||||
// "removed": []
|
||||
// }
|
||||
```
|
||||
|
||||
### 服务端: 修改配置(移除一个目录)
|
||||
|
||||
```go
|
||||
config := protocol.TamperProtectConfig{
|
||||
Paths: []string{"/etc/nginx"}, // 只保留一个
|
||||
}
|
||||
|
||||
sendMessage(protocol.MessageTypeTamperProtect, config)
|
||||
|
||||
// 期望响应
|
||||
// {
|
||||
// "success": true,
|
||||
// "message": "防篡改保护已更新: 新增 0 个, 移除 1 个, 当前保护 1 个目录",
|
||||
// "paths": ["/etc/nginx"],
|
||||
// "added": [],
|
||||
// "removed": ["/var/www"]
|
||||
// }
|
||||
```
|
||||
|
||||
### 服务端: 添加新目录
|
||||
|
||||
```go
|
||||
config := protocol.TamperProtectConfig{
|
||||
Paths: []string{"/etc/nginx", "/etc/ssh", "/opt/app"},
|
||||
}
|
||||
|
||||
sendMessage(protocol.MessageTypeTamperProtect, config)
|
||||
|
||||
// 期望响应
|
||||
// {
|
||||
// "success": true,
|
||||
// "message": "防篡改保护已更新: 新增 2 个, 移除 0 个, 当前保护 3 个目录",
|
||||
// "paths": ["/etc/nginx", "/etc/ssh", "/opt/app"],
|
||||
// "added": ["/etc/ssh", "/opt/app"],
|
||||
// "removed": []
|
||||
// }
|
||||
```
|
||||
|
||||
### 服务端: 清空所有保护
|
||||
|
||||
```go
|
||||
config := protocol.TamperProtectConfig{
|
||||
Paths: []string{}, // 空列表
|
||||
}
|
||||
|
||||
sendMessage(protocol.MessageTypeTamperProtect, config)
|
||||
|
||||
// 期望响应
|
||||
// {
|
||||
// "success": true,
|
||||
// "message": "已停止所有防篡改保护",
|
||||
// "paths": [],
|
||||
// "added": [],
|
||||
// "removed": ["/etc/nginx", "/etc/ssh", "/opt/app"]
|
||||
// }
|
||||
```
|
||||
|
||||
### 服务端: 接收事件通知
|
||||
|
||||
```go
|
||||
// 监听事件消息
|
||||
case protocol.MessageTypeTamperEvent:
|
||||
var event protocol.TamperEventData
|
||||
json.Unmarshal(msg.Data, &event)
|
||||
|
||||
// 处理防篡改事件
|
||||
log.Printf("检测到篡改: %s - %s at %d",
|
||||
event.Path, event.Operation, event.Timestamp)
|
||||
|
||||
// 可以进行告警、记录日志等操作
|
||||
sendAlert(event)
|
||||
```
|
||||
|
||||
## 测试
|
||||
|
||||
### 运行测试
|
||||
|
||||
```bash
|
||||
# 运行所有测试(在非 Linux 系统上部分测试会跳过)
|
||||
go test -v ./pkg/agent/tamper/
|
||||
|
||||
# 在 Linux 系统上以 root 权限运行完整测试
|
||||
sudo go test -v ./pkg/agent/tamper/
|
||||
```
|
||||
|
||||
### 测试场景
|
||||
|
||||
测试覆盖了完整的动态管理场景:
|
||||
|
||||
1. ✅ **第一次配置 `/a /b /c`**: 验证新增 3 个目录
|
||||
2. ✅ **第二次配置 `/a /b`**: 验证移除 1 个目录
|
||||
3. ✅ **第三次配置 `/a /b /c /d`**: 验证新增 2 个目录
|
||||
4. ✅ **第四次配置 `[]`**: 验证移除所有目录
|
||||
5. ✅ **重复配置**: 验证幂等性(无变化时不执行操作)
|
||||
6. ✅ **事件监控**: 验证文件变动事件上报
|
||||
7. ✅ **JSON 序列化**: 验证数据结构正确性
|
||||
|
||||
## 设计优势
|
||||
|
||||
### 1. 简化服务端逻辑
|
||||
|
||||
服务端无需维护状态,每次只需发送完整的目录列表,差异计算由 Agent 完成:
|
||||
|
||||
```
|
||||
❌ 复杂的方式(需要服务端计算差异):
|
||||
- 服务端需要知道 Agent 当前保护的目录
|
||||
- 需要对比计算差异
|
||||
- 发送增删指令
|
||||
|
||||
✅ 简单的方式(Agent 计算差异):
|
||||
- 服务端只需发送完整列表
|
||||
- Agent 自动计算并应用差异
|
||||
- 响应中包含完整的变更信息
|
||||
```
|
||||
|
||||
### 2. 幂等性保证
|
||||
|
||||
相同的配置多次发送不会产生副作用:
|
||||
|
||||
```go
|
||||
// 第一次
|
||||
UpdatePaths(["/a", "/b"]) → added: ["/a", "/b"]
|
||||
|
||||
// 第二次(相同配置)
|
||||
UpdatePaths(["/a", "/b"]) → added: [], removed: [] (无操作)
|
||||
```
|
||||
|
||||
### 3. 容错性
|
||||
|
||||
部分目录操作失败不影响其他目录:
|
||||
|
||||
```go
|
||||
result, err := UpdatePaths(["/valid", "/invalid", "/valid2"])
|
||||
// 即使 /invalid 失败,/valid 和 /valid2 仍然会被保护
|
||||
// result.Added = ["/valid", "/valid2"]
|
||||
// err 会包含失败信息
|
||||
```
|
||||
|
||||
### 4. 清晰的状态反馈
|
||||
|
||||
响应中包含完整的变更信息,便于服务端验证:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"paths": ["/a", "/b", "/c"], // 当前状态
|
||||
"added": ["/c"], // 本次新增
|
||||
"removed": ["/d"] // 本次移除
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **权限要求**: 必须以 root 权限运行 Agent,否则 `chattr` 命令会失败
|
||||
2. **操作系统限制**: 仅支持 Linux 系统,在其他系统上会返回错误
|
||||
3. **文件系统支持**: 目标文件系统必须支持 chattr 属性(如 ext2/ext3/ext4/xfs)
|
||||
4. **性能影响**:
|
||||
- `chattr +i` 会阻止所有写操作,包括合法的系统更新
|
||||
- 文件监控会消耗一定的系统资源
|
||||
5. **事件队列**: 事件通道缓冲区大小为 100,如果处理不及时可能丢失事件
|
||||
6. **并发安全**: 使用了读写锁保护共享状态,支持并发操作
|
||||
7. **watcher 生命周期**: 首次调用 UpdatePaths 时创建,调用 StopAll 时销毁
|
||||
|
||||
## 安全建议
|
||||
|
||||
1. **谨慎选择保护路径**: 不要保护系统关键目录(如 `/`, `/usr`, `/bin`),可能导致系统无法正常运行
|
||||
2. **定期审查**: 定期检查防篡改事件,及时发现异常行为
|
||||
3. **配合其他安全措施**: 防篡改保护应作为纵深防御的一部分,配合防火墙、入侵检测等
|
||||
4. **备份重要文件**: 在启用保护前备份重要配置文件
|
||||
5. **测试环境验证**: 先在测试环境验证,确认不影响业务后再部署到生产环境
|
||||
|
||||
## 故障排查
|
||||
|
||||
### chattr 命令失败
|
||||
|
||||
```
|
||||
错误: 执行 chattr 失败: permission denied
|
||||
解决: 确保以 root 权限运行 Agent
|
||||
```
|
||||
|
||||
### 不支持的文件系统
|
||||
|
||||
```
|
||||
错误: 执行 chattr 失败: Inappropriate ioctl for device
|
||||
解决: 检查目标目录所在的文件系统是否支持 chattr 属性
|
||||
```
|
||||
|
||||
### 事件未上报
|
||||
|
||||
```
|
||||
问题: 配置了防篡改保护,但没有收到事件
|
||||
排查:
|
||||
1. 检查 Agent 日志,确认保护已启用
|
||||
2. 确认文件确实发生了变动
|
||||
3. 检查事件队列是否已满
|
||||
4. 检查网络连接是否正常
|
||||
```
|
||||
|
||||
### 部分目录操作失败
|
||||
|
||||
```
|
||||
响应: {
|
||||
"success": false,
|
||||
"error": "部分操作失败: 添加失败 1 个, 移除失败 0 个",
|
||||
"paths": ["/valid"],
|
||||
"added": ["/valid"],
|
||||
"removed": []
|
||||
}
|
||||
|
||||
处理:
|
||||
1. 检查失败目录的权限
|
||||
2. 检查失败目录是否存在
|
||||
3. 检查文件系统类型
|
||||
4. 查看 Agent 日志获取详细错误
|
||||
```
|
||||
|
||||
## 架构图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 服务端 │
|
||||
│ │
|
||||
│ 管理员配置: ["/etc/nginx", "/var/www", "/opt/app"] │
|
||||
│ ↓ │
|
||||
│ 发送完整列表(TamperProtectConfig) │
|
||||
└────────────────────────┬────────────────────────────────────┘
|
||||
│ WebSocket
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Agent │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────┐ │
|
||||
│ │ Protector.UpdatePaths() │ │
|
||||
│ │ │ │
|
||||
│ │ 1. 计算差异: │ │
|
||||
│ │ • 新列表 - 当前 = 需要新增 │ │
|
||||
│ │ • 当前 - 新列表 = 需要移除 │ │
|
||||
│ │ │ │
|
||||
│ │ 2. 执行操作: │ │
|
||||
│ │ 移除: chattr -i + watcher.Remove() │ │
|
||||
│ │ 新增: chattr +i + watcher.Add() │ │
|
||||
│ │ │ │
|
||||
│ │ 3. 返回结果: │ │
|
||||
│ │ { added: [...], removed: [...], current: [...] } │ │
|
||||
│ └────────────────────────────────────────────────────┘ │
|
||||
│ ↓ │
|
||||
│ ┌────────────────────────────────────────────────────┐ │
|
||||
│ │ 文件监控(fsnotify.Watcher) │ │
|
||||
│ │ │ │
|
||||
│ │ 监控所有受保护的目录 │ │
|
||||
│ │ ↓ │ │
|
||||
│ │ 检测到文件变动 │ │
|
||||
│ │ ↓ │ │
|
||||
│ │ 生成 TamperEvent │ │
|
||||
│ │ ↓ │ │
|
||||
│ │ 通过 channel 发送 │ │
|
||||
│ └────────────────────────────────────────────────────┘ │
|
||||
│ ↓ │
|
||||
│ 上报事件到服务端(TamperEventData) │
|
||||
└────────────────────────┬────────────────────────────────────┘
|
||||
│ WebSocket
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 服务端 │
|
||||
│ │
|
||||
│ 接收事件 → 告警 → 日志 → 审计 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 未来改进
|
||||
|
||||
- [ ] 支持递归监控子目录
|
||||
- [ ] 支持白名单机制(允许特定进程修改)
|
||||
- [ ] 增加事件过滤器(过滤掉不重要的事件)
|
||||
- [ ] 支持更多文件系统属性(如 append-only)
|
||||
- [ ] 增加性能监控指标
|
||||
- [ ] 支持 Windows 平台(使用不同的实现机制)
|
||||
- [ ] 支持配置持久化(Agent 重启后恢复保护状态)
|
||||
- [ ] 支持按文件粒度保护(不仅仅是目录)
|
||||
@@ -0,0 +1,379 @@
|
||||
package tamper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
// TamperEvent 防篡改事件
|
||||
type TamperEvent struct {
|
||||
Path string `json:"path"` // 被修改的路径
|
||||
Operation string `json:"operation"` // 操作类型: write, remove, rename, chmod
|
||||
Timestamp time.Time `json:"timestamp"` // 事件时间
|
||||
Details string `json:"details"` // 详细信息
|
||||
}
|
||||
|
||||
// AttributeTamperAlert 属性篡改告警
|
||||
type AttributeTamperAlert struct {
|
||||
Path string `json:"path"` // 被篡改的路径
|
||||
Timestamp time.Time `json:"timestamp"` // 检测时间
|
||||
Details string `json:"details"` // 详细信息(如: "不可变属性被移除")
|
||||
Restored bool `json:"restored"` // 是否已自动恢复
|
||||
}
|
||||
|
||||
// UpdateResult 更新结果
|
||||
type UpdateResult struct {
|
||||
Added []string // 新增保护的目录
|
||||
Removed []string // 移除保护的目录
|
||||
Current []string // 当前所有保护的目录
|
||||
}
|
||||
|
||||
// Protector 防篡改保护器
|
||||
type Protector struct {
|
||||
mu sync.RWMutex
|
||||
paths map[string]bool // 当前保护的目录集合(使用 map 便于查找)
|
||||
watcher *fsnotify.Watcher
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
eventCh chan TamperEvent
|
||||
alertCh chan AttributeTamperAlert // 属性篡改告警通道
|
||||
watcherOnce sync.Once // 确保 watcher 只创建一次
|
||||
patrolOnce sync.Once // 确保巡检只启动一次
|
||||
}
|
||||
|
||||
// NewProtector 创建防篡改保护器
|
||||
func NewProtector() *Protector {
|
||||
return &Protector{
|
||||
paths: make(map[string]bool),
|
||||
eventCh: make(chan TamperEvent, 100),
|
||||
}
|
||||
}
|
||||
|
||||
// UpdatePaths 更新保护的目录列表
|
||||
// 参数 newPaths: 新的完整目录列表
|
||||
// 返回: 更新结果(新增/移除的目录)和错误
|
||||
func (p *Protector) UpdatePaths(ctx context.Context, newPaths []string) (*UpdateResult, error) {
|
||||
// 检查操作系统
|
||||
if runtime.GOOS != "linux" {
|
||||
return nil, fmt.Errorf("防篡改功能仅支持 Linux 系统")
|
||||
}
|
||||
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
// 转换为 map 便于比较
|
||||
newPathsMap := make(map[string]bool)
|
||||
for _, path := range newPaths {
|
||||
newPathsMap[path] = true
|
||||
}
|
||||
|
||||
// 计算需要新增的目录
|
||||
var toAdd []string
|
||||
for path := range newPathsMap {
|
||||
if !p.paths[path] {
|
||||
toAdd = append(toAdd, path)
|
||||
}
|
||||
}
|
||||
|
||||
// 计算需要移除的目录
|
||||
var toRemove []string
|
||||
for path := range p.paths {
|
||||
if !newPathsMap[path] {
|
||||
toRemove = append(toRemove, path)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有变化,直接返回
|
||||
if len(toAdd) == 0 && len(toRemove) == 0 {
|
||||
log.Println("ℹ️ 防篡改保护目录列表无变化")
|
||||
return &UpdateResult{
|
||||
Added: []string{},
|
||||
Removed: []string{},
|
||||
Current: p.getCurrentPaths(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 初始化 watcher(如果还没创建)
|
||||
if err := p.initWatcher(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 处理需要移除的目录
|
||||
var removeFailed []string
|
||||
for _, path := range toRemove {
|
||||
if err := p.removePath(path); err != nil {
|
||||
log.Printf("⚠️ 移除目录 %s 保护失败: %v", path, err)
|
||||
removeFailed = append(removeFailed, path)
|
||||
} else {
|
||||
delete(p.paths, path)
|
||||
log.Printf("✅ 已取消保护目录: %s", path)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理需要新增的目录
|
||||
var addFailed []string
|
||||
for _, path := range toAdd {
|
||||
if err := p.addPath(path); err != nil {
|
||||
log.Printf("⚠️ 添加目录 %s 保护失败: %v", path, err)
|
||||
addFailed = append(addFailed, path)
|
||||
} else {
|
||||
p.paths[path] = true
|
||||
log.Printf("✅ 已保护目录: %s", path)
|
||||
}
|
||||
}
|
||||
|
||||
// 构建结果
|
||||
result := &UpdateResult{
|
||||
Added: filterFailed(toAdd, addFailed),
|
||||
Removed: filterFailed(toRemove, removeFailed),
|
||||
Current: p.getCurrentPaths(),
|
||||
}
|
||||
|
||||
// 如果有失败的操作,返回错误
|
||||
if len(addFailed) > 0 || len(removeFailed) > 0 {
|
||||
return result, fmt.Errorf("部分操作失败: 添加失败 %d 个, 移除失败 %d 个", len(addFailed), len(removeFailed))
|
||||
}
|
||||
|
||||
log.Printf("✅ 防篡改保护已更新: 新增 %d 个目录, 移除 %d 个目录, 当前保护 %d 个目录",
|
||||
len(result.Added), len(result.Removed), len(result.Current))
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// StopAll 停止所有防篡改保护
|
||||
func (p *Protector) StopAll() error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if len(p.paths) == 0 {
|
||||
log.Println("ℹ️ 没有正在保护的目录")
|
||||
return nil
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
|
||||
// 取消 context
|
||||
if p.cancel != nil {
|
||||
p.cancel()
|
||||
p.cancel = nil
|
||||
}
|
||||
|
||||
// 关闭监控器
|
||||
if p.watcher != nil {
|
||||
if err := p.watcher.Close(); err != nil {
|
||||
log.Printf("⚠️ 关闭文件监控器失败: %v", err)
|
||||
lastErr = err
|
||||
}
|
||||
p.watcher = nil
|
||||
p.watcherOnce = sync.Once{} // 重置,允许下次重新创建
|
||||
}
|
||||
|
||||
// 移除所有目录的不可变属性
|
||||
for path := range p.paths {
|
||||
if err := p.setImmutable(path, false); err != nil {
|
||||
log.Printf("⚠️ 移除目录 %s 不可变属性失败: %v", path, err)
|
||||
lastErr = err
|
||||
} else {
|
||||
log.Printf("✅ 已取消保护目录: %s", path)
|
||||
}
|
||||
}
|
||||
|
||||
// 清空路径列表
|
||||
p.paths = make(map[string]bool)
|
||||
|
||||
log.Println("✅ 已停止所有防篡改保护")
|
||||
return lastErr
|
||||
}
|
||||
|
||||
// GetEvents 获取事件通道
|
||||
func (p *Protector) GetEvents() <-chan TamperEvent {
|
||||
return p.eventCh
|
||||
}
|
||||
|
||||
// GetProtectedPaths 获取受保护的路径列表
|
||||
func (p *Protector) GetProtectedPaths() []string {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
return p.getCurrentPaths()
|
||||
}
|
||||
|
||||
// IsProtected 检查路径是否受保护
|
||||
func (p *Protector) IsProtected(path string) bool {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
return p.paths[path]
|
||||
}
|
||||
|
||||
// getCurrentPaths 获取当前路径列表(内部方法,不加锁)
|
||||
func (p *Protector) getCurrentPaths() []string {
|
||||
paths := make([]string, 0, len(p.paths))
|
||||
for path := range p.paths {
|
||||
paths = append(paths, path)
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
// initWatcher 初始化文件监控器(只会创建一次)
|
||||
func (p *Protector) initWatcher(ctx context.Context) error {
|
||||
var err error
|
||||
p.watcherOnce.Do(func() {
|
||||
// 创建文件监控器
|
||||
p.watcher, err = fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("创建文件监控器失败: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 创建 context
|
||||
p.ctx, p.cancel = context.WithCancel(ctx)
|
||||
|
||||
// 启动监控循环
|
||||
go p.watchLoop()
|
||||
|
||||
log.Println("✅ 文件监控器已启动")
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// addPath 添加目录保护(内部方法,不加锁)
|
||||
func (p *Protector) addPath(path string) error {
|
||||
// 设置不可变属性
|
||||
if err := p.setImmutable(path, true); err != nil {
|
||||
return fmt.Errorf("设置目录不可变属性失败: %w", err)
|
||||
}
|
||||
|
||||
// 添加到监控
|
||||
if p.watcher != nil {
|
||||
if err := p.watcher.Add(path); err != nil {
|
||||
// 如果添加监控失败,尝试回滚不可变属性
|
||||
_ = p.setImmutable(path, false)
|
||||
return fmt.Errorf("添加目录到监控失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// removePath 移除目录保护(内部方法,不加锁)
|
||||
func (p *Protector) removePath(path string) error {
|
||||
// 从监控中移除
|
||||
if p.watcher != nil {
|
||||
if err := p.watcher.Remove(path); err != nil {
|
||||
log.Printf("⚠️ 从监控中移除目录失败: %v", err)
|
||||
// 继续执行,不返回错误
|
||||
}
|
||||
}
|
||||
|
||||
// 移除不可变属性
|
||||
if err := p.setImmutable(path, false); err != nil {
|
||||
return fmt.Errorf("移除目录不可变属性失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// watchLoop 监控循环
|
||||
func (p *Protector) watchLoop() {
|
||||
for {
|
||||
select {
|
||||
case <-p.ctx.Done():
|
||||
return
|
||||
case event, ok := <-p.watcher.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
p.handleEvent(event)
|
||||
case err, ok := <-p.watcher.Errors:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
log.Printf("⚠️ 文件监控错误: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleEvent 处理文件系统事件
|
||||
func (p *Protector) handleEvent(event fsnotify.Event) {
|
||||
var operation string
|
||||
var details string
|
||||
|
||||
switch {
|
||||
case event.Op&fsnotify.Write == fsnotify.Write:
|
||||
operation = "write"
|
||||
details = "文件被写入"
|
||||
case event.Op&fsnotify.Remove == fsnotify.Remove:
|
||||
operation = "remove"
|
||||
details = "文件被删除"
|
||||
case event.Op&fsnotify.Rename == fsnotify.Rename:
|
||||
operation = "rename"
|
||||
details = "文件被重命名"
|
||||
case event.Op&fsnotify.Chmod == fsnotify.Chmod:
|
||||
operation = "chmod"
|
||||
details = "文件权限被修改"
|
||||
case event.Op&fsnotify.Create == fsnotify.Create:
|
||||
operation = "create"
|
||||
details = "文件被创建"
|
||||
default:
|
||||
operation = "unknown"
|
||||
details = fmt.Sprintf("未知操作: %v", event.Op)
|
||||
}
|
||||
|
||||
tamperEvent := TamperEvent{
|
||||
Path: event.Name,
|
||||
Operation: operation,
|
||||
Timestamp: time.Now(),
|
||||
Details: details,
|
||||
}
|
||||
|
||||
// 发送事件(非阻塞)
|
||||
select {
|
||||
case p.eventCh <- tamperEvent:
|
||||
log.Printf("🚨 检测到文件变动: %s - %s (%s)", event.Name, operation, details)
|
||||
default:
|
||||
log.Printf("⚠️ 事件队列已满,丢弃事件: %s", event.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// setImmutable 设置或移除文件/目录的不可变属性
|
||||
func (p *Protector) setImmutable(path string, immutable bool) error {
|
||||
var cmd *exec.Cmd
|
||||
if immutable {
|
||||
cmd = exec.Command("chattr", "+i", path)
|
||||
} else {
|
||||
cmd = exec.Command("chattr", "-i", path)
|
||||
}
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("执行 chattr 失败: %w, 输出: %s", err, string(output))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// filterFailed 过滤掉失败的项,返回成功的项
|
||||
func filterFailed(all []string, failed []string) []string {
|
||||
if len(failed) == 0 {
|
||||
return all
|
||||
}
|
||||
|
||||
failedMap := make(map[string]bool)
|
||||
for _, f := range failed {
|
||||
failedMap[f] = true
|
||||
}
|
||||
|
||||
var success []string
|
||||
for _, item := range all {
|
||||
if !failedMap[item] {
|
||||
success = append(success, item)
|
||||
}
|
||||
}
|
||||
return success
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
package tamper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewProtector(t *testing.T) {
|
||||
p := NewProtector()
|
||||
if p == nil {
|
||||
t.Fatal("NewProtector() 返回 nil")
|
||||
}
|
||||
|
||||
paths := p.GetProtectedPaths()
|
||||
if len(paths) != 0 {
|
||||
t.Errorf("新建的保护器不应该有受保护的路径,当前有 %d 个", len(paths))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdatePaths(t *testing.T) {
|
||||
// 此测试仅在 Linux 系统上运行
|
||||
if runtime.GOOS != "linux" {
|
||||
t.Skip("防篡改功能仅支持 Linux 系统")
|
||||
}
|
||||
|
||||
// 检查是否有 root 权限
|
||||
if os.Geteuid() != 0 {
|
||||
t.Skip("此测试需要 root 权限才能运行 chattr 命令")
|
||||
}
|
||||
|
||||
p := NewProtector()
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建测试目录
|
||||
testDir1 := "/tmp/tamper_test_1_" + time.Now().Format("20060102150405")
|
||||
testDir2 := "/tmp/tamper_test_2_" + time.Now().Format("20060102150405")
|
||||
testDir3 := "/tmp/tamper_test_3_" + time.Now().Format("20060102150405")
|
||||
testDir4 := "/tmp/tamper_test_4_" + time.Now().Format("20060102150405")
|
||||
|
||||
if err := os.MkdirAll(testDir1, 0755); err != nil {
|
||||
t.Fatalf("创建测试目录失败: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(testDir1)
|
||||
|
||||
if err := os.MkdirAll(testDir2, 0755); err != nil {
|
||||
t.Fatalf("创建测试目录失败: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(testDir2)
|
||||
|
||||
if err := os.MkdirAll(testDir3, 0755); err != nil {
|
||||
t.Fatalf("创建测试目录失败: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(testDir3)
|
||||
|
||||
if err := os.MkdirAll(testDir4, 0755); err != nil {
|
||||
t.Fatalf("创建测试目录失败: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(testDir4)
|
||||
|
||||
defer p.StopAll()
|
||||
|
||||
// 第一次更新: 添加 /a /b /c
|
||||
t.Run("第一次配置: /a /b /c", func(t *testing.T) {
|
||||
result, err := p.UpdatePaths(ctx, []string{testDir1, testDir2, testDir3})
|
||||
if err != nil {
|
||||
t.Fatalf("UpdatePaths() 失败: %v", err)
|
||||
}
|
||||
|
||||
if len(result.Added) != 3 {
|
||||
t.Errorf("应该新增 3 个目录,实际新增 %d 个", len(result.Added))
|
||||
}
|
||||
|
||||
if len(result.Removed) != 0 {
|
||||
t.Errorf("不应该移除任何目录,实际移除 %d 个", len(result.Removed))
|
||||
}
|
||||
|
||||
if len(result.Current) != 3 {
|
||||
t.Errorf("当前应该保护 3 个目录,实际保护 %d 个", len(result.Current))
|
||||
}
|
||||
|
||||
// 验证目录确实受保护
|
||||
if !p.IsProtected(testDir1) {
|
||||
t.Errorf("目录 %s 应该受保护", testDir1)
|
||||
}
|
||||
})
|
||||
|
||||
// 第二次更新: 修改为 /a /b (移除 /c)
|
||||
t.Run("第二次配置: /a /b", func(t *testing.T) {
|
||||
result, err := p.UpdatePaths(ctx, []string{testDir1, testDir2})
|
||||
if err != nil {
|
||||
t.Fatalf("UpdatePaths() 失败: %v", err)
|
||||
}
|
||||
|
||||
if len(result.Added) != 0 {
|
||||
t.Errorf("不应该新增任何目录,实际新增 %d 个", len(result.Added))
|
||||
}
|
||||
|
||||
if len(result.Removed) != 1 {
|
||||
t.Errorf("应该移除 1 个目录,实际移除 %d 个", len(result.Removed))
|
||||
}
|
||||
|
||||
if len(result.Current) != 2 {
|
||||
t.Errorf("当前应该保护 2 个目录,实际保护 %d 个", len(result.Current))
|
||||
}
|
||||
|
||||
// 验证 /c 不再受保护
|
||||
if p.IsProtected(testDir3) {
|
||||
t.Errorf("目录 %s 不应该受保护", testDir3)
|
||||
}
|
||||
})
|
||||
|
||||
// 第三次更新: 修改为 /a /b /c /d (新增 /c /d)
|
||||
t.Run("第三次配置: /a /b /c /d", func(t *testing.T) {
|
||||
result, err := p.UpdatePaths(ctx, []string{testDir1, testDir2, testDir3, testDir4})
|
||||
if err != nil {
|
||||
t.Fatalf("UpdatePaths() 失败: %v", err)
|
||||
}
|
||||
|
||||
if len(result.Added) != 2 {
|
||||
t.Errorf("应该新增 2 个目录,实际新增 %d 个", len(result.Added))
|
||||
}
|
||||
|
||||
if len(result.Removed) != 0 {
|
||||
t.Errorf("不应该移除任何目录,实际移除 %d 个", len(result.Removed))
|
||||
}
|
||||
|
||||
if len(result.Current) != 4 {
|
||||
t.Errorf("当前应该保护 4 个目录,实际保护 %d 个", len(result.Current))
|
||||
}
|
||||
|
||||
// 验证所有目录都受保护
|
||||
for _, dir := range []string{testDir1, testDir2, testDir3, testDir4} {
|
||||
if !p.IsProtected(dir) {
|
||||
t.Errorf("目录 %s 应该受保护", dir)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 第四次更新: 清空列表
|
||||
t.Run("第四次配置: 清空", func(t *testing.T) {
|
||||
result, err := p.UpdatePaths(ctx, []string{})
|
||||
if err != nil {
|
||||
t.Fatalf("UpdatePaths() 失败: %v", err)
|
||||
}
|
||||
|
||||
if len(result.Added) != 0 {
|
||||
t.Errorf("不应该新增任何目录,实际新增 %d 个", len(result.Added))
|
||||
}
|
||||
|
||||
if len(result.Removed) != 4 {
|
||||
t.Errorf("应该移除 4 个目录,实际移除 %d 个", len(result.Removed))
|
||||
}
|
||||
|
||||
if len(result.Current) != 0 {
|
||||
t.Errorf("当前应该保护 0 个目录,实际保护 %d 个", len(result.Current))
|
||||
}
|
||||
})
|
||||
|
||||
// 第五次更新: 重复配置相同的列表
|
||||
t.Run("第五次配置: /a /b (重复)", func(t *testing.T) {
|
||||
// 先添加
|
||||
p.UpdatePaths(ctx, []string{testDir1, testDir2})
|
||||
|
||||
// 再次添加相同的配置
|
||||
result, err := p.UpdatePaths(ctx, []string{testDir1, testDir2})
|
||||
if err != nil {
|
||||
t.Fatalf("UpdatePaths() 失败: %v", err)
|
||||
}
|
||||
|
||||
if len(result.Added) != 0 {
|
||||
t.Errorf("重复配置不应该新增任何目录,实际新增 %d 个", len(result.Added))
|
||||
}
|
||||
|
||||
if len(result.Removed) != 0 {
|
||||
t.Errorf("重复配置不应该移除任何目录,实际移除 %d 个", len(result.Removed))
|
||||
}
|
||||
|
||||
if len(result.Current) != 2 {
|
||||
t.Errorf("当前应该保护 2 个目录,实际保护 %d 个", len(result.Current))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestStopAll(t *testing.T) {
|
||||
// 此测试仅在 Linux 系统上运行
|
||||
if runtime.GOOS != "linux" {
|
||||
t.Skip("防篡改功能仅支持 Linux 系统")
|
||||
}
|
||||
|
||||
// 检查是否有 root 权限
|
||||
if os.Geteuid() != 0 {
|
||||
t.Skip("此测试需要 root 权限才能运行 chattr 命令")
|
||||
}
|
||||
|
||||
p := NewProtector()
|
||||
|
||||
// 创建测试目录
|
||||
testDir := "/tmp/tamper_test_stop_" + time.Now().Format("20060102150405")
|
||||
if err := os.MkdirAll(testDir, 0755); err != nil {
|
||||
t.Fatalf("创建测试目录失败: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(testDir)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// 添加保护
|
||||
_, err := p.UpdatePaths(ctx, []string{testDir})
|
||||
if err != nil {
|
||||
t.Fatalf("UpdatePaths() 失败: %v", err)
|
||||
}
|
||||
|
||||
// 验证目录受保护
|
||||
if !p.IsProtected(testDir) {
|
||||
t.Error("目录应该受保护")
|
||||
}
|
||||
|
||||
// 停止所有保护
|
||||
err = p.StopAll()
|
||||
if err != nil {
|
||||
t.Fatalf("StopAll() 失败: %v", err)
|
||||
}
|
||||
|
||||
// 验证目录不再受保护
|
||||
if p.IsProtected(testDir) {
|
||||
t.Error("目录不应该受保护")
|
||||
}
|
||||
|
||||
paths := p.GetProtectedPaths()
|
||||
if len(paths) != 0 {
|
||||
t.Errorf("停止后不应该有受保护的路径,当前有 %d 个", len(paths))
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtectorEvents(t *testing.T) {
|
||||
// 此测试仅在 Linux 系统上运行
|
||||
if runtime.GOOS != "linux" {
|
||||
t.Skip("防篡改功能仅支持 Linux 系统")
|
||||
}
|
||||
|
||||
// 检查是否有 root 权限
|
||||
if os.Geteuid() != 0 {
|
||||
t.Skip("此测试需要 root 权限才能运行 chattr 命令")
|
||||
}
|
||||
|
||||
p := NewProtector()
|
||||
|
||||
// 创建测试目录
|
||||
testDir := "/tmp/tamper_test_events_" + time.Now().Format("20060102150405")
|
||||
if err := os.MkdirAll(testDir, 0755); err != nil {
|
||||
t.Fatalf("创建测试目录失败: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(testDir)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// 启动保护器
|
||||
_, err := p.UpdatePaths(ctx, []string{testDir})
|
||||
if err != nil {
|
||||
t.Fatalf("UpdatePaths() 失败: %v", err)
|
||||
}
|
||||
defer p.StopAll()
|
||||
|
||||
// 获取事件通道
|
||||
eventCh := p.GetEvents()
|
||||
|
||||
// 等待一段时间让 watcher 准备就绪
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
// 尝试在受保护的目录中创建文件(这应该被阻止或触发事件)
|
||||
testFile := testDir + "/test.txt"
|
||||
_ = os.WriteFile(testFile, []byte("test"), 0644)
|
||||
|
||||
// 等待事件
|
||||
select {
|
||||
case event := <-eventCh:
|
||||
t.Logf("收到防篡改事件: Path=%s, Operation=%s, Details=%s",
|
||||
event.Path, event.Operation, event.Details)
|
||||
// 验证事件字段
|
||||
if event.Path == "" {
|
||||
t.Error("事件路径不应为空")
|
||||
}
|
||||
if event.Operation == "" {
|
||||
t.Error("事件操作不应为空")
|
||||
}
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Log("在超时时间内没有收到事件(这可能是正常的,因为 chattr +i 会阻止操作)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTamperEventJSON(t *testing.T) {
|
||||
event := TamperEvent{
|
||||
Path: "/tmp/test.txt",
|
||||
Operation: "write",
|
||||
Timestamp: time.Now(),
|
||||
Details: "文件被写入",
|
||||
}
|
||||
|
||||
// 测试 JSON 序列化
|
||||
data, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
t.Fatalf("JSON 序列化失败: %v", err)
|
||||
}
|
||||
|
||||
// 测试 JSON 反序列化
|
||||
var decoded TamperEvent
|
||||
err = json.Unmarshal(data, &decoded)
|
||||
if err != nil {
|
||||
t.Fatalf("JSON 反序列化失败: %v", err)
|
||||
}
|
||||
|
||||
// 验证字段
|
||||
if decoded.Path != event.Path {
|
||||
t.Errorf("Path = %s, want %s", decoded.Path, event.Path)
|
||||
}
|
||||
if decoded.Operation != event.Operation {
|
||||
t.Errorf("Operation = %s, want %s", decoded.Operation, event.Operation)
|
||||
}
|
||||
if decoded.Details != event.Details {
|
||||
t.Errorf("Details = %s, want %s", decoded.Details, event.Details)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateResultJSON(t *testing.T) {
|
||||
result := UpdateResult{
|
||||
Added: []string{"/tmp/a", "/tmp/b"},
|
||||
Removed: []string{"/tmp/c"},
|
||||
Current: []string{"/tmp/a", "/tmp/b", "/tmp/d"},
|
||||
}
|
||||
|
||||
// 测试 JSON 序列化
|
||||
data, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
t.Fatalf("JSON 序列化失败: %v", err)
|
||||
}
|
||||
|
||||
// 测试 JSON 反序列化
|
||||
var decoded UpdateResult
|
||||
err = json.Unmarshal(data, &decoded)
|
||||
if err != nil {
|
||||
t.Fatalf("JSON 反序列化失败: %v", err)
|
||||
}
|
||||
|
||||
// 验证字段
|
||||
if len(decoded.Added) != len(result.Added) {
|
||||
t.Errorf("Added length = %d, want %d", len(decoded.Added), len(result.Added))
|
||||
}
|
||||
if len(decoded.Removed) != len(result.Removed) {
|
||||
t.Errorf("Removed length = %d, want %d", len(decoded.Removed), len(result.Removed))
|
||||
}
|
||||
if len(decoded.Current) != len(result.Current) {
|
||||
t.Errorf("Current length = %d, want %d", len(decoded.Current), len(result.Current))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user