调整目录保护功能和 SSH 登录监控
This commit is contained in:
1 parent
7386c6e8dd
commit
911c105414
23 files changed
+762
-914
No files matched your search
+12
-16
@@ -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 登录事件
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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 创建事件记录
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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`);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -483,6 +483,8 @@ export interface SSHLoginConfig {
|
||||
agentId: string;
|
||||
enabled: boolean;
|
||||
recordFailed: boolean;
|
||||
applyStatus?: string; // 配置应用状态: success/failed/pending
|
||||
applyMessage?: string; // 应用结果消息
|
||||
createdAt?: number;
|
||||
updatedAt?: number;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user