调整目录保护功能和 SSH 登录监控

This commit is contained in:
dushixiang committed 2026-01-05 21:44:20 +08:00
1 parent 7386c6e8dd
commit 911c105414
23 files changed
+762 -914

No files matched your search

+12 -16
View File
@@ -255,7 +255,6 @@ func setupApi(app *orz.App, components *AppComponents) {
adminApi.GET("/agents/:id/tamper/config", components.TamperHandler.GetTamperConfig)
adminApi.PUT("/agents/:id/tamper/config", components.TamperHandler.UpdateTamperConfig)
adminApi.GET("/agents/:id/tamper/events", components.TamperHandler.GetTamperEvents)
adminApi.GET("/agents/:id/tamper/alerts", components.TamperHandler.GetTamperAlerts)
// SSH 登录监控管理(管理员功能)
adminApi.GET("/agents/:id/ssh-login/config", components.SSHLoginHandler.GetConfig)
@@ -307,21 +306,18 @@ func setupApi(app *orz.App, components *AppComponents) {
func autoMigrate(database *gorm.DB) error {
// 自动迁移数据库表
return database.AutoMigrate(
&models.Agent{}, // 探针
&models.ApiKey{}, // ApiKey
&models.HostMetric{}, // 保留主机静态信息表
&models.AuditResult{}, // 审计历史
&models.Property{}, // 系统属性
&models.AlertRecord{}, // 告警记录
&models.AlertState{}, // 告警状态
&models.MonitorTask{}, // 服务监控
&models.TamperProtectConfig{}, // 防篡改配置
&models.TamperEvent{}, // 防篡改事件
&models.TamperAlert{}, // 防篡改告警
&models.DDNSConfig{}, // DDNS 配置
&models.DDNSRecord{}, // DDNS 记录
&models.SSHLoginConfig{}, // SSH 登录监控配置
&models.SSHLoginEvent{}, // SSH 登录事件
&models.Agent{}, // 探针
&models.ApiKey{}, // ApiKey
&models.HostMetric{}, // 保留主机静态信息表
&models.AuditResult{}, // 审计历史
&models.Property{}, // 系统属性
&models.AlertRecord{}, // 告警记录
&models.AlertState{}, // 告警状态
&models.MonitorTask{}, // 服务监控
&models.TamperEvent{}, // 防篡改事件
&models.DDNSConfig{}, // DDNS 配置
&models.DDNSRecord{}, // DDNS 记录
&models.SSHLoginEvent{}, // SSH 登录事件
)
}
+12 -20
View File
@@ -184,15 +184,6 @@ func (h *AgentHandler) handleWebSocketMessage(ctx context.Context, agentID strin
}
return h.tamperService.CreateEvent(agentID, eventData.Path, eventData.Operation, eventData.Details, eventData.Timestamp)
case protocol.MessageTypeTamperAlert:
// 防篡改告警
var alertData protocol.TamperAlertData
if err := json.Unmarshal(data, &alertData); err != nil {
h.logger.Error("failed to unmarshal tamper alert", zap.Error(err))
return err
}
return h.tamperService.CreateAlert(agentID, alertData.Path, alertData.Details, alertData.Restored, alertData.Timestamp)
case protocol.MessageTypeDDNSIPReport:
// DDNS IP 上报 - 异步处理,避免阻塞 WebSocket 消息循环
var ipReport protocol.DDNSIPReportData
@@ -238,8 +229,7 @@ func (h *AgentHandler) handleWebSocketMessage(ctx context.Context, agentID strin
} else {
h.logger.Error("tamper protect config apply failed",
zap.String("agentID", agentID),
zap.String("message", protectResp.Message),
zap.String("error", protectResp.Error))
zap.String("message", protectResp.Message))
}
return nil
@@ -551,17 +541,19 @@ func (h *AgentHandler) GetAgents(c echo.Context) error {
"status": agent.Status,
"lastSeenAt": agent.LastSeenAt,
"visibility": agent.Visibility,
// 流量统计相关字段
"trafficLimit": agent.TrafficLimit,
"trafficUsed": agent.TrafficUsed,
"trafficResetDay": agent.TrafficResetDay,
"trafficPeriodStart": agent.TrafficPeriodStart,
"trafficBaselineRecv": agent.TrafficBaselineRecv,
"trafficAlertSent80": agent.TrafficAlertSent80,
"trafficAlertSent90": agent.TrafficAlertSent90,
"trafficAlertSent100": agent.TrafficAlertSent100,
}
// 添加流量统计数据
trafficStats := agent.TrafficStats.Data()
item["trafficLimit"] = trafficStats.Limit
item["trafficUsed"] = trafficStats.Used
item["trafficResetDay"] = trafficStats.ResetDay
item["trafficPeriodStart"] = trafficStats.PeriodStart
item["trafficBaselineRecv"] = trafficStats.BaselineRecv
item["trafficAlertSent80"] = trafficStats.AlertSent80
item["trafficAlertSent90"] = trafficStats.AlertSent90
item["trafficAlertSent100"] = trafficStats.AlertSent100
// 获取最新指标数据
metrics, ok := h.metricService.GetLatestMetrics(agent.ID)
if ok {
+2 -3
View File
@@ -63,8 +63,7 @@ func (h *SSHLoginHandler) UpdateConfig(c echo.Context) error {
}
var req struct {
Enabled bool `json:"enabled"`
RecordFailed bool `json:"recordFailed"`
Enabled bool `json:"enabled"`
}
if err := c.Bind(&req); err != nil {
@@ -73,7 +72,7 @@ func (h *SSHLoginHandler) UpdateConfig(c echo.Context) error {
})
}
config, configSent, err := h.service.UpdateConfig(c.Request().Context(), agentID, req.Enabled, req.RecordFailed)
config, configSent, err := h.service.UpdateConfig(c.Request().Context(), agentID, req.Enabled)
if err != nil {
h.logger.Error("更新SSH登录监控配置失败", zap.Error(err))
return c.JSON(http.StatusInternalServerError, map[string]string{
+3 -35
View File
@@ -33,7 +33,8 @@ func (h *TamperHandler) UpdateTamperConfig(c echo.Context) error {
}
var req struct {
Paths []string `json:"paths"`
Enabled bool `json:"enabled"`
Paths []string `json:"paths"`
}
if err := c.Bind(&req); err != nil {
@@ -43,7 +44,7 @@ func (h *TamperHandler) UpdateTamperConfig(c echo.Context) error {
})
}
config, err := h.tamperService.UpdateConfig(agentID, req.Paths)
config, err := h.tamperService.UpdateConfig(agentID, req.Enabled, req.Paths)
if err != nil {
h.logger.Error("更新防篡改配置失败", zap.Error(err), zap.String("agentId", agentID))
return c.JSON(http.StatusInternalServerError, map[string]interface{}{
@@ -126,36 +127,3 @@ func (h *TamperHandler) GetTamperEvents(c echo.Context) error {
},
})
}
// GetTamperAlerts 获取探针的防篡改告警
// GET /api/agents/:id/tamper/alerts
func (h *TamperHandler) GetTamperAlerts(c echo.Context) error {
agentID := c.Param("id")
if agentID == "" {
return c.JSON(http.StatusBadRequest, map[string]interface{}{
"success": false,
"message": "探针ID不能为空",
})
}
// 获取分页参数
pageNum, _ := strconv.Atoi(c.QueryParam("pageNum"))
pageSize, _ := strconv.Atoi(c.QueryParam("pageSize"))
alerts, total, err := h.tamperService.GetAlertsByAgentID(agentID, pageNum, pageSize)
if err != nil {
h.logger.Error("获取防篡改告警失败", zap.Error(err), zap.String("agentId", agentID))
return c.JSON(http.StatusInternalServerError, map[string]interface{}{
"success": false,
"message": "获取告警失败",
})
}
return c.JSON(http.StatusOK, map[string]interface{}{
"success": true,
"data": map[string]interface{}{
"items": alerts,
"total": total,
},
})
}
+34 -8
View File
@@ -20,14 +20,40 @@ type Agent struct {
UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"` // 更新时间(时间戳毫秒)
// 流量统计相关字段
TrafficLimit uint64 `json:"trafficLimit"` // 流量限额(字节), 0表示不限制
TrafficUsed uint64 `json:"trafficUsed"` // 当前周期已使用流量(字节)
TrafficResetDay int `json:"trafficResetDay"` // 流量重置日期(1-31), 0表示不自动重置
TrafficPeriodStart int64 `json:"trafficPeriodStart"` // 当前周期开始时间(时间戳毫秒)
TrafficBaselineRecv uint64 `json:"trafficBaselineRecv"` // 当前周期流量基线(BytesRecvTotal)
TrafficAlertSent80 bool `json:"trafficAlertSent80"` // 是否已发送80%告警
TrafficAlertSent90 bool `json:"trafficAlertSent90"` // 是否已发送90%告警
TrafficAlertSent100 bool `json:"trafficAlertSent100"` // 是否已发送100%告警
TrafficStats datatypes.JSONType[TrafficStatsData] `json:"trafficStats,omitempty"` // 流量统计
// 防篡改保护配置
TamperProtectConfig datatypes.JSONType[TamperProtectConfigData] `json:"tamperProtectConfig,omitempty"` // 防篡改保护配置
// SSH登录监控配置
SSHLoginConfig datatypes.JSONType[SSHLoginConfigData] `json:"sshLoginConfig,omitempty"` // SSH登录监控配置
}
// TrafficStatsData 流量统计数据
type TrafficStatsData struct {
Limit uint64 `json:"limit"` // 流量限额(字节), 0表示不限制
Used uint64 `json:"used"` // 当前周期已使用流量(字节)
ResetDay int `json:"resetDay"` // 流量重置日期(1-31), 0表示不自动重置
PeriodStart int64 `json:"periodStart"` // 当前周期开始时间(时间戳毫秒)
BaselineRecv uint64 `json:"baselineRecv"` // 当前周期流量基线(BytesRecvTotal)
AlertSent80 bool `json:"alertSent80"` // 是否已发送80%告警
AlertSent90 bool `json:"alertSent90"` // 是否已发送90%告警
AlertSent100 bool `json:"alertSent100"` // 是否已发送100%告警
}
// TamperProtectConfigData 防篡改保护配置数据
type TamperProtectConfigData struct {
Enabled bool `json:"enabled"` // 是否启用
Paths []string `json:"paths"` // 受保护的目录列表
ApplyStatus string `json:"applyStatus,omitempty"` // 配置应用状态: success/failed/pending
ApplyMessage string `json:"applyMessage,omitempty"` // 应用结果消息
}
// SSHLoginConfigData SSH登录监控配置数据
type SSHLoginConfigData struct {
Enabled bool `json:"enabled"` // 是否启用
ApplyStatus string `json:"applyStatus,omitempty"` // 配置应用状态: success/failed/pending
ApplyMessage string `json:"applyMessage,omitempty"` // 应用结果消息
}
func (Agent) TableName() string {
-40
View File
@@ -1,37 +1,5 @@
package models
import (
"time"
"gorm.io/gorm"
)
// SSHLoginConfig SSH登录监控配置
type SSHLoginConfig struct {
ID string `gorm:"primaryKey" json:"id"` // 配置ID (UUID)
AgentID string `gorm:"uniqueIndex;not null" json:"agentId"` // 探针ID(唯一)
Enabled bool `json:"enabled"` // 是否启用
RecordFailed bool `json:"recordFailed"` // 是否记录失败登录
ApplyStatus string `json:"applyStatus,omitempty"` // 配置应用状态: success/failed/pending
ApplyMessage string `json:"applyMessage,omitempty"` // 应用结果消息
ApplyError string `json:"applyError,omitempty"` // 应用错误信息
LastAppliedAt int64 `json:"lastAppliedAt,omitempty"` // 最后应用时间(毫秒)
CreatedAt int64 `json:"createdAt"` // 创建时间(毫秒)
UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"` // 更新时间(毫秒)
}
func (SSHLoginConfig) TableName() string {
return "ssh_login_configs"
}
// BeforeCreate GORM钩子:设置创建时间
func (s *SSHLoginConfig) BeforeCreate(tx *gorm.DB) error {
if s.CreatedAt == 0 {
s.CreatedAt = time.Now().UnixMilli()
}
return nil
}
// SSHLoginEvent SSH登录事件
type SSHLoginEvent struct {
ID string `gorm:"primaryKey" json:"id"` // 事件ID (UUID)
@@ -50,11 +18,3 @@ type SSHLoginEvent struct {
func (SSHLoginEvent) TableName() string {
return "ssh_login_events"
}
// BeforeCreate GORM钩子:设置创建时间
func (s *SSHLoginEvent) BeforeCreate(tx *gorm.DB) error {
if s.CreatedAt == 0 {
s.CreatedAt = time.Now().UnixMilli()
}
return nil
}
-30
View File
@@ -1,20 +1,5 @@
package models
import "gorm.io/datatypes"
// TamperProtectConfig 防篡改保护配置
type TamperProtectConfig struct {
ID string `gorm:"primaryKey" json:"id"` // 配置ID (UUID)
AgentID string `gorm:"index;not null" json:"agentId"` // 探针ID
Paths datatypes.JSONSlice[string] `json:"paths"` // 受保护的目录列表
CreatedAt int64 `json:"createdAt"` // 创建时间(时间戳毫秒)
UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"` // 更新时间(时间戳毫秒)
}
func (TamperProtectConfig) TableName() string {
return "tamper_protect_configs"
}
// TamperEvent 防篡改事件
type TamperEvent struct {
ID string `gorm:"primaryKey" json:"id"` // 事件ID (UUID)
@@ -29,18 +14,3 @@ type TamperEvent struct {
func (TamperEvent) TableName() string {
return "tamper_events"
}
// TamperAlert 防篡改属性告警
type TamperAlert struct {
ID string `gorm:"primaryKey" json:"id"` // 告警ID (UUID)
AgentID string `gorm:"index;not null" json:"agentId"` // 探针ID
Path string `gorm:"index" json:"path"` // 被篡改的路径
Details string `json:"details"` // 详细信息
Restored bool `json:"restored"` // 是否已自动恢复
Timestamp int64 `gorm:"index" json:"timestamp"` // 检测时间(时间戳毫秒)
CreatedAt int64 `json:"createdAt"` // 记录创建时间(时间戳毫秒)
}
func (TamperAlert) TableName() string {
return "tamper_alerts"
}
+8 -10
View File
@@ -59,7 +59,6 @@ const (
// 防篡改消息
MessageTypeTamperProtect MessageType = "tamper_protect"
MessageTypeTamperEvent MessageType = "tamper_event"
MessageTypeTamperAlert MessageType = "tamper_alert"
// DDNS 消息
MessageTypeDDNSConfig MessageType = "ddns_config"
MessageTypeDDNSIPReport MessageType = "ddns_ip_report"
@@ -323,15 +322,15 @@ type TamperProtectResponse struct {
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"` // 详细信息
Path string `json:"path"` // 被修改的路径
Operation string `json:"operation"` // 操作类型: write, remove, rename, chmod, create, attr_tamper
Timestamp int64 `json:"timestamp"` // 事件时间(毫秒)
Details string `json:"details"` // 详细信息
Restored bool `json:"restored,omitempty"` // 是否已自动恢复(仅用于 attr_tamper 操作)
}
// TamperAlertData 防篡改属性告警数据
@@ -687,10 +686,9 @@ type SSHLoginConfig struct {
// SSHLoginConfigResult SSH登录监控配置应用结果(Agent 反馈)
type SSHLoginConfigResult struct {
Success bool `json:"success"` // 配置应用是否成功
Enabled bool `json:"enabled"` // 当前启用状态
Message string `json:"message"` // 结果描述信息
Error string `json:"error,omitempty"` // 错误信息(如果失败)
Success bool `json:"success"` // 配置应用是否成功
Enabled bool `json:"enabled"` // 当前启用状态
Message string `json:"message"` // 结果描述信息
}
// SSHLoginEvent SSH登录事件
+50 -10
View File
@@ -222,17 +222,57 @@ func (r *AgentRepo) GetAllTags(ctx context.Context) ([]string, error) {
// FindAgentsWithTrafficReset 查询配置了流量自动重置的探针
func (r *AgentRepo) FindAgentsWithTrafficReset(ctx context.Context) ([]models.Agent, error) {
var allAgents []models.Agent
err := r.db.WithContext(ctx).Find(&allAgents).Error
if err != nil {
return nil, err
}
// 在应用层过滤配置了流量重置的探针
var agents []models.Agent
err := r.db.WithContext(ctx).
Where("traffic_reset_day > 0").
Find(&agents).Error
return agents, err
for _, agent := range allAgents {
stats := agent.TrafficStats.Data()
if stats.ResetDay > 0 {
agents = append(agents, agent)
}
}
return agents, nil
}
// UpdateTrafficStats 批量更新流量统计字段
func (r *AgentRepo) UpdateTrafficStats(ctx context.Context, agentID string, updates map[string]interface{}) error {
return r.db.WithContext(ctx).
Model(&models.Agent{}).
Where("id = ?", agentID).
Updates(updates).Error
// === 配置管理 ===
// GetTamperProtectConfig 获取防篡改保护配置
func (r *AgentRepo) GetTamperProtectConfig(ctx context.Context, agentID string) (*models.TamperProtectConfigData, error) {
var agent models.Agent
err := r.db.WithContext(ctx).Select("tamper_protect_config").Where("id = ?", agentID).First(&agent).Error
if err != nil {
return nil, err
}
data := agent.TamperProtectConfig.Data()
return &data, nil
}
// UpdateTamperProtectConfig 更新防篡改保护配置
func (r *AgentRepo) UpdateTamperProtectConfig(ctx context.Context, agentID string, config *models.TamperProtectConfigData) error {
return r.db.WithContext(ctx).Model(&models.Agent{}).
Where("id = ?", agentID).
Update("tamper_protect_config", config).Error
}
// GetSSHLoginConfig 获取SSH登录监控配置
func (r *AgentRepo) GetSSHLoginConfig(ctx context.Context, agentID string) (*models.SSHLoginConfigData, error) {
var agent models.Agent
err := r.db.WithContext(ctx).Select("ssh_login_config").Where("id = ?", agentID).First(&agent).Error
if err != nil {
return nil, err
}
data := agent.SSHLoginConfig.Data()
return &data, nil
}
// UpdateSSHLoginConfig 更新SSH登录监控配置
func (r *AgentRepo) UpdateSSHLoginConfig(ctx context.Context, agentID string, config *models.SSHLoginConfigData) error {
return r.db.WithContext(ctx).Model(&models.Agent{}).
Where("id = ?", agentID).
Update("ssh_login_config", config).Error
}
-48
View File
@@ -18,54 +18,6 @@ func NewSSHLoginRepo(db *gorm.DB) *SSHLoginRepo {
return &SSHLoginRepo{db: db}
}
// === 配置相关 ===
// GetConfigByAgentID 获取探针的配置
func (r *SSHLoginRepo) GetConfigByAgentID(agentID string) (*models.SSHLoginConfig, error) {
var config models.SSHLoginConfig
err := r.db.Where("agent_id = ?", agentID).First(&config).Error
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return &config, err
}
// UpsertConfig 创建或更新配置
func (r *SSHLoginRepo) UpsertConfig(config *models.SSHLoginConfig) error {
// 检查是否已存在
existing, err := r.GetConfigByAgentID(config.AgentID)
if err != nil {
return err
}
if existing != nil {
// 更新
return r.db.Model(&models.SSHLoginConfig{}).
Where("agent_id = ?", config.AgentID).
Updates(map[string]interface{}{
"enabled": config.Enabled,
"record_failed": config.RecordFailed,
}).Error
}
// 创建
config.ID = uuid.New().String()
config.CreatedAt = time.Now().UnixMilli()
return r.db.Create(config).Error
}
// DeleteConfigByAgentID 删除探针的配置
func (r *SSHLoginRepo) DeleteConfigByAgentID(agentID string) error {
return r.db.Where("agent_id = ?", agentID).Delete(&models.SSHLoginConfig{}).Error
}
// UpdateConfigStatus 更新配置应用状态
func (r *SSHLoginRepo) UpdateConfigStatus(agentID string, updates map[string]interface{}) error {
return r.db.Model(&models.SSHLoginConfig{}).
Where("agent_id = ?", agentID).
Updates(updates).Error
}
// === 事件相关 ===
// CreateEvent 创建事件记录
-49
View File
@@ -13,26 +13,6 @@ func NewTamperRepo(db *gorm.DB) *TamperRepo {
return &TamperRepo{db: db}
}
// GetConfigByAgentID 根据探针ID获取防篡改配置
func (r *TamperRepo) GetConfigByAgentID(agentID string) (*models.TamperProtectConfig, error) {
var config models.TamperProtectConfig
err := r.db.Where("agent_id = ?", agentID).First(&config).Error
if err != nil {
return nil, err
}
return &config, nil
}
// SaveConfig 保存或更新防篡改配置
func (r *TamperRepo) SaveConfig(config *models.TamperProtectConfig) error {
return r.db.Save(config).Error
}
// DeleteConfig 删除防篡改配置
func (r *TamperRepo) DeleteConfig(agentID string) error {
return r.db.Where("agent_id = ?", agentID).Delete(&models.TamperProtectConfig{}).Error
}
// CreateEvent 创建防篡改事件
func (r *TamperRepo) CreateEvent(event *models.TamperEvent) error {
return r.db.Create(event).Error
@@ -57,36 +37,7 @@ func (r *TamperRepo) GetEventsByAgentID(agentID string, limit, offset int) ([]mo
return events, total, err
}
// CreateAlert 创建防篡改告警
func (r *TamperRepo) CreateAlert(alert *models.TamperAlert) error {
return r.db.Create(alert).Error
}
// GetAlertsByAgentID 获取探针的防篡改告警(分页)
func (r *TamperRepo) GetAlertsByAgentID(agentID string, limit, offset int) ([]models.TamperAlert, int64, error) {
var alerts []models.TamperAlert
var total int64
query := r.db.Model(&models.TamperAlert{}).Where("agent_id = ?", agentID)
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
err := query.Order("timestamp DESC").
Limit(limit).
Offset(offset).
Find(&alerts).Error
return alerts, total, err
}
// DeleteOldEvents 删除旧的事件记录(保留最近N天)
func (r *TamperRepo) DeleteOldEvents(beforeTimestamp int64) error {
return r.db.Where("timestamp < ?", beforeTimestamp).Delete(&models.TamperEvent{}).Error
}
// DeleteOldAlerts 删除旧的告警记录(保留最近N天)
func (r *TamperRepo) DeleteOldAlerts(beforeTimestamp int64) error {
return r.db.Where("timestamp < ?", beforeTimestamp).Delete(&models.TamperAlert{}).Error
}
+44 -37
View File
@@ -13,6 +13,7 @@ import (
"github.com/dushixiang/pika/internal/repo"
"github.com/go-orz/orz"
"go.uber.org/zap"
"gorm.io/datatypes"
"gorm.io/gorm"
)
@@ -480,21 +481,23 @@ func (s *AgentService) UpdateTrafficConfig(ctx context.Context, agentID string,
}
now := time.Now().UnixMilli()
oldResetDay := agent.TrafficResetDay
stats := agent.TrafficStats.Data()
oldResetDay := stats.ResetDay
agent.TrafficLimit = limit
agent.TrafficResetDay = resetDay
stats.Limit = limit
stats.ResetDay = resetDay
// 如果是首次设置或修改重置日期,重置流量统计
if agent.TrafficPeriodStart == 0 || resetDay != oldResetDay {
agent.TrafficUsed = 0
agent.TrafficPeriodStart = now
agent.TrafficBaselineRecv = 0 // 下次上报时会设置正确的基线
agent.TrafficAlertSent80 = false
agent.TrafficAlertSent90 = false
agent.TrafficAlertSent100 = false
if stats.PeriodStart == 0 || resetDay != oldResetDay {
stats.Used = 0
stats.PeriodStart = now
stats.BaselineRecv = 0 // 下次上报时会设置正确的基线
stats.AlertSent80 = false
stats.AlertSent90 = false
stats.AlertSent100 = false
}
agent.TrafficStats = datatypes.NewJSONType(stats)
agent.UpdatedAt = now
return s.AgentRepo.UpdateById(ctx, &agent)
}
@@ -506,32 +509,34 @@ func (s *AgentService) GetTrafficStats(ctx context.Context, agentID string) (*Tr
return nil, err
}
trafficData := agent.TrafficStats.Data()
stats := &TrafficStats{
TrafficLimit: agent.TrafficLimit,
TrafficUsed: agent.TrafficUsed,
TrafficResetDay: agent.TrafficResetDay,
PeriodStart: agent.TrafficPeriodStart,
TrafficLimit: trafficData.Limit,
TrafficUsed: trafficData.Used,
TrafficResetDay: trafficData.ResetDay,
PeriodStart: trafficData.PeriodStart,
AlertsSent: TrafficAlerts{
Sent80: agent.TrafficAlertSent80,
Sent90: agent.TrafficAlertSent90,
Sent100: agent.TrafficAlertSent100,
Sent80: trafficData.AlertSent80,
Sent90: trafficData.AlertSent90,
Sent100: trafficData.AlertSent100,
},
}
// 计算使用百分比
if agent.TrafficLimit > 0 {
stats.TrafficUsedPercent = float64(agent.TrafficUsed) / float64(agent.TrafficLimit) * 100
if agent.TrafficUsed < agent.TrafficLimit {
stats.TrafficRemaining = agent.TrafficLimit - agent.TrafficUsed
if trafficData.Limit > 0 {
stats.TrafficUsedPercent = float64(trafficData.Used) / float64(trafficData.Limit) * 100
if trafficData.Used < trafficData.Limit {
stats.TrafficRemaining = trafficData.Limit - trafficData.Used
} else {
stats.TrafficRemaining = 0
}
}
// 计算下次重置日期和剩余天数
if agent.TrafficResetDay > 0 && agent.TrafficPeriodStart > 0 {
periodStart := time.UnixMilli(agent.TrafficPeriodStart)
nextReset := calculateNextResetDate(periodStart, agent.TrafficResetDay)
if trafficData.ResetDay > 0 && trafficData.PeriodStart > 0 {
periodStart := time.UnixMilli(trafficData.PeriodStart)
nextReset := calculateNextResetDate(periodStart, trafficData.ResetDay)
stats.PeriodEnd = nextReset.UnixMilli()
stats.DaysUntilReset = int(time.Until(nextReset).Hours() / 24)
if stats.DaysUntilReset < 0 {
@@ -550,22 +555,23 @@ func (s *AgentService) ResetAgentTraffic(ctx context.Context, agentID string) er
}
now := time.Now().UnixMilli()
stats := agent.TrafficStats.Data()
updates := map[string]interface{}{
"traffic_used": 0,
"traffic_baseline_recv": 0, // 下次上报时会设置正确的基线
"traffic_period_start": now,
"traffic_alert_sent80": false,
"traffic_alert_sent90": false,
"traffic_alert_sent100": false,
"updated_at": now,
}
stats.Used = 0
stats.BaselineRecv = 0 // 下次上报时会设置正确的基线
stats.PeriodStart = now
stats.AlertSent80 = false
stats.AlertSent90 = false
stats.AlertSent100 = false
agent.TrafficStats = datatypes.NewJSONType(stats)
agent.UpdatedAt = now
s.logger.Info("探针流量已重置",
zap.String("agentId", agentID),
zap.String("agentName", agent.Name))
return s.AgentRepo.UpdateTrafficStats(ctx, agentID, updates)
return s.AgentRepo.UpdateById(ctx, &agent)
}
// CheckAndResetTraffic 检查并重置所有到期的探针流量(定时任务调用)
@@ -599,12 +605,13 @@ func (s *AgentService) CheckAndResetTraffic(ctx context.Context) error {
// shouldResetTraffic 判断是否需要重置流量
func (s *AgentService) shouldResetTraffic(agent *models.Agent, now time.Time) bool {
if agent.TrafficResetDay == 0 || agent.TrafficPeriodStart == 0 {
stats := agent.TrafficStats.Data()
if stats.ResetDay == 0 || stats.PeriodStart == 0 {
return false
}
periodStart := time.UnixMilli(agent.TrafficPeriodStart)
nextReset := calculateNextResetDate(periodStart, agent.TrafficResetDay)
periodStart := time.UnixMilli(stats.PeriodStart)
nextReset := calculateNextResetDate(periodStart, stats.ResetDay)
return now.After(nextReset) || now.Equal(nextReset)
}
+15 -28
View File
@@ -38,8 +38,8 @@ func NewSSHLoginService(logger *zap.Logger, db *gorm.DB, wsManager *websocket.Ma
// === 配置管理 ===
// GetConfig 获取探针的配置
func (s *SSHLoginService) GetConfig(agentID string) (*models.SSHLoginConfig, error) {
config, err := s.repo.GetConfigByAgentID(agentID)
func (s *SSHLoginService) GetConfig(agentID string) (*models.SSHLoginConfigData, error) {
config, err := s.agentRepo.GetSSHLoginConfig(context.Background(), agentID)
if err != nil {
return nil, err
}
@@ -48,7 +48,7 @@ func (s *SSHLoginService) GetConfig(agentID string) (*models.SSHLoginConfig, err
// UpdateConfig 更新配置并下发到 Agent
// 返回: config - 配置对象, configSent - 是否成功下发到 Agent, error - 错误信息
func (s *SSHLoginService) UpdateConfig(ctx context.Context, agentID string, enabled, recordFailed bool) (*models.SSHLoginConfig, bool, error) {
func (s *SSHLoginService) UpdateConfig(ctx context.Context, agentID string, enabled bool) (*models.SSHLoginConfigData, bool, error) {
// 检查探针是否存在
agent, err := s.agentRepo.FindById(ctx, agentID)
if err != nil {
@@ -56,20 +56,18 @@ func (s *SSHLoginService) UpdateConfig(ctx context.Context, agentID string, enab
}
// 保存配置到数据库
config := &models.SSHLoginConfig{
AgentID: agentID,
Enabled: enabled,
RecordFailed: recordFailed,
config := &models.SSHLoginConfigData{
Enabled: enabled,
}
if err := s.repo.UpsertConfig(config); err != nil {
if err := s.agentRepo.UpdateSSHLoginConfig(ctx, agentID, config); err != nil {
return nil, false, err
}
// 下发配置到 Agent
configSent := false
if agent.Status == 1 { // 仅在探针在线时尝试下发
if err := s.sendConfigToAgent(agentID, enabled, recordFailed); err != nil {
if err := s.sendConfigToAgent(agentID, enabled); err != nil {
s.logger.Warn("下发SSH登录监控配置到Agent失败", zap.Error(err), zap.String("agentId", agentID))
} else {
s.logger.Info("成功下发SSH登录监控配置", zap.String("agentId", agentID), zap.Bool("enabled", enabled))
@@ -83,10 +81,9 @@ func (s *SSHLoginService) UpdateConfig(ctx context.Context, agentID string, enab
}
// sendConfigToAgent 下发配置到 Agent
func (s *SSHLoginService) sendConfigToAgent(agentID string, enabled, recordFailed bool) error {
func (s *SSHLoginService) sendConfigToAgent(agentID string, enabled bool) error {
configData := protocol.SSHLoginConfig{
Enabled: enabled,
RecordFailed: recordFailed,
Enabled: enabled,
}
message := protocol.OutboundMessage{
@@ -105,7 +102,7 @@ func (s *SSHLoginService) sendConfigToAgent(agentID string, enabled, recordFaile
// HandleConfigResult 处理 Agent 上报的配置应用结果
func (s *SSHLoginService) HandleConfigResult(agentID string, result protocol.SSHLoginConfigResult) error {
// 获取现有配置
config, err := s.repo.GetConfigByAgentID(agentID)
config, err := s.agentRepo.GetSSHLoginConfig(context.Background(), agentID)
if err != nil {
return fmt.Errorf("获取配置失败: %w", err)
}
@@ -120,14 +117,10 @@ func (s *SSHLoginService) HandleConfigResult(agentID string, result protocol.SSH
status = "failed"
}
updates := map[string]interface{}{
"apply_status": status,
"apply_message": result.Message,
"apply_error": result.Error,
"last_applied_at": getCurrentTimestamp(),
}
config.ApplyStatus = status
config.ApplyMessage = result.Message
if err := s.repo.UpdateConfigStatus(agentID, updates); err != nil {
if err := s.agentRepo.UpdateSSHLoginConfig(context.Background(), agentID, config); err != nil {
s.logger.Error("更新配置应用状态失败", zap.Error(err), zap.String("agentId", agentID))
return err
}
@@ -141,8 +134,7 @@ func (s *SSHLoginService) HandleConfigResult(agentID string, result protocol.SSH
} else {
s.logger.Warn("Agent应用SSH登录监控配置失败",
zap.String("agentId", agentID),
zap.String("message", result.Message),
zap.String("error", result.Error))
zap.String("message", result.Message))
}
return nil
@@ -158,7 +150,7 @@ func getCurrentTimestamp() int64 {
// HandleEvent 处理 Agent 上报的事件
func (s *SSHLoginService) HandleEvent(agentID string, eventData protocol.SSHLoginEvent) error {
// 检查是否启用监控
config, err := s.repo.GetConfigByAgentID(agentID)
config, err := s.agentRepo.GetSSHLoginConfig(context.Background(), agentID)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
@@ -169,11 +161,6 @@ func (s *SSHLoginService) HandleEvent(agentID string, eventData protocol.SSHLogi
return nil
}
// 如果不记录失败登录且状态为失败,忽略
if !config.RecordFailed && eventData.Status == "failed" {
return nil
}
// 去重检查(避免 Agent 重启时重复上报)
existing, err := s.repo.FindEventByTimestamp(agentID, eventData.Timestamp, 5000) // 5秒容差
if err != nil {
+14 -62
View File
@@ -1,6 +1,7 @@
package service
import (
"context"
"encoding/json"
"errors"
"time"
@@ -11,12 +12,12 @@ import (
"github.com/dushixiang/pika/internal/websocket"
"github.com/google/uuid"
"go.uber.org/zap"
"gorm.io/datatypes"
"gorm.io/gorm"
)
type TamperService struct {
logger *zap.Logger
agentRepo *repo.AgentRepo
tamperRepo *repo.TamperRepo
wsManager *websocket.Manager
}
@@ -24,14 +25,15 @@ type TamperService struct {
func NewTamperService(logger *zap.Logger, db *gorm.DB, wsManager *websocket.Manager) *TamperService {
return &TamperService{
logger: logger,
agentRepo: repo.NewAgentRepo(db),
tamperRepo: repo.NewTamperRepo(db),
wsManager: wsManager,
}
}
// GetConfigByAgentID 获取探针的防篡改配置
func (s *TamperService) GetConfigByAgentID(agentID string) (*models.TamperProtectConfig, error) {
config, err := s.tamperRepo.GetConfigByAgentID(agentID)
func (s *TamperService) GetConfigByAgentID(agentID string) (*models.TamperProtectConfigData, error) {
config, err := s.agentRepo.GetTamperProtectConfig(context.Background(), agentID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil // 没有配置不算错误
@@ -42,11 +44,9 @@ func (s *TamperService) GetConfigByAgentID(agentID string) (*models.TamperProtec
}
// UpdateConfig 更新探针的防篡改配置
func (s *TamperService) UpdateConfig(agentID string, paths []string) (*models.TamperProtectConfig, error) {
now := time.Now().UnixMilli()
func (s *TamperService) UpdateConfig(agentID string, enabled bool, paths []string) (*models.TamperProtectConfigData, error) {
// 查找现有配置
config, err := s.tamperRepo.GetConfigByAgentID(agentID)
config, err := s.agentRepo.GetTamperProtectConfig(context.Background(), agentID)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
@@ -60,23 +60,14 @@ func (s *TamperService) UpdateConfig(agentID string, paths []string) (*models.Ta
// 计算新增和移除的路径
added, removed := s.calculatePathDiff(oldPaths, paths)
if config == nil {
// 创建新配置
config = &models.TamperProtectConfig{
ID: uuid.New().String(),
AgentID: agentID,
Paths: datatypes.NewJSONSlice(paths),
CreatedAt: now,
UpdatedAt: now,
}
} else {
// 更新现有配置
config.Paths = datatypes.NewJSONSlice(paths)
config.UpdatedAt = now
// 创建或更新配置
newConfig := &models.TamperProtectConfigData{
Enabled: enabled,
Paths: paths,
}
// 保存配置到数据库
if err := s.tamperRepo.SaveConfig(config); err != nil {
if err := s.agentRepo.UpdateTamperProtectConfig(context.Background(), agentID, newConfig); err != nil {
return nil, err
}
@@ -96,7 +87,7 @@ func (s *TamperService) UpdateConfig(agentID string, paths []string) (*models.Ta
zap.Int("totalPaths", len(paths)))
}
return config, nil
return newConfig, nil
}
// calculatePathDiff 计算路径的新增和移除
@@ -154,11 +145,6 @@ func (s *TamperService) sendConfigToAgent(agentID string, added, removed []strin
return s.wsManager.SendToClient(agentID, msgBytes)
}
// DeleteConfig 删除探针的防篡改配置
func (s *TamperService) DeleteConfig(agentID string) error {
return s.tamperRepo.DeleteConfig(agentID)
}
// CreateEvent 创建防篡改事件
func (s *TamperService) CreateEvent(agentID, path, operation, details string, timestamp int64) error {
event := &models.TamperEvent{
@@ -189,44 +175,10 @@ func (s *TamperService) GetEventsByAgentID(agentID string, pageNum, pageSize int
return s.tamperRepo.GetEventsByAgentID(agentID, pageSize, offset)
}
// CreateAlert 创建防篡改告警
func (s *TamperService) CreateAlert(agentID, path, details string, restored bool, timestamp int64) error {
alert := &models.TamperAlert{
ID: uuid.New().String(),
AgentID: agentID,
Path: path,
Details: details,
Restored: restored,
Timestamp: timestamp,
CreatedAt: time.Now().UnixMilli(),
}
return s.tamperRepo.CreateAlert(alert)
}
// GetAlertsByAgentID 获取探针的防篡改告警
func (s *TamperService) GetAlertsByAgentID(agentID string, pageNum, pageSize int) ([]models.TamperAlert, int64, error) {
if pageNum < 1 {
pageNum = 1
}
if pageSize < 1 {
pageSize = 20
}
if pageSize > 100 {
pageSize = 100
}
offset := (pageNum - 1) * pageSize
return s.tamperRepo.GetAlertsByAgentID(agentID, pageSize, offset)
}
// CleanupOldRecords 清理旧记录(保留最近30天)
func (s *TamperService) CleanupOldRecords() error {
// 30天前的时间戳
threshold := time.Now().AddDate(0, 0, -30).UnixMilli()
if err := s.tamperRepo.DeleteOldEvents(threshold); err != nil {
return err
}
return s.tamperRepo.DeleteOldAlerts(threshold)
return s.tamperRepo.DeleteOldEvents(threshold)
}
+35 -27
View File
@@ -8,6 +8,7 @@ import (
"github.com/dushixiang/pika/internal/models"
"github.com/dushixiang/pika/internal/repo"
"go.uber.org/zap"
"gorm.io/datatypes"
"gorm.io/gorm"
)
@@ -32,66 +33,73 @@ func (s *TrafficService) UpdateAgentTraffic(ctx context.Context, agentID string,
return err
}
// 获取流量统计数据
stats := agent.TrafficStats.Data()
// 如果未配置流量限制,跳过更新
if agent.TrafficLimit == 0 && agent.TrafficResetDay == 0 {
if stats.Limit == 0 && stats.ResetDay == 0 {
return nil
}
// 初始化基线(首次统计)
if agent.TrafficBaselineRecv == 0 {
agent.TrafficBaselineRecv = currentRecvTotal
agent.TrafficUsed = 0
if agent.TrafficPeriodStart == 0 {
agent.TrafficPeriodStart = time.Now().UnixMilli()
if stats.BaselineRecv == 0 {
stats.BaselineRecv = currentRecvTotal
stats.Used = 0
if stats.PeriodStart == 0 {
stats.PeriodStart = time.Now().UnixMilli()
}
agent.TrafficStats = datatypes.NewJSONType(stats)
return s.agentRepo.UpdateById(ctx, &agent)
}
// 检测计数器重置(探针重启)
if currentRecvTotal < agent.TrafficBaselineRecv {
if currentRecvTotal < stats.BaselineRecv {
s.logger.Warn("检测到流量计数器重置",
zap.String("agentId", agentID),
zap.Uint64("baseline", agent.TrafficBaselineRecv),
zap.Uint64("baseline", stats.BaselineRecv),
zap.Uint64("current", currentRecvTotal))
agent.TrafficBaselineRecv = currentRecvTotal
// 保持 TrafficUsed 不变,避免丢失已统计的流量
stats.BaselineRecv = currentRecvTotal
// 保持 Used 不变,避免丢失已统计的流量
} else {
// 计算使用量
agent.TrafficUsed = currentRecvTotal - agent.TrafficBaselineRecv
stats.Used = currentRecvTotal - stats.BaselineRecv
}
// 检查告警(如果配置了限额)
if agent.TrafficLimit > 0 {
s.checkTrafficAlerts(ctx, &agent)
if stats.Limit > 0 {
s.checkTrafficAlerts(ctx, &agent, &stats)
}
// 保存更新后的统计数据
agent.TrafficStats = datatypes.NewJSONType(stats)
// 更新数据库
return s.agentRepo.UpdateById(ctx, &agent)
}
// checkTrafficAlerts 检查并发送流量告警
func (s *TrafficService) checkTrafficAlerts(ctx context.Context, agent *models.Agent) {
usagePercent := float64(agent.TrafficUsed) / float64(agent.TrafficLimit) * 100
func (s *TrafficService) checkTrafficAlerts(ctx context.Context, agent *models.Agent, stats *models.TrafficStatsData) {
usagePercent := float64(stats.Used) / float64(stats.Limit) * 100
// 100% 告警
if usagePercent >= 100 && !agent.TrafficAlertSent100 {
s.sendTrafficAlert(ctx, agent, 100, usagePercent)
agent.TrafficAlertSent100 = true
if usagePercent >= 100 && !stats.AlertSent100 {
s.sendTrafficAlert(ctx, agent, stats, 100, usagePercent)
stats.AlertSent100 = true
}
// 90% 告警
if usagePercent >= 90 && !agent.TrafficAlertSent90 {
s.sendTrafficAlert(ctx, agent, 90, usagePercent)
agent.TrafficAlertSent90 = true
if usagePercent >= 90 && !stats.AlertSent90 {
s.sendTrafficAlert(ctx, agent, stats, 90, usagePercent)
stats.AlertSent90 = true
}
// 80% 告警
if usagePercent >= 80 && !agent.TrafficAlertSent80 {
s.sendTrafficAlert(ctx, agent, 80, usagePercent)
agent.TrafficAlertSent80 = true
if usagePercent >= 80 && !stats.AlertSent80 {
s.sendTrafficAlert(ctx, agent, stats, 80, usagePercent)
stats.AlertSent80 = true
}
}
// sendTrafficAlert 发送流量告警
func (s *TrafficService) sendTrafficAlert(ctx context.Context, agent *models.Agent, threshold int, actualPercent float64) {
func (s *TrafficService) sendTrafficAlert(ctx context.Context, agent *models.Agent, stats *models.TrafficStatsData, threshold int, actualPercent float64) {
level := "info"
if threshold == 100 {
level = "critical"
@@ -106,8 +114,8 @@ func (s *TrafficService) sendTrafficAlert(ctx context.Context, agent *models.Age
AlertType: "traffic",
Message: fmt.Sprintf("流量使用已达到%d%%,当前使用%.2f%%(%s/%s)",
threshold, actualPercent,
formatBytes(agent.TrafficUsed),
formatBytes(agent.TrafficLimit)),
formatBytes(stats.Used),
formatBytes(stats.Limit)),
Threshold: float64(threshold),
ActualValue: actualPercent,
Level: level,
+24 -43
View File
@@ -235,11 +235,6 @@ func (a *Agent) runOnce(ctx context.Context, onConnected func()) error {
a.tamperEventLoop(ctx, conn, done)
})
// 启动防篡改属性告警监控
wg.Go(func() {
a.tamperAlertLoop(ctx, conn, done)
})
// 启动 SSH 登录事件监控
wg.Go(func() {
a.sshLoginEventLoop(ctx, conn, done)
@@ -626,7 +621,7 @@ func (a *Agent) handleTamperProtect(data json.RawMessage) {
var tamperProtectConfig protocol.TamperProtectConfig
if err := json.Unmarshal(data, &tamperProtectConfig); err != nil {
slog.Warn("解析防篡改保护配置失败", "error", err)
a.sendTamperProtectResponse(false, "解析配置失败", nil, nil, nil, err.Error())
a.sendTamperProtectResponse(false, fmt.Sprintf("解析配置失败: %v", err), nil, nil, nil)
return
}
@@ -641,7 +636,7 @@ func (a *Agent) handleTamperProtect(data json.RawMessage) {
// 如果没有新增也没有移除,不需要做任何操作
if len(tamperProtectConfig.Added) == 0 && len(tamperProtectConfig.Removed) == 0 {
slog.Info("配置无变化,跳过更新")
a.sendTamperProtectResponse(true, "配置无变化", a.tamperProtector.GetProtectedPaths(), []string{}, []string{}, "")
a.sendTamperProtectResponse(true, "", a.tamperProtector.GetProtectedPaths(), []string{}, []string{})
return
}
@@ -653,9 +648,9 @@ func (a *Agent) handleTamperProtect(data json.RawMessage) {
slog.Warn("应用增量更新失败", "error", err)
// 即使有错误也返回部分成功的结果
if result != nil {
a.sendTamperProtectResponse(false, "部分更新失败", result.Current, result.Added, result.Removed, err.Error())
a.sendTamperProtectResponse(false, fmt.Sprintf("应用增量更新失败: %v", err), result.Current, result.Added, result.Removed)
} else {
a.sendTamperProtectResponse(false, "更新失败", nil, nil, nil, err.Error())
a.sendTamperProtectResponse(false, fmt.Sprintf("应用增量更新失败: %v", err), nil, nil, nil)
}
return
}
@@ -664,11 +659,11 @@ func (a *Agent) handleTamperProtect(data json.RawMessage) {
message := fmt.Sprintf("防篡改保护已更新: 新增 %d 个, 移除 %d 个, 当前保护 %d 个目录",
len(result.Added), len(result.Removed), len(result.Current))
slog.Info(message)
a.sendTamperProtectResponse(true, message, result.Current, result.Added, result.Removed, "")
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) {
func (a *Agent) sendTamperProtectResponse(success bool, message string, paths []string, added []string, removed []string) {
conn := a.getActiveConn()
if conn == nil {
return
@@ -680,7 +675,6 @@ func (a *Agent) sendTamperProtectResponse(success bool, message string, paths []
Paths: paths,
Added: added,
Removed: removed,
Error: errMsg,
}
if err := conn.WriteJSON(protocol.OutboundMessage{
@@ -691,9 +685,10 @@ func (a *Agent) sendTamperProtectResponse(success bool, message string, paths []
}
}
// tamperEventLoop 防篡改事件监控循环
// tamperEventLoop 防篡改事件监控循环(包含事件和告警)
func (a *Agent) tamperEventLoop(ctx context.Context, conn *safeConn, done chan struct{}) {
eventCh := a.tamperProtector.GetEvents()
alertCh := a.tamperProtector.GetAlerts()
for {
select {
@@ -718,40 +713,27 @@ func (a *Agent) tamperEventLoop(ctx context.Context, conn *safeConn, done chan s
} else {
slog.Info("已上报防篡改事件", "path", event.Path, "operation", event.Operation)
}
}
}
}
// tamperAlertLoop 防篡改属性告警监控循环
func (a *Agent) tamperAlertLoop(ctx context.Context, conn *safeConn, done chan struct{}) {
alertCh := a.tamperProtector.GetAlerts()
for {
select {
case <-done:
return
case <-ctx.Done():
return
case alert := <-alertCh:
// 发送属性篡改告警到服务端
alertData := protocol.TamperAlertData{
// 将防篡改告警转换为事件格式发送
eventData := protocol.TamperEventData{
Path: alert.Path,
Operation: "attr_tamper", // 使用特殊的操作类型标识属性篡改
Timestamp: alert.Timestamp.UnixMilli(),
Details: alert.Details,
Restored: alert.Restored,
}
if err := conn.WriteJSON(protocol.OutboundMessage{
Type: protocol.MessageTypeTamperAlert,
Data: alertData,
Type: protocol.MessageTypeTamperEvent,
Data: eventData,
}); err != nil {
slog.Warn("发送属性篡改告警失败", "error", err)
slog.Warn("发送防篡改告警失败", "error", err)
} else {
status := "未恢复"
if alert.Restored {
status = "已恢复"
}
slog.Info("已上报属性篡改告警", "path", alert.Path, "status", status)
slog.Info("已上报防篡改告警", "path", alert.Path, "status", status)
}
}
}
@@ -868,42 +850,41 @@ func (a *Agent) handleUninstall() {
// handleSSHLoginConfig 处理 SSH 登录监控配置
func (a *Agent) handleSSHLoginConfig(conn *websocket.Conn, data json.RawMessage) {
var config protocol.SSHLoginConfig
if err := json.Unmarshal(data, &config); err != nil {
var sshLoginConfig protocol.SSHLoginConfig
if err := json.Unmarshal(data, &sshLoginConfig); err != nil {
slog.Warn("解析SSH登录监控配置失败", "error", err)
// 发送失败结果
a.sendSSHLoginConfigResult(conn, false, false, "解析配置失败", err.Error())
a.sendSSHLoginConfigResult(conn, false, false, err.Error())
return
}
slog.Info("收到SSH登录监控配置", "enabled", config.Enabled, "recordFailed", config.RecordFailed)
slog.Info("收到SSH登录监控配置", "enabled", sshLoginConfig.Enabled, "recordFailed", sshLoginConfig.RecordFailed)
// 应用配置
ctx := context.Background()
if err := a.sshMonitor.Start(ctx, config); err != nil {
if err := a.sshMonitor.Start(ctx, sshLoginConfig); err != nil {
slog.Warn("应用SSH登录监控配置失败", "error", err)
// 发送失败结果,包含详细错误信息
a.sendSSHLoginConfigResult(conn, false, config.Enabled, "应用配置失败", err.Error())
a.sendSSHLoginConfigResult(conn, false, sshLoginConfig.Enabled, err.Error())
return
}
// 发送成功结果
message := "配置已成功应用"
if config.Enabled {
if sshLoginConfig.Enabled {
message = "SSH登录监控已启用"
} else {
message = "SSH登录监控已禁用"
}
a.sendSSHLoginConfigResult(conn, true, config.Enabled, message, "")
a.sendSSHLoginConfigResult(conn, true, sshLoginConfig.Enabled, message)
}
// sendSSHLoginConfigResult 发送 SSH 登录监控配置应用结果
func (a *Agent) sendSSHLoginConfigResult(conn *websocket.Conn, success bool, enabled bool, message string, errorMsg string) {
func (a *Agent) sendSSHLoginConfigResult(conn *websocket.Conn, success bool, enabled bool, message string) {
result := protocol.SSHLoginConfigResult{
Success: success,
Enabled: enabled,
Message: message,
Error: errorMsg,
}
msg := protocol.OutboundMessage{
+10 -2
View File
@@ -4,7 +4,10 @@ import qs from 'qs';
export interface TamperConfig {
id: string;
agentId: string;
enabled: boolean;
paths: string[];
applyStatus?: string;
applyMessage?: string;
createdAt: number;
updatedAt: number;
}
@@ -45,10 +48,10 @@ export const getTamperConfig = (agentId: string) => {
};
// 更新防篡改配置
export const updateTamperConfig = (agentId: string, paths: string[]) => {
export const updateTamperConfig = (agentId: string, enabled: boolean, paths: string[]) => {
return request.put<{ success: boolean; message: string; data: TamperConfig }>(
`/admin/agents/${agentId}/tamper/config`,
{paths}
{enabled, paths}
);
};
@@ -69,3 +72,8 @@ export const getTamperAlerts = (agentId: string, pageIndex: number = 1, pageSize
`/admin/agents/${agentId}/tamper/alerts?${paramStr}`,
);
};
// 删除防篡改事件
export const deleteTamperEvents = (agentId: string) => {
return request.delete<{ success: boolean; message: string }>(`/admin/agents/${agentId}/tamper/events`);
};
+89 -77
View File
@@ -1,8 +1,10 @@
import React, {useEffect, useState} from 'react';
import {Alert, App, Button, Card, Form, Switch} from 'antd';
import React, {useEffect} from 'react';
import {Alert, App, Button, Card, Form, Space, Switch} from 'antd';
import {Save, Terminal} from 'lucide-react';
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
import type {SSHLoginConfig as SSHLoginConfigType} from '@/types';
import {getSSHLoginConfig, updateSSHLoginConfig} from '@/api/agent';
import {getErrorMessage} from '@/lib/utils';
interface SSHLoginConfigProps {
agentId: string;
@@ -11,56 +13,47 @@ interface SSHLoginConfigProps {
const SSHLoginConfig: React.FC<SSHLoginConfigProps> = ({agentId}) => {
const {message} = App.useApp();
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [config, setConfig] = useState<SSHLoginConfigType | null>(null);
const queryClient = useQueryClient();
// 加载配置
const loadConfig = async () => {
try {
setLoading(true);
const cfg = await getSSHLoginConfig(agentId);
setConfig(cfg);
// 设置表单值
form.setFieldsValue({
enabled: cfg.enabled || false,
recordFailed: cfg.recordFailed !== undefined ? cfg.recordFailed : true,
// 获取 SSH 登录监控配置
const {data: config, isLoading} = useQuery({
queryKey: ['sshLoginConfig', agentId],
queryFn: () => getSSHLoginConfig(agentId),
});
// 保存配置 mutation
const saveMutation = useMutation({
mutationFn: async () => {
const values = form.getFieldsValue();
return updateSSHLoginConfig(agentId, {
enabled: values.enabled,
recordFailed: values.recordFailed,
});
} catch (error) {
console.error('Failed to load SSH login config:', error);
// 发生错误时使用默认配置
},
onSuccess: () => {
message.success('配置已保存');
queryClient.invalidateQueries({queryKey: ['sshLoginConfig', agentId]});
},
onError: (error: unknown) => {
console.error('Failed to save SSH login config:', error);
message.error(getErrorMessage(error, '配置保存失败'));
},
});
// 初始化表单值
useEffect(() => {
if (config) {
form.setFieldsValue({
enabled: config.enabled || false,
recordFailed: config.recordFailed !== undefined ? config.recordFailed : true,
});
} else {
form.setFieldsValue({
enabled: false,
recordFailed: true,
});
} finally {
setLoading(false);
}
};
// 保存配置
const handleSaveConfig = async () => {
try {
setSaving(true);
const values = form.getFieldsValue();
await updateSSHLoginConfig(agentId, {
enabled: values.enabled,
recordFailed: values.recordFailed,
});
// 显示来自后端的消息
message.success('配置已保存');
await loadConfig();
} catch (error: any) {
console.error('Failed to save SSH login config:', error);
message.error(error.response?.data?.error || '配置保存失败');
} finally {
setSaving(false);
}
};
useEffect(() => {
loadConfig();
}, [agentId]);
}, [config, form]);
return (
<Card
@@ -74,46 +67,65 @@ const SSHLoginConfig: React.FC<SSHLoginConfigProps> = ({agentId}) => {
<Button
type="primary"
icon={<Save size={16}/>}
onClick={handleSaveConfig}
loading={saving}
onClick={() => saveMutation.mutate()}
loading={saveMutation.isPending}
>
保存配置
</Button>
}
loading={loading}
loading={isLoading}
>
<Form
form={form}
layout="vertical"
initialValues={{
enabled: false,
recordFailed: true,
}}
>
<Form.Item
label="启用监控"
name="enabled"
valuePropName="checked"
extra={'启用后,探针将自动安装 PAM Hook 并开始监控 SSH 登录事件'}
<Space direction="vertical" style={{width: '100%'}} size="large">
<Form
form={form}
layout="vertical"
initialValues={{
enabled: false,
recordFailed: true,
}}
>
<Switch
checkedChildren="已启用"
unCheckedChildren="已禁用"
/>
</Form.Item>
<Form.Item
label="启用监控"
name="enabled"
valuePropName="checked"
extra={'启用后,探针将自动安装 PAM Hook 并开始监控 SSH 登录事件'}
>
<Switch
checkedChildren="已启用"
unCheckedChildren="已禁用"
/>
</Form.Item>
<Form.Item
label="记录失败登录"
name="recordFailed"
valuePropName="checked"
extra={'是否记录失败的 SSH 登录尝试(有助于发现暴力破解攻击)'}
>
<Switch
checkedChildren="记录"
unCheckedChildren="不记录"
<Form.Item
label="记录失败登录"
name="recordFailed"
valuePropName="checked"
extra={'是否记录失败的 SSH 登录尝试(有助于发现暴力破解攻击)'}
>
<Switch
checkedChildren="记录"
unCheckedChildren="不记录"
/>
</Form.Item>
</Form>
{config?.applyStatus && (
<Alert
message={
config.applyStatus === 'success' ? '配置应用成功' :
config.applyStatus === 'failed' ? '配置应用失败' :
'配置应用中...'
}
description={config.applyMessage}
type={
config.applyStatus === 'success' ? 'success' :
config.applyStatus === 'failed' ? 'error' :
'info'
}
showIcon
/>
</Form.Item>
</Form>
)}
</Space>
</Card>
);
};
+16 -10
View File
@@ -3,8 +3,10 @@ import {App, Button, Tag, Tooltip} from 'antd';
import {Terminal} from 'lucide-react';
import type {ActionType, ProColumns} from '@ant-design/pro-table';
import ProTable from '@ant-design/pro-table';
import {useMutation} from '@tanstack/react-query';
import type {SSHLoginEvent} from '@/types';
import {deleteSSHLoginEvents, getSSHLoginEvents} from '@/api/agent';
import {getErrorMessage} from '@/lib/utils';
import dayjs from 'dayjs';
interface SSHLoginEventsProps {
@@ -112,6 +114,19 @@ const SSHLoginEvents: React.FC<SSHLoginEventsProps> = ({agentId}) => {
},
];
// 删除所有事件 mutation
const deleteMutation = useMutation({
mutationFn: () => deleteSSHLoginEvents(agentId),
onSuccess: () => {
message.success('所有事件已删除');
actionRef.current?.reload();
},
onError: (error: unknown) => {
console.error('Failed to delete SSH login events:', error);
message.error(getErrorMessage(error, '删除失败'));
},
});
// 删除所有事件
const handleDeleteAllEvents = () => {
modal.confirm({
@@ -120,16 +135,7 @@ const SSHLoginEvents: React.FC<SSHLoginEventsProps> = ({agentId}) => {
okText: '确定删除',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
try {
await deleteSSHLoginEvents(agentId);
message.success('所有事件已删除');
actionRef.current?.reload();
} catch (error: any) {
console.error('Failed to delete SSH login events:', error);
message.error(error.response?.data?.error || '删除失败');
}
},
onOk: () => deleteMutation.mutate(),
});
};
+19 -359
View File
@@ -1,385 +1,45 @@
import React, {useEffect, useState} from 'react';
import {AlertTriangle, FileWarning, Plus, RefreshCw, Save, Shield, Trash2} from 'lucide-react';
import {
getTamperAlerts,
getTamperConfig,
getTamperEvents,
type TamperAlert,
type TamperConfig,
type TamperEvent,
updateTamperConfig
} from '@/api/tamper.ts';
import {App} from "antd";
import React, {useState} from 'react';
import TamperProtectionConfig from './TamperProtectionConfig';
import TamperProtectionEvents from './TamperProtectionEvents';
interface TamperProtectionProps {
agentId: string;
}
const TamperProtection: React.FC<TamperProtectionProps> = ({agentId}) => {
const [activeTab, setActiveTab] = useState<'config' | 'events' | 'alerts'>('config');
const [config, setConfig] = useState<TamperConfig | null>(null);
const [events, setEvents] = useState<TamperEvent[]>([]);
const [alerts, setAlerts] = useState<TamperAlert[]>([]);
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
let {message} = App.useApp();
// 配置编辑状态
const [editPaths, setEditPaths] = useState<string[]>([]);
const [newPath, setNewPath] = useState('');
// 分页状态
const [eventsPage, setEventsPage] = useState(1);
const [eventsTotal, setEventsTotal] = useState(0);
const [alertsPage, setAlertsPage] = useState(1);
const [alertsTotal, setAlertsTotal] = useState(0);
// 加载配置
const loadConfig = async () => {
try {
setLoading(true);
const response = await getTamperConfig(agentId);
if (response.data.success && response.data.data) {
setConfig(response.data.data);
setEditPaths(response.data.data.paths || []);
} else {
setEditPaths([]);
}
} catch (error) {
console.error('Failed to load tamper config:', error);
} finally {
setLoading(false);
}
};
// 加载事件
const loadEvents = async (page: number = 1) => {
try {
setLoading(true);
const response = await getTamperEvents(agentId, page, 20);
if (response.data.success) {
setEvents(response.data.data.items || []);
setEventsTotal(response.data.data.total || 0);
setEventsPage(page);
}
} catch (error) {
console.error('Failed to load tamper events:', error);
} finally {
setLoading(false);
}
};
// 加载告警
const loadAlerts = async (page: number = 1) => {
try {
setLoading(true);
const response = await getTamperAlerts(agentId, page, 20);
if (response.data.success) {
setAlerts(response.data.data.items || []);
setAlertsTotal(response.data.data.total || 0);
setAlertsPage(page);
}
} catch (error) {
console.error('Failed to load tamper alerts:', error);
} finally {
setLoading(false);
}
};
// 保存配置
const handleSaveConfig = async () => {
try {
setSaving(true);
const response = await updateTamperConfig(agentId, editPaths);
if (response.data.success) {
message.success('配置保存成功!');
await loadConfig();
}
} catch (error) {
console.error('Failed to save config:', error);
message.error('配置保存失败,请重试。');
} finally {
setSaving(false);
}
};
// 添加路径
const handleAddPath = () => {
if (newPath.trim() && !editPaths.includes(newPath.trim())) {
setEditPaths([...editPaths, newPath.trim()]);
setNewPath('');
}
};
// 删除路径
const handleRemovePath = (path: string) => {
setEditPaths(editPaths.filter(p => p !== path));
};
// 初始化加载
useEffect(() => {
if (activeTab === 'config') {
loadConfig();
} else if (activeTab === 'events') {
loadEvents(1);
} else if (activeTab === 'alerts') {
loadAlerts(1);
}
}, [activeTab, agentId]);
// 格式化时间
const formatTime = (timestamp: number) => {
return new Date(timestamp).toLocaleString('zh-CN');
};
const [activeTab, setActiveTab] = useState<'config' | 'events'>('config');
return (
<div className="space-y-6">
{/* 标签页导航 */}
<div className="flex space-x-2 border-b border-slate-200">
<div className="space-y-4">
{/* Tab 切换 */}
<div className="flex gap-2 border-b border-gray-200">
<button
onClick={() => setActiveTab('config')}
className={`px-4 py-2 text-sm font-medium transition ${
className={`px-4 py-2 text-sm font-medium transition-colors ${
activeTab === 'config'
? 'border-b-2 border-blue-600 text-blue-600'
: 'text-slate-500 hover:text-slate-700'
? 'border-b-2 border-blue-500 text-blue-600'
: 'text-gray-600 hover:text-gray-900'
}`}
onClick={() => setActiveTab('config')}
>
<Shield className="inline h-4 w-4 mr-1"/>
保护配置
</button>
<button
onClick={() => setActiveTab('events')}
className={`px-4 py-2 text-sm font-medium transition ${
className={`px-4 py-2 text-sm font-medium transition-colors ${
activeTab === 'events'
? 'border-b-2 border-blue-600 text-blue-600'
: 'text-slate-500 hover:text-slate-700'
? 'border-b-2 border-blue-500 text-blue-600'
: 'text-gray-600 hover:text-gray-900'
}`}
onClick={() => setActiveTab('events')}
>
<FileWarning className="inline h-4 w-4 mr-1"/>
文件事件
</button>
<button
onClick={() => setActiveTab('alerts')}
className={`px-4 py-2 text-sm font-medium transition ${
activeTab === 'alerts'
? 'border-b-2 border-blue-600 text-blue-600'
: 'text-slate-500 hover:text-slate-700'
}`}
>
<AlertTriangle className="inline h-4 w-4 mr-1"/>
属性告警
</button>
</div>
{/* 配置标签页 */}
{activeTab === 'config' && (
<div className="space-y-4">
<div className="rounded-lg border border-slate-200 bg-blue-50 p-4">
<p className="text-sm text-slate-700">
<Shield className="inline h-4 w-4 mr-1 text-blue-600"/>
防篡改保护通过设置目录的不可变属性来防止文件被修改、删除或重命名。配置更新后将实时同步到探针。
</p>
</div>
{/* 配置面板 */}
{activeTab === 'config' && <TamperProtectionConfig agentId={agentId}/>}
{/* 添加路径 */}
<div className="flex gap-2">
<input
type="text"
value={newPath}
onChange={(e) => setNewPath(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleAddPath()}
placeholder="输入要保护的目录路径,如 /etc/nginx"
className="flex-1 rounded-lg border border-slate-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200"
/>
<button
onClick={handleAddPath}
className="flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
>
<Plus className="h-4 w-4"/>
添加
</button>
</div>
{/* 路径列表 */}
<div className="space-y-2">
{editPaths.length === 0 ? (
<div className="rounded-lg border border-dashed border-slate-300 p-8 text-center">
<Shield className="mx-auto h-12 w-12 text-slate-300"/>
<p className="mt-2 text-sm text-slate-500">暂未配置保护目录</p>
</div>
) : (
editPaths.map((path, index) => (
<div
key={index}
className="flex items-center justify-between rounded-lg border border-slate-200 bg-white p-3"
>
<span className="font-mono text-sm text-slate-700">{path}</span>
<button
onClick={() => handleRemovePath(path)}
className="text-red-600 hover:text-red-700"
>
<Trash2 className="h-4 w-4"/>
</button>
</div>
))
)}
</div>
{/* 保存按钮 */}
<div className="flex justify-end gap-2">
<button
onClick={loadConfig}
disabled={loading}
className="flex items-center gap-2 rounded-lg border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50"
>
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`}/>
重新加载
</button>
<button
onClick={handleSaveConfig}
disabled={saving}
className="flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
>
<Save className="h-4 w-4"/>
{saving ? '保存中...' : '保存配置'}
</button>
</div>
</div>
)}
{/* 事件标签页 */}
{activeTab === 'events' && (
<div className="space-y-4">
<div className="text-sm text-slate-500">
共 {eventsTotal} 条事件记录
</div>
{events.length === 0 ? (
<div className="rounded-lg border border-dashed border-slate-300 p-8 text-center">
<FileWarning className="mx-auto h-12 w-12 text-slate-300"/>
<p className="mt-2 text-sm text-slate-500">暂无文件事件</p>
</div>
) : (
<div className="space-y-2">
{events.map((event) => (
<div
key={event.id}
className="rounded-lg border border-slate-200 bg-white p-4"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2">
<span
className="rounded bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-700">
{event.operation}
</span>
<span className="font-mono text-sm text-slate-700">{event.path}</span>
</div>
<p className="mt-1 text-xs text-slate-500">{event.details}</p>
</div>
<span className="text-xs text-slate-400">{formatTime(event.timestamp)}</span>
</div>
</div>
))}
</div>
)}
{/* 分页 */}
{eventsTotal > 20 && (
<div className="flex justify-center gap-2">
<button
onClick={() => loadEvents(eventsPage - 1)}
disabled={eventsPage === 1}
className="rounded-lg border border-slate-300 px-3 py-1 text-sm disabled:opacity-50"
>
上一页
</button>
<span className="px-3 py-1 text-sm text-slate-700">
{eventsPage} / {Math.ceil(eventsTotal / 20)}
</span>
<button
onClick={() => loadEvents(eventsPage + 1)}
disabled={eventsPage >= Math.ceil(eventsTotal / 20)}
className="rounded-lg border border-slate-300 px-3 py-1 text-sm disabled:opacity-50"
>
下一页
</button>
</div>
)}
</div>
)}
{/* 告警标签页 */}
{activeTab === 'alerts' && (
<div className="space-y-4">
<div className="text-sm text-slate-500">
共 {alertsTotal} 条告警记录
</div>
{alerts.length === 0 ? (
<div className="rounded-lg border border-dashed border-slate-300 p-8 text-center">
<AlertTriangle className="mx-auto h-12 w-12 text-slate-300"/>
<p className="mt-2 text-sm text-slate-500">暂无属性告警</p>
</div>
) : (
<div className="space-y-2">
{alerts.map((alert) => (
<div
key={alert.id}
className={`rounded-lg border p-4 ${
alert.restored
? 'border-green-200 bg-green-50'
: 'border-red-200 bg-red-50'
}`}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2">
<AlertTriangle
className={`h-4 w-4 ${
alert.restored ? 'text-green-600' : 'text-red-600'
}`}
/>
<span className="font-mono text-sm text-slate-700">{alert.path}</span>
</div>
<p className="mt-1 text-xs text-slate-600">{alert.details}</p>
<p className={`mt-1 text-xs font-medium ${
alert.restored ? 'text-green-600' : 'text-red-600'
}`}>
{alert.restored ? '✓ 已自动恢复' : '✗ 恢复失败'}
</p>
</div>
<span className="text-xs text-slate-400">{formatTime(alert.timestamp)}</span>
</div>
</div>
))}
</div>
)}
{/* 分页 */}
{alertsTotal > 20 && (
<div className="flex justify-center gap-2">
<button
onClick={() => loadAlerts(alertsPage - 1)}
disabled={alertsPage === 1}
className="rounded-lg border border-slate-300 px-3 py-1 text-sm disabled:opacity-50"
>
上一页
</button>
<span className="px-3 py-1 text-sm text-slate-700">
{alertsPage} / {Math.ceil(alertsTotal / 20)}
</span>
<button
onClick={() => loadAlerts(alertsPage + 1)}
disabled={alertsPage >= Math.ceil(alertsTotal / 20)}
className="rounded-lg border border-slate-300 px-3 py-1 text-sm disabled:opacity-50"
>
下一页
</button>
</div>
)}
</div>
)}
{/* 事件列表面板 */}
{activeTab === 'events' && <TamperProtectionEvents agentId={agentId}/>}
</div>
);
};
@@ -0,0 +1,190 @@
import React, {useEffect, useState} from 'react';
import {Alert, App, Button, Card, Form, Input, List, Space, Switch} from 'antd';
import {Plus, Save, Shield, Trash2} from 'lucide-react';
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
import {getTamperConfig, updateTamperConfig} from '@/api/tamper';
import {getErrorMessage} from '@/lib/utils';
interface TamperProtectionConfigProps {
agentId: string;
}
const TamperProtectionConfig: React.FC<TamperProtectionConfigProps> = ({agentId}) => {
const {message} = App.useApp();
const [form] = Form.useForm();
const queryClient = useQueryClient();
const [editPaths, setEditPaths] = useState<string[]>([]);
const [newPath, setNewPath] = useState('');
// 获取防篡改配置
const {data: config, isLoading} = useQuery({
queryKey: ['tamperConfig', agentId],
queryFn: async () => {
const response = await getTamperConfig(agentId);
if (response.data.success && response.data.data) {
return response.data.data;
}
return null;
},
});
// 保存配置 mutation
const saveMutation = useMutation({
mutationFn: async () => {
const values = form.getFieldsValue();
return updateTamperConfig(agentId, values.enabled, editPaths);
},
onSuccess: () => {
message.success('配置已保存');
queryClient.invalidateQueries({queryKey: ['tamperConfig', agentId]});
},
onError: (error: unknown) => {
console.error('Failed to save config:', error);
message.error(getErrorMessage(error, '配置保存失败'));
},
});
// 添加路径
const handleAddPath = () => {
if (newPath.trim() && !editPaths.includes(newPath.trim())) {
setEditPaths([...editPaths, newPath.trim()]);
setNewPath('');
}
};
// 删除路径
const handleRemovePath = (path: string) => {
setEditPaths(editPaths.filter(p => p !== path));
};
// 初始化表单值
useEffect(() => {
if (config) {
setEditPaths(config.paths || []);
form.setFieldsValue({
enabled: config.enabled || false,
});
} else {
setEditPaths([]);
form.setFieldsValue({
enabled: false,
});
}
}, [config, form]);
return (
<Card
title={
<div className="flex items-center gap-2">
<Shield size={18}/>
<span>防篡改保护配置</span>
</div>
}
extra={
<Button
type="primary"
icon={<Save size={16}/>}
onClick={() => saveMutation.mutate()}
loading={saveMutation.isPending}
>
保存配置
</Button>
}
loading={isLoading}
>
<Space orientation="vertical" style={{width: '100%'}} size="large">
<Alert
title="防篡改保护通过设置目录的不可变属性来防止文件被修改、删除或重命名。配置更新后将实时同步到探针。"
type="info"
showIcon
icon={<Shield size={16}/>}
/>
<Form
form={form}
layout="vertical"
initialValues={{
enabled: false,
}}
>
<Form.Item
label="启用防篡改保护"
name="enabled"
valuePropName="checked"
extra="开启后将对配置的目录进行实时监控和保护"
>
<Switch
checkedChildren="已启用"
unCheckedChildren="已禁用"
/>
</Form.Item>
</Form>
<div>
<div className="mb-2 text-sm font-medium">受保护的目录列表</div>
<Space.Compact style={{width: '100%'}} className="mb-3">
<Input
placeholder="输入要保护的目录路径,如 /etc/nginx"
value={newPath}
onChange={(e) => setNewPath(e.target.value)}
onPressEnter={handleAddPath}
/>
<Button
type="primary"
icon={<Plus size={16}/>}
onClick={handleAddPath}
>
添加
</Button>
</Space.Compact>
{editPaths.length === 0 ? (
<div className="rounded border border-dashed border-gray-300 p-8 text-center">
<Shield size={48} className="mx-auto mb-2 text-gray-300"/>
<p className="text-sm text-gray-500">暂未配置保护目录</p>
</div>
) : (
<List
bordered
dataSource={editPaths}
renderItem={(path) => (
<List.Item
actions={[
<Button
key="delete"
type="text"
danger
icon={<Trash2 size={16}/>}
onClick={() => handleRemovePath(path)}
/>,
]}
>
<span className="font-mono text-sm">{path}</span>
</List.Item>
)}
/>
)}
</div>
{config?.applyStatus && (
<Alert
title={
config.applyStatus === 'success' ? '配置应用成功' :
config.applyStatus === 'failed' ? '配置应用失败' :
'配置应用中...'
}
description={config.applyMessage}
type={
config.applyStatus === 'success' ? 'success' :
config.applyStatus === 'failed' ? 'error' :
'info'
}
showIcon
/>
)}
</Space>
</Card>
);
};
export default TamperProtectionConfig;
@@ -0,0 +1,183 @@
import React, {useRef} from 'react';
import {App, Button, Tag, Tooltip} from 'antd';
import {FileWarning} from 'lucide-react';
import type {ActionType, ProColumns} from '@ant-design/pro-table';
import ProTable from '@ant-design/pro-table';
import {useMutation} from '@tanstack/react-query';
import {deleteTamperEvents, getTamperEvents, type TamperEvent} from '@/api/tamper';
import {getErrorMessage} from '@/lib/utils';
import dayjs from 'dayjs';
interface TamperProtectionEventsProps {
agentId: string;
}
const TamperProtectionEvents: React.FC<TamperProtectionEventsProps> = ({agentId}) => {
const {message, modal} = App.useApp();
const actionRef = useRef<ActionType>(null);
// 定义表格列
const columns: ProColumns<TamperEvent>[] = [
{
title: '时间',
dataIndex: 'timestamp',
key: 'timestamp',
width: 180,
valueType: 'dateTime',
hideInSearch: true,
render: (_, record) => (
<span className="text-sm">
{dayjs(record.timestamp).format('YYYY-MM-DD HH:mm:ss')}
</span>
),
},
{
title: '操作类型',
dataIndex: 'operation',
key: 'operation',
width: 120,
valueType: 'select',
valueEnum: {
CREATE: {text: '创建', status: 'Processing'},
MODIFY: {text: '修改', status: 'Warning'},
DELETE: {text: '删除', status: 'Error'},
RENAME: {text: '重命名', status: 'Default'},
CHMOD: {text: '权限变更', status: 'Default'},
},
render: (_, record) => {
const operationColors: Record<string, string> = {
CREATE: 'blue',
MODIFY: 'orange',
DELETE: 'red',
RENAME: 'purple',
CHMOD: 'cyan',
};
return (
<Tag color={operationColors[record.operation] || 'default'}>
{record.operation}
</Tag>
);
},
},
{
title: '文件路径',
dataIndex: 'path',
key: 'path',
ellipsis: true,
render: (_, record) => (
<Tooltip title={record.path}>
<span className="font-mono text-sm">{record.path}</span>
</Tooltip>
),
},
{
title: '详细信息',
dataIndex: 'details',
key: 'details',
hideInSearch: true,
ellipsis: true,
render: (_, record) => (
record.details ? (
<Tooltip title={record.details}>
<span className="text-xs text-gray-600">{record.details}</span>
</Tooltip>
) : '-'
),
},
];
// 删除所有事件 mutation
const deleteMutation = useMutation({
mutationFn: () => deleteTamperEvents(agentId),
onSuccess: () => {
message.success('所有事件已删除');
actionRef.current?.reload();
},
onError: (error: unknown) => {
console.error('Failed to delete tamper events:', error);
message.error(getErrorMessage(error, '删除失败'));
},
});
// 删除所有事件
const handleDeleteAllEvents = () => {
modal.confirm({
title: '确认删除',
content: '确定要删除该探针的所有防篡改事件吗?此操作不可恢复。',
okText: '确定删除',
okType: 'danger',
cancelText: '取消',
onOk: () => deleteMutation.mutate(),
});
};
return (
<ProTable<TamperEvent>
columns={columns}
actionRef={actionRef}
cardBordered
request={async (params) => {
try {
const response = await getTamperEvents(
agentId,
params.current || 1,
params.pageSize || 20
);
if (response.data.success) {
return {
data: response.data.data.items || [],
success: true,
total: response.data.data.total || 0,
};
}
return {
data: [],
success: false,
total: 0,
};
} catch (error) {
console.error('Failed to load tamper events:', error);
return {
data: [],
success: false,
total: 0,
};
}
}}
rowKey="id"
search={{
labelWidth: 'auto',
}}
pagination={{
pageSize: 20,
showSizeChanger: true,
showTotal: (total) => `共 ${total} 条`,
}}
dateFormatter="string"
headerTitle="文件事件"
toolBarRender={() => [
<Tooltip key="delete" title="删除所有事件">
<Button
onClick={handleDeleteAllEvents}
danger={true}
>
删除所有事件
</Button>
</Tooltip>,
]}
locale={{
emptyText: (
<div className="py-8 text-center text-gray-500">
<FileWarning size={48} className="mx-auto mb-2 opacity-20"/>
<p>暂无防篡改事件</p>
<p className="text-sm mt-2">
请先在"保护配置"中启用保护功能并配置目录
</p>
</div>
),
}}
/>
);
};
export default TamperProtectionEvents;
+2
View File
@@ -483,6 +483,8 @@ export interface SSHLoginConfig {
agentId: string;
enabled: boolean;
recordFailed: boolean;
applyStatus?: string; // 配置应用状态: success/failed/pending
applyMessage?: string; // 应用结果消息
createdAt?: number;
updatedAt?: number;
}