Files
pika/pkg/agent/tamper/tamper_protector.go
T

380 lines
9.3 KiB
Go

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
}