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 }