go20/devops/agent/script/script_integrity.go

170 lines
4.1 KiB
Go
Raw Normal View History

2026-03-08 18:05:17 +08:00
package script
import (
"crypto/sha256"
"embed"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"maps"
"os"
"path/filepath"
"sync"
"github.com/infraboard/mcube/v2/ioc/config/log"
"github.com/rs/zerolog"
)
// 编译时嵌入的脚本 hash 文件
//
//go:embed hashes.json
var scriptHashesFS embed.FS
// ScriptIntegrityManager 脚本完整性管理器
// 脚本 hash 在编译时计算并硬编码到二进制中
// 在执行脚本前校验 hash防止脚本被篡改
// 这确保了生产环境的脚本不会被篡改,即使脚本目录变为可写
type ScriptIntegrityManager struct {
log *zerolog.Logger
mu sync.RWMutex
// 注册的脚本 hash (相对路径 -> hash),从编译时常量加载
registeredHashes map[string]string
// 脚本根目录
scriptDir string
// 是否启用校验(编译时参数)
enabled bool
}
// NewScriptIntegrityManager 创建脚本完整性管理器
// 从编译时嵌入的 hash 文件加载脚本 hash
func NewScriptIntegrityManager(scriptDir string, enabled bool) *ScriptIntegrityManager {
m := &ScriptIntegrityManager{
log: log.Sub("script_integrity"),
registeredHashes: make(map[string]string),
scriptDir: scriptDir,
enabled: enabled,
}
// 如果启用了校验,加载编译时的 hash
if enabled {
if err := m.loadCompiledHashes(); err != nil {
m.log.Error().Err(err).Msg("加载编译时的脚本 hash 失败")
}
}
return m
}
// loadCompiledHashes 从编译时嵌入的 hash 文件加载脚本 hash
func (m *ScriptIntegrityManager) loadCompiledHashes() error {
data, err := scriptHashesFS.ReadFile("hashes.json")
if err != nil {
return fmt.Errorf("读取嵌入的 hash 文件失败: %v", err)
}
var hashes map[string]string
if err := json.Unmarshal(data, &hashes); err != nil {
return fmt.Errorf("解析 hash 文件失败: %v", err)
}
m.mu.Lock()
m.registeredHashes = hashes
m.mu.Unlock()
m.log.Info().Int("count", len(hashes)).Msg("加载编译时的脚本 hash 完成")
return nil
}
// Enable 启用校验
func (m *ScriptIntegrityManager) Enable() {
m.mu.Lock()
defer m.mu.Unlock()
m.enabled = true
}
// Disable 禁用校验
func (m *ScriptIntegrityManager) Disable() {
m.mu.Lock()
defer m.mu.Unlock()
m.enabled = false
}
// IsEnabled 是否启用校验
func (m *ScriptIntegrityManager) IsEnabled() bool {
m.mu.RLock()
defer m.mu.RUnlock()
return m.enabled
}
// VerifyScript 校验脚本完整性
// 在执行脚本前调用,验证脚本是否被篡改
func (m *ScriptIntegrityManager) VerifyScript(scriptPath string) error {
if !m.enabled {
return nil
}
// 计算相对路径
relPath, err := filepath.Rel(m.scriptDir, scriptPath)
if err != nil {
return fmt.Errorf("计算相对路径失败: %v", err)
}
m.mu.RLock()
expectedHash, exists := m.registeredHashes[relPath]
m.mu.RUnlock()
if !exists {
return fmt.Errorf("脚本未注册: %s (可能是新增的脚本,请重启 Agent)", relPath)
}
// 计算当前 hash
currentHash, err := m.calculateFileHash(scriptPath)
if err != nil {
return fmt.Errorf("计算脚本 hash 失败: %v", err)
}
// 对比 hash
if currentHash != expectedHash {
m.log.Error().
Str("script", relPath).
Str("expected_hash", expectedHash).
Str("current_hash", currentHash).
Msg("脚本完整性校验失败:脚本可能被篡改")
return fmt.Errorf("脚本完整性校验失败: %s (hash 不匹配)", relPath)
}
m.log.Debug().Str("script", relPath).Str("hash", currentHash).Msg("脚本完整性校验通过")
return nil
}
// calculateFileHash 计算文件的 SHA256 hash
func (m *ScriptIntegrityManager) 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
}
// GetRegisteredScripts 获取已注册的脚本列表
func (m *ScriptIntegrityManager) GetRegisteredScripts() map[string]string {
m.mu.RLock()
defer m.mu.RUnlock()
// 返回副本
result := make(map[string]string, len(m.registeredHashes))
maps.Copy(result, m.registeredHashes)
return result
}